PostgreSQL 18 中文参考文档(未翻译部分保留英文)🌐 切换到英文原版 →

4.2. 值表达式 #

4.2.1. 列引用
4.2.2. 位置参数
4.2.3. 下标
4.2.4. 字段选择
4.2.5. 运算符调用
4.2.6. 函数调用
4.2.7. 聚合表达式
4.2.8. 窗口函数调用
4.2.9. 类型转换
4.2.10. 排序规则表达式
4.2.11. 标量子查询
4.2.12. 数组构造器
4.2.13. 行构造器
4.2.14. 表达式求值规则

值表达式在 SQL 中应用于极其广泛的语境,例如在 SELECT 命令的目标输出列表中、在 INSERT 或 UPDATE 中作为待写入的新列值、以及在诸多命令的搜索过滤条件中。值表达式的计算结果常被称为标量(scalar),以此与表表达式(返回结果为二维表)相区别。因此值表达式也常被称为标量表达式(或直接简称为表达式)。表达式语法允许通过算术运算、逻辑运算、集合操作等对基本元部件进行组合求值。

一个值表达式可以是以下形式之一:

除了上述清单外,SQL 中还存在若干归属于表达式范畴、但未遵循通用语法规则的特殊结构。它们在语义上通常表现为函数或运算符,并在 Chapter 9 中进行了专题说明,典型的代表如 IS NULL 子句。

我们已经在 Section 4.1.2 中讨论了各种常量的语法。以下各节将系统阐述剩余的各类表达式。

4.2.1. 列引用 #

对数据表字段列的引用遵循以下格式:

correlation.columnname

其中 correlation 可以是数据表的名称(可带模式前缀),也可以是通过 FROM 子句定义的表别名。如果该列名在当前查询涉及的所有表中是全局唯一的,则可以省略表名及句点前缀(另见 Chapter 7)。

4.2.2. 位置参数 #

位置参数引用用于指代从外部动态传递给 SQL 语句的数据值。参数主要应用于 SQL 函数体定义以及预备查询语句(prepared queries)中。部分客户端驱动库还支持将数据值与 SQL 文本字符串分离开来独立传递,此时同样使用位置参数引用行外数据。位置参数的形式为:

$number

例如,考虑定义一个查询部门信息的函数 dept:

CREATE FUNCTION dept(text) RETURNS dept
    AS $$ SELECT * FROM dept WHERE name = $1 $$
    LANGUAGE SQL;

在该函数被调用时,$1 引用传递进来的第一个实参的值。

4.2.3. 下标 #

如果一个表达式的计算结果为数组类型,可以通过附加下标提取数组中的特定元素:

expression[subscript]

或者通过声明切片范围提取相邻的一组连续元素(“数组切片”):

expression[lower_subscript:upper_subscript]

(此处的方括号 [ ] 必须字面原样输入)。每个 subscript 本身都是一个独立的表达式,其计算结果会被自动四舍五入为最近的整数。

通常情况下,前置的数组表达式 expression 必须用圆括号包裹;但当被索引的表达式仅仅是一个简单的列名或位置参数时,外层圆括号可以省略。此外,对于多维数组,可以连续级联书写多个方括号下标。例如:

mytable.arraycolumn[4]
mytable.two_d_column[17][34]
$1[10:42]
(arrayfunction(a,b))[42]

最后一个示例中的外层圆括号是强制必需的。关于数组类型的系统介绍,详见 ???。

4.2.4. 字段选择 #

如果一个表达式的计算结果属于复合类型(行类型),可以通过以下语法提取复合值中的指定子字段:

expression.fieldname

通常情况下,行表达式 expression 必须使用圆括号包裹;但如果被选取的对象仅仅是一个简单的表引用或位置参数,圆括号可以省略。例如:

mytable.mycolumn
$1.somecolumn
(rowfunction(a,b)).col3

(由此可见,带表前缀的列引用在语法上本质正是字段选择的一种特例)。一个重要的典型场景是从数据表中本身属于复合类型的某列中提取子字段:

