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)
在第 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
.
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()
是一个返回复合类型的函数,其中包含列a
、b
和c
,那么这两个查询具有相同的结果
SELECT (myfunc(x)).* FROM some_table;
SELECT (myfunc(x)).a, (myfunc(x)).b, (myfunc(x)).c FROM some_table;
提示
PostgreSQL通过实际将第一种形式转换为第二种形式来处理列展开。因此,在此示例中,myfunc()
将使用任一语法对每行调用三次。如果这是一个昂贵的函数,你可能希望避免这种情况,你可以使用类似这样的查询将函数放在LATERAL``FROM
项中可以防止它对每行调用多次。m.*
仍然展开为m.a, m.b, m.c
,但现在这些变量只是对FROM
项输出的引用。(LATERAL
关键字在这里是可选的,但我们显示它是为了阐明该函数从some_table
中获取x
。)
当*composite_value
*.*
语法出现在SELECT
输出列表、INSERT
/UPDATE
/DELETE
中的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.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
的行构造函数。)
与复合值关联的另一个特殊语法行为是,我们可以使用函数符号来提取复合值的一个字段。解释这种现象的简单方法是,符号*
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
字段,您需要编写字符串文字处理器会删除一层反斜杠,以便到达复合值解析器的内容看起来像("\"\\")
。反过来,馈送到text
数据类型的输入例程的字符串变为"\
。(如果我们使用输入例程也特殊处理反斜杠的数据类型,例如bytea
,我们可能需要在命令中使用多达八个反斜杠才能将一个反斜杠放入存储的复合字段中。)美元引用(请参阅第 4.1.2.4 节)可用于避免需要加倍反斜杠。
提示
在 SQL 命令中编写复合值时,ROW
构造器语法通常比复合文字语法更容易使用。在ROW
中,单个字段值以与它们不是复合成员时编写的相同方式编写。