2025年9月25日: PostgreSQL 18 发布!
支持的版本:当前 (18) / 17 / 16 / 15 / 14 / 13
开发版本:devel
不支持的版本:12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0

8.16. 复合类型 #

复合类型 代表一个行或记录的结构;它本质上只是一个字段名及其数据类型的列表。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 的空字符串,第三个字段是 NULL。

(这些常量实际上只是前面讨论的通用类型常量的一个特例 第 4.1.2.7 节。常量最初被视为字符串,然后传递给复合类型输入转换例程。可能需要显式类型规范来告知将常量转换为哪种类型。)

ROW 表达式语法也可用于构造复合值。在大多数情况下,它比字符串字面量语法更容易使用,因为您不必担心多层引号。我们上面已经使用了这种方法:

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

只要表达式中有一个以上的字段,ROW 关键字实际上是可选的,所以这些可以简化为:

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

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

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.column_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/MERGERETURNING 列表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 不是表中的真实列。

提示

由于这种行为,给一个接受单个复合类型参数的函数起一个与该复合类型的任何字段相同的名字是不明智的。如果存在歧义,如果使用字段名语法,则将选择字段名解释;如果使用函数调用语法,则将选择函数。但是,PostgreSQL 11 之前的版本总是选择字段名解释,除非调用语法要求它是一个函数调用。在旧版本中强制进行函数解释的一种方法是为函数名加上模式限定,即写 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 中,单个字段值会按照它们不作为复合值成员时的写法来编写。

提交更正

如果您在文档中看到任何不正确、与您对特定功能的经验不符或需要进一步澄清的内容,请使用 此表单 来报告文档问题。