(compositecol).somefield
(mytable.compositecol).somefield

此处的圆括号是必需的,它明确告知解析器 compositecol 是一个列名而非表名,或者在第二种写法中表明 mytable 是表名而非模式名。

你可以通过使用通配符 .* 一次性提取复合值的所有成员字段:

(compositecol).*

这一记法在不同的语法上下文中会有不同的展开表现,详见 ???。

4.2.5. 运算符调用 #

运算符调用主要支持两种语法形态:

expression operator expression(二元中缀运算符)
operator expression(一元前缀运算符)

其中 operator 记号遵循 Section 4.1.3 的命名规则,或者为关键字 AND、OR、NOT 之一,或者为如下带有模式限定的显式运算符格式:

OPERATOR(schema.operatorname)

系统中具体存在哪些可用运算符以及它们是一元还是二元运算符,取决于系统内置定义及用户扩展定义。详见 Chapter 9。

4.2.6. 函数调用 #

函数调用的标准语法为:函数名称(可选带模式前缀),后紧跟由圆括号包裹的参数列表:

function_name ([expression [, expression ... ]] )

例如,计算 2 的平方根:

sqrt(2)

所有系统内置函数均列于 Chapter 9 中,用户也可以随时向系统注册新函数。

在多租户或者存在权限互不信任场景的数据库中执行函数调用时,请严格遵守 Section 10.3 中的安全防护规范。

传递参数时还可以显式带上参数名,详见 Section 4.3。

Note

对于接收单个复合类型参数的函数,可以选择使用字段选择语法来调用;反之,字段选择也可以写为函数式调用风格。换言之,col(table) 与 table.col 在语法上可以完全互换。这一行为并非 SQL 国际标准,但 PostgreSQL 提供了该支持,因为它允许利用函数极其平滑地模拟出数据表的“计算字段”。详情参阅 ???。

4.2.7. 聚合表达式 #

一个聚合表达式(aggregate expression)代表在查询选中的多行数据集合上执行聚合计算。聚合函数将多行输入缩减为一个单一的统计输出,例如求和或求平均值。聚合表达式的语法支持以下形态之一:

aggregate_name (expression [ , ... ] [ order_by_clause ] ) [ FILTER ( WHERE filter_clause ) ]
aggregate_name (ALL expression [ , ... ] [ order_by_clause ] ) [ FILTER ( WHERE filter_clause ) ]
aggregate_name (DISTINCT expression [ , ... ] [ order_by_clause ] ) [ FILTER ( WHERE filter_clause ) ]
aggregate_name ( * ) [ FILTER ( WHERE filter_clause ) ]
aggregate_name ( [ expression [ , ... ] ] ) WITHIN GROUP ( order_by_clause ) [ FILTER ( WHERE filter_clause ) ]

其中 aggregate_name 是已注册的聚合函数名(可带模式前缀),而 expression 是任何自身不包含嵌套聚合表达式或窗口函数调用的普通值表达式。可选的 order_by_clause 与 filter_clause 说明如下。

第一种形式针对每个输入行调用一次聚合计算。第二种形式与第一种完全相同,因为 ALL 是系统的默认行为。第三种形式对输入行中出现的各唯一不同表达式值(当有多个参数时指唯一的参数组合)仅调用一次聚合计算。第四种形式针对每个输入行无差别调用一次聚合,由于没有传递具体的值表达式,它通常专用于 count(*) 行数统计。最后一种形式用于有序集(ordered-set)聚合函数。

绝大多数聚合函数会自动忽略 NULL 输入值,因此包含空值的行会被直接丢弃。除非另有明确声明,所有系统内置聚合函数均遵循这一规则。

例如,count(*) 统计输入行的总物理行数;count(f1) 统计其中 f1 字段为非空的行数;而 count(distinct f1) 则统计 f1 的非空唯一值个数。

