Redrock Postgres 搜索 英文
版本: 9.3 / 9.4 / 9.5 / 9.6 / 10 / 11 / 12 / 13 / 14 / 15 / 16 / 17

8.16 复合类型 #

8.16.1. 复合类型的声明
8.16.2. 构建复合值
8.16.3. 访问复合类型
8.16.4. 修改复合类型
8.16.5. 在查询中使用复合类型
8.16.6. 复合类型输入和输出语法

复合类型表示一行的结构或记录;它本质上只是一列字段名与其数据类型。PostgreSQL允许复合类型应用于许多能够使用简单类型的地方中。例如,可声明某表的列为复合类型。

8.16.1. 复合类型声明 #

下面是定义复合类型两个简单示例

CREATE TYPE complex AS (
    r       double precision,
    i       double precision
);

CREATE TYPE inventory_item AS (
    name            text,
    supplier_id     integer,
    price           numeric
);

语法与 CREATE TABLE 类似,只是可以指定的只有字段名和类型;目前不能包括任何约束(如 NOT NULL)。注意 AS 关键字必不可少;如果没有,系统则会认为这是另一种 CREATE TYPE 命令,您将得到奇怪的语法错误。

完成类型定义后,可以使用它们创建表

CREATE TABLE on_hand (
    item      inventory_item,
    count     integer
);

INSERT INTO on_hand VALUES (ROW('fuzzy dice', 42, 1.99), 1000);

或函数

CREATE FUNCTION price_extension(inventory_item, integer) RETURNS numeric
AS 'SELECT $1.price * $2' LANGUAGE SQL;

SELECT price_extension(item, 10) FROM on_hand;

每当您创建表时,还将自动创建一个复合类型,其名称与表名相同,代表表的行类型。例如,如果我们说

CREATE TABLE inventory_item (
    name            text,
    supplier_id     integer REFERENCES suppliers,
    price           numeric CHECK (price > 0)
);

那么上面所示的 inventory_item 复合类型将作为衍生产品出现,并可像上面那样使用。但请注意当前实现的一个重要限制:由于与复合类型不关联任何约束,因此表定义中所示的约束不适用于表外部复合类型的值。(要解决此问题,请针对复合类型创建一个,并应用所需的约束作为域的 CHECK 约束。)

8.16.2. 构造复合值 #

要以文本常量编写复合值,请使用括号将字段值括起来,并用逗号分隔。可以在任何字段值周围加上双引号,如果字段值包含逗号或括号,必须加上双引号。(有关更多详细信息,请参见 此处。)因此,复合常量的总体格式如下

'( val1 , val2 , ... )'

示例如下

'("fuzzy dice",42,1.99)'

这将是上面所定义 inventory_item 类型的有效值。若要使字段为 NULL,请在其在列表中的位置上不编写任何字符。例如,此常量指定第三个字段为 NULL

'("fuzzy dice",42,)'

如果要得到一个空字符串而不是 NULL,请输入双引号

'("",42,)'

此处第一个字段是一个非空空字符串,第三个是 NULL。

(这些常量实际上只是通用类型常量的特例,如第 4.1.2.7 节所述。该常量最初是作为一个字符串进行处理,并传递到复合类型输入转换例程。可能需要明确指定类型来告诉例程将常量转换成哪种类型。)

还可以使用ROW表达式语法构造复合值。在大多数情况下,这比字符串字面量语法要容易得多,因为不必担心多个引号层次。我们已经在上面使用了此方法

ROW('fuzzy dice', 42, 1.99)
ROW('', 42, NULL)

只要表达式中有两个以上的字段,ROW 关键字实际上是可选的,因此可以将其简化为

('fuzzy dice', 42, 1.99)
('', 42, NULL)

第 4.2.13 节中更详细地讨论了ROW表达式语法。

8.16.3. 访问复合类型 #

要访问复合列的一个字段,可以写上一个点和字段名称,就像从表名中选择一个字段一样。事实上,这非常类似于从表名中进行选择,您经常必须使用括号来避免混淆解析器。例如,您可以尝试从我们的on_hand示例表中选择一些子字段,类似于

SELECT item.name FROM on_hand WHERE item.price > 9.99;

这不起作用,因为根据 SQL 语法规则,名称item被认为是一个表名,而不是on_hand的列名。您必须这样写

SELECT (item).name FROM on_hand WHERE (item).price > 9.99;

或者,如果需要使用表名(例如在多表查询中),请像这样写

SELECT (on_hand.item).name FROM on_hand WHERE (on_hand.item).price > 9.99;

现在括号中的对象被正确地解释为对item列的引用,然后可以从中选择子字段。

