Postgresql 中文操作指南

8.16. Composite Types #

composite type 表示行或记录的结构;它本质上只是一份字段名称及其数据类型列表。PostgreSQL 允许复合类型以与简单类型相同的方式使用。例如,可以将表的列声明为复合类型。

8.16.1. Declaration of Composite Types #

以下是定义复合类型两个简单的示例:

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 复合类型将作为副产品而生成,并且可以像上面一样使用。但是,请注意当前实现的一个重要限制:由于没有约束与复合类型相关联,因此表定义 do not apply 中显示的约束对表外部复合类型的取值无效。(要解决此问题,请创建复合类型上的 [role="bare"]glossary.html#GLOSSARY-DOMAIN domain ,并将所需的约束应用为该域的 CHECK 约束。)

8.16.2. Constructing Composite Values #

要将复合值写入为文字常量,请将字段值括在括号中并用逗号分隔。您可以在任何字段值周围加上双引号,如果它包含逗号或括号,则必须这样做。(更多详细信息请参见 below)。因此,复合常数的通用格式如下:

'( val1 , val2 , ... )'

一个示例是:

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

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

'("fuzzy dice",42,)'

如果你想要一个空字符串而不是 NULL,请写入双引号:

'("",42,)'

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

(这些常量实际上只是 Section 4.1.2.7 中讨论的泛型类型常量的一个特例。最初将该常量视为一个字符串,并将其传递给复合类型输入转换例程。明确的类型规范可能が必要,以告诉将常量转换为哪种类型。)

也可以使用 ROW 表达式语法来构造复合值。在大多数情况下,这比字符串字面量语法简单得多,因为你无需担心多层引号。我们已经在上面使用了此方法:

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

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

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

ROW 表达式语法在 Section 4.2.13 中进行了更详细的讨论。

8.16.3. Accessing Composite Types #

要访问复合列的字段,请写一个点和字段名称,就像从表名中选择一个字段一样。事实上,它很像从表名中选择,所以你通常必须使用括号来避免混淆解析器。例如,你可以尝试通过如下方式从我们的 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. Modifying Composite Types #

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

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. Using Composite Types in Queries #

查询中与复合类型关联的特殊语法规则和行为。这些规则提供了有用的捷径,但如果你不知道它们背后的逻辑,可能会感到困惑。

在 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() 是一个返回复合类型的函数,其中包含列 ab、和 c,那么这些两个查询具有相同的结果:

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 output listINSERT / UPDATE / DELETE 中的 RETURNING listVALUES 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.24.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 的行构造器。)

与复合值关联的另一个特殊语法行为是我们可以使用 functional notation 提取复合值的一个字段。解释此方法的简单方法是 field(table)_ and _ 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. Composite Type Input and Output Syntax #

复合值的外部文本表示形式由根据各个字段类型 I/O 转换规则解释的项目组成,加上指示复合结构的装饰。装饰包括圆括号 (()),以及相邻项目之间的逗号 (,)。圆括号外的空格会被忽略,但在括号内它会被视为字段值的一部分,并且根据字段数据类型的输入转换规则,它可能会有或没有意义。例如,在:

'(  42)'

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

如前所述,在编写复合值时,可以在任何单个字段值周围加上双引号。如果你 must 这样做,字段值可能会混淆复合值解析器。特别是,包含圆括号、逗号、双引号或反斜杠的字段必须用双引号引起来。要在引用的复合字段值中放置双引号或反斜杠,请在其前面加上一个反斜杠。(此外,双引号字段值中的双引号对被视为表示一个双引号字符,类似于 SQL 文本字符串中单引号的规则。)或者,你可以避免使用引号并使用反斜杠转义来保护所有否则会被视为复合语法的字符。

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

如果复合输出例程是空字符串或包含圆括号、逗号、双引号、反斜杠或空格,它将用双引号将字段值引起来。(对空格进行此类处理不是必要的,但有助于提高可读性。)嵌入在字段值中的双引号和反斜杠将被加倍。

Note

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

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

字符串字面量处理器移除一级反斜杠,以便到达复合值解析器的内容看起来像 ("\"\\")。反过来,馈送至 text 数据类型输入例程的字符串变为 "\。(如果我们正在处理一个输入例程也专门处理反斜杠的数据类型,例如 bytea,则我们可能需要八个反斜杠在命令中输入一个反斜杠进入存储的复合字段。)可以使用 Dollar 引用(请参阅 Section 4.1.2.4)来避免需要加倍反斜杠。

Tip

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