在常规情况下,数据行输入给聚合函数的顺序是未定义的。在很多场景下这不会产生影响;例如无论以何种顺序输入数据,min 算出的最小值都是完全相同的。然而部分聚合函数(如 array_agg 与 string_agg)的输出结果高度依赖于输入行的排列次序。在使用这类聚合函数时,可以通过附加可选的 order_by_clause 来显式指定排序规则。其语法与查询级的 ORDER BY 完全一致(详见 Section 7.5),区别在于其表达式必须是普通的计算表达式,不能使用外部输出列的别名或序号。例如:

WITH vals (v) AS ( VALUES (1),(3),(4),(3),(2) )
SELECT array_agg(v ORDER BY v DESC) FROM vals;
  array_agg
-------------
 {4,3,3,2,1}

再如 jsonb 在遇到同名键时仅保留最后一条记录,因此其键值的输入排序至关重要:

WITH vals (k, v) AS ( VALUES ('key0','1'), ('key1','3'), ('key1','2') )
SELECT jsonb_object_agg(k, v ORDER BY v) FROM vals;
      jsonb_object_agg
----------------------------
 {"key0": "1", "key1": "3"}

在处理多参数聚合函数时,请注意 ORDER BY 子句必须紧跟在所有常规实参之后。例如应写为:

SELECT string_agg(a, ',' ORDER BY a) FROM table;

而严禁写为:

SELECT string_agg(a ORDER BY a, ',') FROM table;  -- 错误写法

如果在带有 order_by_clause 的同时还声明了 DISTINCT,那么 ORDER BY 内部引用的表达式必须严格限制在 DISTINCT 的参数列表中。例如:

WITH vals (v) AS ( VALUES (1),(3),(4),(3),(2) )
SELECT array_agg(DISTINCT v ORDER BY v DESC) FROM vals;
 array_agg
-----------
 {4,3,2,1}

前面介绍的在聚合函数常规参数列表内声明 ORDER BY,用于通用的数据行排序需求,此时排序属于可选增强。而在 SQL 中存在一类特殊的子集被称为有序集聚合(ordered-set aggregates),在这类函数中 order_by_clause 是强制必需的,因为该聚合函数的数学逻辑脱离了特定排序便无法成立。典型的有序集聚合包括排名函数与百分位数计算。对于有序集聚合,排序子句必须书写在 WITHIN GROUP (...) 内部,如前述最后一种语法所示。在 order_by_clause 内部声明的表达式会针对每行输入求值并按序排好后传递给聚合函数。位于 WITHIN GROUP 之前的参数被称为直接参数(direct arguments),以与排序内部的聚合参数(aggregated arguments)相区别。与普通聚合参数不同,直接参数在每次聚合调用中仅求值一次,而非逐行求值,通常用于指定百分位分位数(例如 0.5)。

有序集聚合调用的示例如下:

SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY income) FROM households;
 percentile_cont
-----------------
           50489

该查询计算 households 表中 income 收入字段的第 50 百分位数(即中位数)。此处的 0.5 即为直接参数。

如果附加了 FILTER 子句,那么只有令 filter_clause 求值为真(True)的输入行才会进入该聚合函数的统计池,其余行被直接忽略。例如:

SELECT
    count(*) AS unfiltered,
    count(*) FILTER (WHERE i < 5) AS filtered
FROM generate_series(1,10) AS s(i);
 unfiltered | filtered
------------+----------
         10 |        4
(1 row)

所有系统预定义的聚合函数均在 Section 9.21 中进行了完整列举,用户也可以按需注册自定义聚合函数。

聚合表达式仅允许出现在 SELECT 查询的目标选择列表以及 HAVING 过滤子句中。在 WHERE 等子句中是绝对禁止编写聚合函数的,因为这些子句在逻辑上严格执行于聚合计算之前。

当聚合表达式出现在子查询中时(参见 Section 4.2.11 与 Section 9.24),聚合通常针对子查询自身的行集进行计算。但存在一个例外:如果该聚合函数的参数(以及可选的 filter_clause)仅包含来自外层查询的外部变量,则该聚合逻辑归属于最近的外层查询层级,并在外层查询的行集上完成计算。

4.2.8. 窗口函数调用 #