无论您何时从复合值中选择一个字段,都会应用类似的语法问题。例如,要仅从返回复合值的函数结果中选择一个字段,您需要写类似于

SELECT (my_func(...)).field FROM ...

如果没有额外的括号,这将生成一个语法错误。

特殊字段名称*表示 所有字段,如第 8.16.5 节中进一步解释的。

8.16.4. 修改复合类型 #

以下是插入和更新复合列的正确语法的示例。首先,插入或更新整个列

INSERT INTO mytab (complex_col) VALUES((1.1,2.2));

UPDATE mytab SET complex_col = ROW(1.1,2.2) WHERE ...;

第一个示例省略了 ROW,第二个示例使用了它;我们可以在两种方式中选择一种方式。

我们能更新复合列的特定子字段

UPDATE mytab SET complex_col.r = (complex_col).r + 1 WHERE ...;

在此处请注意,我们不必也不可能在 SET 之后出现的列名周围加括号,但我们确实需要在等号右边的表达式中引用相同列时加上括号。

我们也能指定子字段作为 INSERT 的目标

INSERT INTO mytab (complex_col.r, complex_col.i) VALUES(1.1, 2.2);

如果我们未为该列的所有子字段提供值,那么其余子字段将填充为 null 值。

8.16.5. 在查询中使用复合类型 #

查询中的复合类型存在多种特殊语法规则和行为。这些规则提供了有用的快捷方式,但如果你不知道其背后的逻辑,可能会造成困惑。

PostgreSQL 中,查询中对表名(或别名)的引用实际上是对表当前行的复合值的引用。举例而言,如果我们有一个表 inventory_item 如上文 所述,我们可以编写

SELECT c FROM inventory_item c;

此查询生成一个复合值列,因此我们可能会获得以下输出结果

           c
------------------------
 ("fuzzy dice",42,1.99)
(1 row)

不过,请注意,在与表格名称匹配之前,简单的名称与列名称相匹配,因此此示例仅在查询的表格中没有名为 c 的列时才起作用。

普通的限定列名语法 table_name.coluna_name 可以被理解为将 字段选择 应用于表格当前行的复合值。(出于效率的原因,实际上不是以这种方式实现的。)

当我们编写

SELECT c.* FROM inventory_item c;

那么,根据 SQL 标准,我们应将表格的内容扩展到单独的列中

    name    | supplier_id | price
------------+-------------+-------
 fuzzy dice |          42 |  1.99
(1 row)

就好象该查询是

SELECT c.name, c.supplier_id, c.price FROM inventory_item c;

PostgreSQL 会将此展开行为应用于任何复合值表达式,虽然如 上面所示,你需要在 .* 应用到任意非简单表名时,在需要应用的值周围加上括号。例如,如果 myfunc() 是返回带有列 abc 的复合类型的函数,则以下两个查询具有相同的结果

SELECT (myfunc(x)).* FROM some_table;
SELECT (myfunc(x)).a, (myfunc(x)).b, (myfunc(x)).c FROM some_table;

提示

PostgreSQL 会通过事实上将第一种形式转换成第二种形式来处理列展开。因此,在此示例中,对于每行,myfunc() 采用任一语法都会被调用三次。如果它是一个昂贵的函数,你可能希望避免这一点,你可以使用以下查询:

SELECT m.* FROM some_table, LATERAL myfunc(x) AS m;

将函数放置在 LATERAL FROM 项目中可以防止函数被每个行调用超过一次。m.* 仍然展开为 m.a, m.b, m.c,但现在这些变量仅仅是对 FROM 项目输出的引用。(此处 LATERAL 关键字是可选的,但我们显示它以澄清该函数从 some_table 中获取 x。)

composite_value.* 语法出现在 SELECT 输出列表INSERT/UPDATE/DELETE/MERGE 中的 RETURNING 列表VALUES 子句行构造器 的顶层时,它会导致这种形式的列展开。在所有其它上下文中(包括嵌套在其中一个结构内时),将 .* 附加到复合值不会更改该值,因为它表示“所有列”,因此会再次产生相同的复合值。例如,如果 somefunc() 接受复合值参数,则以下查询相同

SELECT somefunc(c.*) FROM inventory_item c;
SELECT somefunc(c) FROM inventory_item c;

在两种情况下,inventory_item 的当前行是以单个复合值自变量的形式传递给函数的。虽然 .* 在这种情况下没有作用,但使用它是一种良好的风格,因为它明确表明预期的是一个复合值。特别是,解析器会将 c 中的 c.* 视为是指表名或别名,而不是列名,所以没有歧义;而如果缺少 .*,则无法清楚是 c 指代表名还是列名,实际上如果存在名为 c 的列,则会优先采用列名解析。

