构造组合值
要把一个组合值写作一个文字常量,将该域值封闭在圆括号中并且用逗号分隔它们。可以在任何域值周围放上双引号,并且如果该域值包含逗号或圆括号则必须这样做。这样,一个组合常量的一般格式是下面这样的:
'( val1 , val2 , ... )'
一个例子是:
'("fuzzy dice",42,1.99)'
这将是上文定义的 inventory_item 类型的一个合法值。要让一个域为 NULL,在列表中它的位置上根本不写字符。例如,这个常量指定其第三个域为 NULL:
'("fuzzy dice",42,)'
如果写一个空字符串而不是 NULL,写上两个引号:
'("",42,)'
这里第一个域是一个非 NULL 空字符串,第三个是 NULL。
ROW 表达式也能被用来构建组合值。在大部分情况下,比起使用字符串语法,这相当简单易用,因为不必担心多层引用。已经在上文用过这种方法:
ROW('fuzzy dice', 42, 1.99)
ROW('', 42, NULL)
只要在表达式中有多于一个域,ROW 关键词实际上就是可选的,因此这些可以被简化成:
('fuzzy dice', 42, 1.99)
('', 42, NULL)
访问组合类型
要访问一个组合列的一个域,可以写成一个点和域的名称,更像从一个表名中选择一个域。事实上,它太像从一个表名中选择,这样不得不使用圆括号来避免让解析器混淆。例如,可能尝试从例子表 on_hand 中选取一些子域:
SELECT item.name FROM on_hand WHERE item.price > 9.99;
这不会有用,因为名称 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 ...
如果没有额外的圆括号,这将生成一个语法错误。
修改组合类型
这里有一些插入和更新组合列的正确语法的例子。首先,插入或者更新一整个列:
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);
如果没有为该列的所有子域提供值,剩下的子域将用空值填充。
在查询中使用组合类型
对于查询中的组合类型有各种特殊的语法规则和行为。这些规则提供了有用的捷径,但是如果不懂背后的逻辑就会被此困扰。
在 AntDB 中,查询中对一个表名(或别名)的引用实际上是对该表的当前行的组合值的引用。例如,如果有一个如上所示的表 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;
尽管如上所示,AntDB 将对任何组合值表达式应用这种展开行为,但只要.*所应用的值不是一个简单的表名,就需要把该值写在圆括号内。例如,如果 myfunc() 是一个返回组合类型的函数,该组合类型由列 a、b 和 c 组成,那么这两个查询有相同的结果:
SELECT (myfunc(x)).* FROM some_table;
SELECT (myfunc(x)).a, (myfunc(x)).b, (myfunc(x)).c FROM some_table;
提示
AntDB 实际上通过将第一种形式转换为第二种来处理列展开。因此,在这个例子中,用两种语法时对每行都会调用 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 中的一个 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.*);
不过,如果 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 不是一个真实的表列。
提示
由于这种行为,让一个接受单一组合类型参数的函数与该组合类型的任意字段具有相同的名称是不明智的。出现歧义时,如果使用了字段名语法,则字段名解释将被选择,而如果使用的是函数调用语法则会选择函数解释。在老的版本中强制函数解释的一种方法是用方案限定函数名,也就是写成 *schema*.*func*(*compositevalue*)。
组合类型输入和输出语法
一个组合值的外部文本表达由根据域类型的 I/O 转换规则解释的项,外加指示组合结构的装饰组成。装饰由整个值周围的圆括号((和)),外加相邻项之间的逗号(,)组成。圆括号之外的空格会被忽略,但是在圆括号之内空格会被当成域值的一部分,并且根据域数据类型的输入转换规则可能有意义,也可能没有意义。例如,在
'( 42)'
中,如果域类型是整数则空格会被忽略,而如果是文本则空格不会被忽略。
如前所示,在写一个组合值时,可以在任意域值周围写上双引号。如果不这样做会让域值迷惑组合值解析器,就必须这么做。特别地,包含圆括号、逗号、双引号或反斜线的域必须用双引号引用。要把一个双引号或者反斜线放在一个被引用的组合域值中,需要在它前面放上一个反斜线(还有,一个双引号引用的域值中的一对双引号被认为是表示一个双引号字符,这和 SQL 字符串中单引号的规则类似)。另一种办法是,可以避免引用以及使用反斜线转义来保护所有可能被当作组合语法的数据字符。
一个全空的域值(在逗号或圆括号之间完全没有字符)表示一个 NULL。要写一个空字符串值而不是 NULL,可以写成""。
如果域值是空串或者包含圆括号、逗号、双引号、反斜线或空格,组合输出例程将在域值周围放上双引号(对空格这样处理并不是不可缺少的,但是可以提高可读性)。嵌入在域值中的双引号及反斜线将被双写。
注意
记住在一个 SQL 命令中写的东西将首先被解释为一个字符串,然后才会被解释为一个组合。这就让所需要的反斜线数量翻倍(假定使用了转义字符串语法)。例如,要在组合值中插入一个含有一个双引号和一个反斜线的 text 域,需要写成:
INSERT ... VALUES ('("\"\\")');
字符串处理器会移除一层反斜线,这样在组合值解析器那里看到的就会是("\"\\")。接着,字符串被交给 text 数据类型的输入例程并且变成"\(如果使用的数据类型的输入例程也会特别处理反斜线,例如 bytea,在命令中可能需要八个反斜线用来在组合域中存储一个反斜线)。美元引用(见第 4.1.2.4 节)可以被用来避免双写反斜线。