窗口函数调用代表将类似聚合的计算逻辑,作用于由查询选中的部分相关数据行集合之上。与普通非窗口聚合函数不同,窗口计算不会将结果行压缩折叠为单行——查询结果中的每一行均完整保留各自的独立身份。然而,窗口函数能够访问根据其分组规范(PARTITION BY 列表)界定的当前行所属窗口范围内的全部上下文行。窗口函数调用的标准语法如下:

function_name ([expression [, expression ... ]]) [ FILTER ( WHERE filter_clause ) ] OVER window_name
function_name ([expression [, expression ... ]]) [ FILTER ( WHERE filter_clause ) ] OVER ( window_definition )
function_name ( * ) [ FILTER ( WHERE filter_clause ) ] OVER window_name
function_name ( * ) [ FILTER ( WHERE filter_clause ) ] OVER ( window_definition )

其中窗口定义 window_definition 的语法为:

[ existing_window_name ]
[ PARTITION BY expression [, ...] ]
[ ORDER BY expression [ ASC | DESC | USING operator ] [ NULLS { FIRST | LAST } ] [, ...] ]
[ frame_clause ]

可选的窗口帧子句 frame_clause 可以是以下形式之一:

{ RANGE | ROWS | GROUPS } frame_start [ frame_exclusion ]
{ RANGE | ROWS | GROUPS } BETWEEN frame_start AND frame_end [ frame_exclusion ]

其中 frame_start 与 frame_end 可以为:

UNBOUNDED PRECEDING
offset PRECEDING
CURRENT ROW
offset FOLLOWING
UNBOUNDED FOLLOWING

而排除选项 frame_exclusion 支持:

EXCLUDE CURRENT ROW
EXCLUDE GROUP
EXCLUDE TIES
EXCLUDE NO OTHERS

此处的 expression 指代任何自身不包含嵌套窗口函数调用的常规值表达式。

window_name 引用了通过当前查询的 WINDOW 子句统一定义的命名窗口。或者,也可以在圆括号内直接编写完整的 window_definition。值得注意的是,OVER wname 与 OVER (wname ...) 并不完全等价;后者意味着复制并派生修改窗口定义,如果被引用的原窗口本身已经包含了窗口帧定义,这种覆盖派生将被系统禁止报错。

PARTITION BY 子句将查询行切分为独立的分区(partitions),窗口函数在各个分区内部互不干扰地独立计算。PARTITION BY 的行为类似于查询级的 GROUP BY,区别在于其内部必须是纯粹的表达式,不能使用外部输出列的别名或数字序号。若省略该子句,全部查询结果行被视为单一全局分区。 ORDER BY 子句用于定义窗口函数在单个分区内部遍历处理数据行的次序。若省略排序子句,处理次序将是不确定的。

frame_clause 用于精确界定构成窗口帧(window frame)的动态行子集。对于那些作用于动态帧而非整个静态分区的窗口函数,窗口帧包含的具体数据行会伴随当前行的移动而动态变化。窗口帧支持在 RANGE、ROWS 或 GROUPS 三种模式下声明,范围从 frame_start 延续至 frame_end。若省略截止边界,默认截止于 CURRENT ROW。

UNBOUNDED PRECEDING 意为窗口帧始于分区的第一行;同理,UNBOUNDED FOLLOWING 意为窗口帧终于分区的最后一行。

在 RANGE 或 GROUPS 模式下,frame_start 为 CURRENT ROW 表示窗口帧始于当前行的首个同位行(peer row,指根据窗口排序规则与当前行并列等价的行);而在 ROWS 模式下,CURRENT ROW 就精确代表物理当前行本身。

在声明偏移量(offset PRECEDING 或 offset FOLLOWING)时,偏移量表达式中严禁包含任何变量、聚合函数或窗口函数。偏移量的具体语义取决于帧模式:

  • 在 ROWS 模式下,偏移量必须是一个非负非空整数,代表在物理行数上相对于当前行向前或向后延伸的具体行数。

  • 在 GROUPS 模式下,偏移量同样必须是非负非空整数,代表相对于当前行的同位组向前或向后跨越的同位分组数(使用此模式必须在窗口中显式声明排序规则)。

  • 在 RANGE 模式下,要求排序子句中必须仅指定单一排序列。此时偏移量表示排序列的数值差值范围。对于时间类排序列,该偏移量通常为 interval 间隔类型,例如 RANGE BETWEEN '1 day' PRECEDING AND '10 days' FOLLOWING。