展示这些概念的另一个示例是所有这些查询的含义相同:

SELECT * FROM inventory_item c ORDER BY c;
SELECT * FROM inventory_item c ORDER BY c.*;
SELECT * FROM inventory_item c ORDER BY ROW(c.*);

所有这些 ORDER BY 子句都指定行的复合值,并按照 第 9.25.6 节 中所述的规则对行进行排序。但是,如果 inventory_item 包含一个名为 c 的列,则第一种情况将不同于其他情况,因为这意味着只按该列进行排序。根据之前显示的列名,这些查询也等同于上述查询。

SELECT * FROM inventory_item c ORDER BY ROW(c.name, c.supplier_id, c.price);
SELECT * FROM inventory_item c ORDER BY (c.name, c.supplier_id, c.price);

(最后一种情况使用了一个省略关键字 ROW 的行构造器。)

与复合值相关的另一个特殊语法行为是,我们可以使用函数符号来提取复合值中的字段。解释这种行为的最简单方法是,符号 field(table)table.field 是可以互换的。例如,以下查询是等效的

SELECT c.name FROM inventory_item c WHERE c.price > 1000;
SELECT name(c) FROM inventory_item c WHERE price(c) > 1000;

此外,如果我们有一个接受复合类型单个参数的函数,则可以使用任一符号调用它。所有这些查询都是等效的

SELECT somefunc(c) FROM inventory_item c;
SELECT somefunc(c.*) FROM inventory_item c;
SELECT c.somefunc FROM inventory_item c;

函数符号和字段符号之间的这种等效性使得对复合类型的函数进行使用成为实现“计算字段”成为可能。 使用上述最后一条查询的应用程序无须直接了解到 somefunc 并不是该表的一个真实列。

提示

由于这种行为,给函数取值一个复合类型变量的名字与复合类型的某个字段的字段名相同时,是不可取的。如果出现歧义,当使用字段名语法时,会选取字段名解释,当使用函数调用语法时,会选取函数。然而,版本 11 以前的PostgreSQL总是选取字段名解释,除非调用的语法要求为函数调用。强制在旧版本中解释函数的一种方法是对函数名进行架构限定,即编写schema.func(compositevalue)

8.16.6. 复合类型输入和输出语法 #

复合值的外部文本表示由根据各个字段类型的 I/O 转换规则解释的项组成,加上指示复合结构的装饰。装饰包括括号 (()) 围绕整个值,加上相邻项之间的逗号 (,)。括号外的空格被忽略,但在括号内被视为字段值的一部分,并且根据字段数据类型的输入转换规则,该值可能或可能不重要。例如,在中

'(  42)'

如果字段类型为整数,则空白会被忽略,但如果为文本,则不会被忽略。

如前所示,在编写复合值时,可以在任何单个字段值周围加上双引号。如果您必须这样做,否则字段值会导致复合值解析程序混乱。特别是,包含括号、逗号、双引号或反斜杠的字段必须加双引号。要在带引号的复合字段值中放入双引号或反斜杠,请在前面加上反斜杠。(另外,双引号字段值中的双引号对被视为一个双引号字符,类似于 SQL 文本字符串中单引号的规则。)或者,您可以避免使用引号,并使用反斜杠转义来保护所有否则会被视为复合语法的数据字符。

一个完全空字段值(逗号或括号之间没有任何字符)表示 NULL。要编写一个值,该值为一个空字符串而不是 NULL,请编写""

复合输出例程会在字段值为空字符串或包含括号、逗号、双引号、反斜杠或空格时,在这些值周围加上双引号。(对空格这样做不是必要的,但这有助于可读性。)嵌入在字段值中的双引号和反斜杠将加倍。

注意

请记住,你在 SQL 命令中编写的内容首先会被解析为字符串文字,然后解析为复合数据类型。这让你需要两倍数量的反斜杠(假定使用了转义符字符串语法)。例如,要插入一个包含双引号和反斜杠的 text 域到复合值中,你会需要这样做

INSERT ... VALUES ('("\"\\")');

字符串文字处理器会移除一层反斜杠,因此复合值解析器接收到的内容看起来像 ("\"\\")。然后,输入 text 数据类型输入例程的字符串变成了 "\。(如果我们要使用输入例程也处理反斜杠的数据类型,例如 bytea,我们可能需要在命令中使用多达八个反斜杠才能将一个反斜杠放入存储的复合域中。)美元引用(请参阅 第 4.1.2.4 节)可以用于避免需要双重反斜杠。

提示

在 SQL 命令中编写复合值时,与复合文字语法相比,ROW 构造器语法通常更易于使用。在 ROW 中,单个字段值以非复合值成员的方式编写。