组合类型 表示一行或一个记录的结构;它本质上只是一个字段名及其数据类型的列表。 PostgreSQL 允许在许多与简单类型相同的场景中使用组合类型。例如,表的某一列可以被声明为某种组合类型。
下面是定义组合类型的两个简单示例:
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 约束。)
要把组合值写成字面量常量,把字段值放在圆括号内并用逗号分隔。你可以给任何字段值加上双引号;如果它包含逗号或圆括号,则必须加双引号。(更多细节见下文。)因此,组合常量的一般格式如下:
'(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。
要访问组合列的某个字段,写一个点号加字段名,就像从表名选择字段一样。实际上,它与从表名选择实在太像了,以至于经常必须使用圆括号以免让解析器混淆。例如,你可能想这样从 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 中有进一步说明。
下面是插入与更新组合列的正确语法的一些示例。 首先是插入或更新整列:
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);
如果我们没有为该列的所有子字段提供值,其余子字段将被填入空值。
查询中有各种与组合类型相关的特殊语法规则与行为。这些规则提供了有用的捷径,但如果你不了解其背后的逻辑,就可能感到困惑。
在 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() 三次。如果它是一个昂贵的函数,你可能希望避免这一点,可以像这样写查询:
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 子句都指定了该行的组合值,从而按照
Section 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)
组合值的外部文本表示由按照各字段类型的 I/O 转换规则解释的条目组成,外加指示组合结构的装饰。装饰包括围绕整个值的圆括号(( 与
)),以及相邻条目之间的逗号(,)。圆括号之外的空白会被忽略;但圆括号内的空白被视为字段值的一部分,是否有意义取决于字段数据类型的输入转换规则。例如在:
'( 42)'
中,如果字段类型是 integer,空白会被忽略;如果是 text,则不会。
如前所示,书写组合值时可以给任何单独的字段值加双引号。如果字段值会让组合值解析器混淆,你则必须加双引号。特别地,包含圆括号、逗号、双引号或反斜杠的字段必须加双引号。要在带引号的组合字段值中放入双引号或反斜杠,在它前面加一个反斜杠。(此外,带双引号的字段值中的一对双引号会被视为一个双引号字符,类似于 SQL 字面量字符串中单引号的规则。)或者,你也可以避免加引号,而使用反斜杠转义来保护所有原本会被当作组合语法的数据字符。
完全空的字段值(逗号或圆括号之间没有任何字符)表示 NULL。要写一个空字符串而非 NULL 的值,写
""。
组合输出例程会在字段值为空字符串,或包含圆括号、逗号、双引号、反斜杠或空白时给它们加上双引号。(对空白这样做并非必需,但有助于提高可读性。)字段值中内嵌的双引号与反斜杠会被双写。
请记住,你在 SQL 命令中写的内容会先被解释为字符串字面量,然后再被解释为组合值。这会使你需要的反斜杠数量加倍(假定使用转义字符串语法)。例如,要在组合值中插入一个同时包含双引号与反斜杠的
text 字段,你需要写:
INSERT ... VALUES ('("\"\\")');
字符串字面量处理器会去掉一层反斜杠,因此到达组合值解析器的内容形如
("\"\\")。继而,传给
text 数据类型输入例程的字符串变为
"\。(如果我们处理的数据类型的输入例程同样特殊对待反斜杠,例如
bytea,那么命令中可能需要多达八个反斜杠才能让一个反斜杠进入存储的组合字段。)美元引用(参见
Section 4.1.2.4)可以用来避免把反斜杠双写。
在 SQL 命令中书写组合值时,ROW 构造器语法通常比组合字面量语法更易用。在
ROW 中,单独字段值的书写方式与其不是组合成员时的书写方式相同。