无论如何界定,窗口帧的实际物理范围绝不会跨越当前分区的物理边界。

排除选项 frame_exclusion 允许在窗口计算中主动将当前行周围的特定行排除在计算之外:EXCLUDE CURRENT ROW 仅排除当前行本身;EXCLUDE GROUP 排除当前行及其所有并列同位行;EXCLUDE TIES 排除当前行的同位行但保留当前行自身;EXCLUDE NO OTHERS 显式声明默认的不排除任何行的行为。

默认的窗口帧配置为 RANGE UNBOUNDED PRECEDING(等价于 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)。在指定了排序的情况下,它将帧界定为从分区起始行到当前行的最后一个并列同位行;在未指定排序时,则等同于包含当前分区的全部行。

语法约束上,起始边界不能声明为 UNBOUNDED FOLLOWING,截止边界不能声明为 UNBOUNDED PRECEDING,且截止边界在逻辑顺序上不能先于起始边界。

如果附加了 FILTER 子句,只有满足过滤条件的行才会进入窗口计算。请注意只有作为窗口函数调用的聚合函数才支持 FILTER。

所有系统内置窗口函数均列于 Table 9.67 中,用户也可以随时扩展注册新函数。此外,任何通用或统计类聚合函数均可作为窗口函数调用。

星号通配符(*)仅用于无参数聚合函数作为窗口函数调用的场景(如 count(*) OVER (...))。专用的窗口函数不支持通配符,且不允许在其函数参数列表中使用 DISTINCT 或 ORDER BY。

窗口函数调用仅被允许出现在查询的 SELECT 目标列表以及最外层的 ORDER BY 排序子句中。

关于窗口函数的更多系统介绍,详见 Section 3.5、Section 9.22 与 Section 7.2.5。

4.2.9. 类型转换 #

类型转换用于显式声明将一种数据类型的值转换为另一种数据类型。PostgreSQL 原生支持以下两种完全等价的类型转换语法:

CAST ( expression AS type )
expression::type

其中 CAST 符合 SQL 国际标准,而双冒号 :: 则是 PostgreSQL 长期以来的经典用法。

当类型转换作用于具有已知类型的表达式时,代表运行时的动态类型转换操作。该转换仅在系统目录中已预先注册了相应的类型转换路径时才会成功。这与纯字面量字符串常量的初始类型标记存在微妙差异(对无修饰的字符串字面量加类型转换,实际上是赋予该字面量初始类型归属,只要字符串文本符合目标类型的输入格式即可生效)。

当表达式的目标类型在上下文中毫无歧义时(例如将数值赋值给指定类型的字段),通常可以省略显式转换,系统会自动执行隐式转换。但是,自动隐式转换仅限于系统目录中被明确标记为“允许隐式应用”(OK to apply implicitly)的安全转换路径;其余转换必须通过显式语法强制调用,这一机制旨在避免系统在静默状态下发生出乎意料的隐式类型失真。

还可以使用类似函数调用的语法指定类型转换:

typename ( expression )

但该语法仅适用于那些类型名称本身属于合法函数标识符的数据类型。例如 double precision 因包含空格无法这样书写(但等价的 float8 可以);而 interval、time 与 timestamp 等名称因语法冲突必须加双引号才能以函数风格调用。因此,函数式转换语法缺乏一致性,建议尽量避免使用。

Note

函数式类型转换在本质上确实就是一次底层的函数调用。在系统底层,执行运行时类型转换也是通过调用已注册的转换函数来实现的,按照惯例这些函数通常与其输出类型同名。关于类型转换的深度技术原理,请参阅 CREATE CAST。

