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);

or functions:

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. 构建综合价值

要将复合值写为 Literals 常量,请将字段值括在括号中,并用逗号分隔。您可以在任何字段值两边加上双引号,并且如果包含逗号或括号,则必须使用双引号。 (更多细节出现在below上。)因此,复合常量的一般格式如下:

'( val1 , val2 , ... )'

一个例子是:

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

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

'("fuzzy dice",42,)'

如果要使用空字符串而不是 NULL,请写双引号:

'("",42,)'

这里的第一个字段是一个非 NULL 的空字符串,第三个字段是 NULL。

(这些常量实际上只是Section 4.1.2.7中讨论的泛型类型常量的一种特殊情况。该常量最初被视为字符串,并传递给复合类型 Importing 转换例程。明确的类型说明可能需要告诉要转换的类型的常数。)

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

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

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

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

Section 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 ...

如果没有多余的括号,将产生语法错误。

特殊字段名称*表示“所有字段”,如Section 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);

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

8 .16.5. 在查询中使用复合类型

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

在 PostgreSQL 中,对查询中表名(或别名)的引用实际上是对表当前行的组合值的引用。例如,如果我们有一个表inventory_item,如above所示,我们可以这样写:

SELECT c FROM inventory_item c;

该查询产生单个复合值列,因此我们可能会得到类似以下的输出:

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

但是请注意,简单名称与表名称之前的列名称匹配,因此此示例仅适用于查询表中没有名为c的列。

普通的限定列名称语法* table_name * . * column_name *可以理解为将field selection应用于表的当前行的复合值。 (出于效率考虑,实际上并没有采用这种方式.)

当我们写

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 会将这种扩展行为应用于任何复合值表达式,尽管如图above所示,只要不是简单的表名,就需要在.*所应用的值周围写上括号。例如,如果myfunc()是返回包含列abc的复合类型的函数,则这两个查询的结果相同:

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

Tip

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/DELETEVALUES clauserow constructor的顶层时,会导致这种列扩展。在所有其他上下文中(包括嵌套在这些结构之一中),将.*附加到复合值不会更改该值,因为它表示“所有列”,因此会再次生成相同的复合值。例如,如果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子句都指定行的复合值,从而根据Section 9.23.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不是表的实际列。

Tip

由于这种行为,给一个采用单个复合类型实参与该复合类型的任何字段同名的函数是不明智的。如果存在歧义,则如果使用字段名语法,则将选择字段名解释,而如果使用函数调用语法,则将选择函数。但是,PostgreSQL 11 之前的版本始终选择字段名解释,除非调用的语法要求它是函数调用。强制在较旧版本中进行函数解释的一种方法是对函数名称进行模式限定,即写入schema.func(compositevalue)

8 .16.6. 复合类型 Importing 和输出语法

复合值的外部文本表示由根据各个字段类型的 I/O 转换规则解释的项目以及表示复合结构的修饰组成。装饰由围绕整个值的括号(())以及相邻项目之间的逗号(,)组成。括号外的空格将被忽略,但括号内的空格将被视为字段值的一部分,并且取决于字段数据类型的 Importing 转换规则,该空格可能有效也可能无效。例如,在:

'(  42)'

如果字段类型是整数,则将忽略空格,但如果是文本,则不会忽略空格。

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

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

如果复合输出例程为空字符串或包含括号,逗号,双引号,反斜杠或空格,则复合输出例程将在字段值两边加上双引号。 (这样做对空格不是必需的,但有助于提高可读性.)字段值中嵌入的双引号和反斜杠将加倍。

Note

请记住,您在 SQL 命令中编写的内容将首先解释为字符串 Literals,然后再解释为组合。这会使所需的反斜杠数量加倍(假设使用了转义字符串语法)。例如,要在复合值中插入包含双引号和反斜杠的text字段,您需要编写:

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

字符串字面量处理器除去一级反斜杠,以便到达复合值解析器的内容看起来像("\"\\")。反过来,馈给text数据类型的 Importing 例程的字符串变为"\。 (如果使用的数据类型的 Importing 例程也对反斜杠进行了特殊处理,例如bytea,则在命令中可能需要多达 8 个反斜杠才能将一个反斜杠放入存储的复合字段中.)美元引号(请参见Section 4.1.2.4)可以用于避免需要加倍反斜杠。

Tip

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