4.2.10. 排序规则表达式 #

COLLATE 子句用于覆盖并显式指定一个表达式的本地化排序规则。它直接附加在目标表达式之后:

expr COLLATE collation

其中 collation 是排序规则的标识符(可带模式前缀)。COLLATE 子句的结合紧密度高于普通运算符,如有需要可使用圆括号明确范围。

如果未显式指定排序规则,系统会根据表达式中涉及的字段列自动推导;如果表达式未涉及任何具有排序属性的列,则默认套用当前数据库全局配置的默认排序规则。

COLLATE 最常见的两大用途是:在 ORDER BY 子句中临时覆盖排序权重次序,例如:

SELECT a, b, c FROM tbl WHERE ... ORDER BY a COLLATE "C";

以及在依赖语言环境的函数或大小比对运算符中显式覆盖规则,例如:

SELECT * FROM tbl WHERE a > 'foo' COLLATE "C";

请注意在后一种场景中,排序规则附加在运算符的输入实参上。无论附加在运算符或函数的哪个输入实参上,其效果是等同的,因为运算符在执行时会综合考量所有实参的排序规则,显式的 COLLATE 会直接覆盖其他实参的默认规则(但在多个实参上附加彼此冲突的不匹配排序规则会导致报错,详见 Section 23.2)。因此下述写法效果完全一致:

SELECT * FROM tbl WHERE a COLLATE "C" > 'foo';

但以下写法是错误的:

SELECT * FROM tbl WHERE (a > 'foo') COLLATE "C";

因为该写法试图将排序规则施加给 > 运算符的布尔型计算结果,而 boolean 属于不具备排序规则属性的数据类型。

4.2.11. 标量子查询 #

标量子查询是由圆括号包裹的普通 SELECT 查询语句,其执行结果严格保证只返回单行单列(关于查询语句编写详见 Chapter 7)。执行该子查询所得到的单一值将被直接代入外层的值表达式中进行计算。若子查询在实际执行中返回了多行或多列,将触发运行时错误(但如果某次执行结果为空集零行,系统不会报错,此时标量结果自动判定为 NULL)。标量子查询可以自由引用外层查询中的变量,这些变量在子查询的一次求值周期内充当常量。涉及子查询的其他高级表达式参见 Section 9.24。

例如,检索各州中人口规模最大的城市人口数:

SELECT name, (SELECT max(pop) FROM cities WHERE cities.state = states.name)
    FROM states;

4.2.12. 数组构造器 #

数组构造器是利用若干成员元素的值动态构建一个数组对象的表达式。基础的数组构造器由关键字 ARRAY、左方括号 [、由逗号分隔的成员元素表达式列表、以及右方括号 ] 构成。例如:

SELECT ARRAY[1,2,3+4];
  array
---------
 {1,2,7}
(1 row)

在默认情况下,数组的元素类型是由各成员表达式公共决定的,其推导规则与 UNION 或 CASE 相同(参见 Section 10.5)。你可以通过对数组构造器附加显式类型转换来强制覆盖目标类型,例如:

SELECT ARRAY[1,2,22.7]::integer[];
  array
----------
 {1,2,23}
(1 row)

这与将每个成员元素单独显式转换为整型的效果相同。关于类型转换详见 Section 4.2.9。

通过嵌套数组构造器可以构造多维数组。在内层构造器中,关键字 ARRAY 可以省略。例如以下两种写法完全等价:

SELECT ARRAY[ARRAY[1,2], ARRAY[3,4]];
     array
---------------
 {{1,2},{3,4}}
(1 row)

SELECT ARRAY[[1,2],[3,4]];
     array
---------------
 {{1,2},{3,4}}
(1 row)

由于多维数组在底层物理结构上必须保证矩阵的规则矩形形态,因此处于同一嵌套层级的内层构造器必须产出维度完全一致的子数组。施加在外层 ARRAY 上的类型转换会自动级联传递至所有内部子构造器。

多维数组构造器的成员元素可以是任何能够返回合规子数组的表达式,不仅限于显式子 ARRAY 结构。例如:

CREATE TABLE arr(f1 int[], f2 int[]);

INSERT INTO arr VALUES (ARRAY[[1,2],[3,4]], ARRAY[[5,6],[7,8]]);

SELECT ARRAY[f1, f2, '{{9,10},{11,12}}'::int[]] FROM arr;
                     array
------------------------------------------------
 {{{1,2},{3,4}},{{5,6},{7,8}},{{9,10},{11,12}}}
(1 row)

你可以构造空数组,但由于数据库中不存在没有具体元素类型的无类型数组,因此声明空数组时必须显式附加目标类型转换:

SELECT ARRAY[]::integer[];
 array
-------
 {}
(1 row)

还可以基于子查询的返回结果直接构建数组。在这种语法形式下,数组构造器写作关键字 ARRAY 后紧跟由圆括号(而非方括号)包裹的子查询:

SELECT ARRAY(SELECT oid FROM pg_proc WHERE proname LIKE 'bytea%');
                              array
------------------------------------------------------------------
 {2011,1954,1948,1952,1951,1244,1950,2005,1949,1953,2006,31,2412}
(1 row)

SELECT ARRAY(SELECT ARRAY[i, i*2] FROM generate_series(1,5) AS a(i));
              array
----------------------------------
 {{1,2},{2,4},{3,6},{4,8},{5,10}}
(1 row)

该子查询必须只返回单列。若子查询输出列为非数组类型,将生成对应的一维数组;若输出列本身为数组类型,则结果将升维为更高一级的多维数组(此时要求子查询返回的每一行子数组维度必须严格矩形对称)。

由 ARRAY 构造器生成的数组,其下标在默认情况下总是从 1 开始计数。关于数组类型的系统使用,详见 ???。

4.2.13. 行构造器 #

行构造器(又称复合值构造器)是利用若干字段成员的值动态组装成行值(复合值)的表达式。行构造器由关键字 ROW、左圆括号、由逗号分隔的零个或多个字段值表达式、以及右圆括号构成。例如:

SELECT ROW(1,2.5,'this is a test');

当参数列表中包含多个表达式时,关键字 ROW 可以完全省略。

行构造器内部支持使用 rowvalue.* 展开语法,它会自动展开为该复合值的所有成员字段清单,如同在 SELECT 顶层列表中使用 .* 展开一样(参见 ???)。例如对于包含 f1 与 f2 字段的表 t,以下两句完全等价:

SELECT ROW(t.*, 42) FROM t;
SELECT ROW(t.f1, t.f2, 42) FROM t;

Note

在 PostgreSQL 8.2 之前,行构造器内部的 .* 语法不会被展开,因此 ROW(t.*, 42) 会生成一个两字段复合值,其首字段是嵌套的复合行。现代行为则更加直观实用。如果确实需要嵌套行的行为,省略星号直接书写行名即可,如 ROW(t, 42)。

默认情况下,由 ROW 表达式生成的复合值属于匿名的 record 记录类型。如有需要,可将其显式转换为具名的具象复合类型——可以是某张已建数据表的行类型,也可以是通过 CREATE TYPE AS 创建的独立复合类型。在存在重载函数调用的场景下,显式转换尤为必要以消除歧义。例如:

CREATE TABLE mytable(f1 int, f2 float, f3 text);

CREATE FUNCTION getf1(mytable) RETURNS int AS 'SELECT $1.f1' LANGUAGE SQL;

-- 由于系统中仅存在一个以 mytable 为入参的 getf1(),此处无需强制转换
SELECT getf1(ROW(1,2.5,'this is a test'));
 getf1
-------
     1
(1 row)

CREATE TYPE myrowtype AS (f1 int, f2 text, f3 numeric);

CREATE FUNCTION getf1(myrowtype) RETURNS int AS 'SELECT $1.f1' LANGUAGE SQL;

-- 存在重载,未具名 record 触发歧义报错:
SELECT getf1(ROW(1,2.5,'this is a test'));
ERROR:  function getf1(record) is not unique

-- 显式强转指定目标类型:
SELECT getf1(ROW(1,2.5,'this is a test')::mytable);
 getf1
-------
     1
(1 row)

SELECT getf1(CAST(ROW(11,'this is a test',2.5) AS myrowtype));
 getf1
-------
    11
(1 row)

行构造器既可用于构建待插入复合类型列的数据,也可以传递给接收复合实参的函数。此外,还可以使用标准比较运算符对整行数据进行大小比对(详见 Section 9.2 与 Section 9.25),以及在涉及行级子查询时作为比对目标(参见 Section 9.24)。

4.2.14. 表达式求值规则 #

在 SQL 中,子表达式的物理求值先后顺序是未定义的。特别是对于一个运算符或函数的各个入参,系统并不保证严格按照从左至右或任何其他固定次序求值。

不仅如此,如果一个表达式的整体结果仅通过计算其中部分分支便已能完全确定,那么剩余的子表达式可能根本不会被执行求值。例如对于:

SELECT true OR somefunc();

函数 somefunc() 极大概率根本不会被调用。同理:

SELECT somefunc() OR true;

也是如此。请注意这与某些通用编程语言中严格从左至右的“短路求值”并不完全相同。

因此,在复杂表达式中使用带有副作用(side effects)的自定义函数是极不明智的。尤其是在 WHERE 与 HAVING 过滤子句中,绝对不要依赖函数的执行副作用或求值次序,因为查询优化器在制定执行计划时会对这些子句进行极其激进的重排。依据布尔代数法则,过滤条件中的 AND、OR、NOT 逻辑组合可以被以任何等价的方式重组和顺序倒换。

当工程上确实必须强制要求严格的求值先后次序时,可以使用 CASE 结构(详见 Section 9.18)。例如,以下在 WHERE 中试图规避除以零错误的做法是不可靠的:

SELECT ... WHERE x > 0 AND y/x > 1.5;

而采用 CASE 的写法则是绝对安全的:

SELECT ... WHERE CASE WHEN x > 0 THEN y/x > 1.5 ELSE false END;

以这种方式使用 CASE 会阻止优化器某些维度的重排优化,因此仅在确有必要时才采用(在上述特例中,更优秀的工程解法是改写为数学等价的不等式 y > 1.5*x,从而彻底避开除法)。

但需要提醒的是,CASE 并非能够解决所有求值时序问题的万灵药。该机制的一大局限在于它无法阻止常量子表达式的提前求值。正如 ??? 中所述,被标记为 IMMUTABLE(不可变)的函数与运算符在查询优化生成执行计划的阶段就会被提前预先求值,而不是等待运行时。例如:

SELECT CASE WHEN x > 0 THEN x ELSE 1/0 END FROM tab;

由于优化器在编译计划阶段会尝试提前化简常量表达式,因此即使表中所有行的 x 均严格大于零、运行时永远不会走到 ELSE 分支,该查询依然极可能在计划阶段直接抛出除以零错误。

虽然上述示例看起来较为极端,但在函数内部执行的动态查询中很容易隐式触发类似问题,因为函数入参和局部变量在计划生成时经常会被代入作为常量。例如在 PL/pgSQL 存储过程中,使用原生的 IF-THEN-ELSE 流程控制语句来防范高风险计算,远比在 SQL 内嵌套 CASE 表达式安全可靠得多。

同理,CASE 结构也无法阻止其内部嵌套的聚合表达式被提前计算,因为在选择列表或 HAVING 子句中,聚合函数的计算优先级严格高于其他普通标量表达式。例如以下查询看似做了防范,却依然可能引发除以零崩溃:

SELECT CASE WHEN min(employees) > 0
            THEN avg(expenses / employees)
       END
    FROM departments;

由于 min() 与 avg() 是在外层并发逐行扫描聚合的,一旦遇到任何一行的 employees 为零,除以零错误会在 min() 的统计结果产生之前立刻爆发。正确的防范手段是使用前置的 WHERE 或聚合函数内层的 FILTER 子句,从数据源头上提前阻断异常输入行进入聚合算子。