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

8.15. 数组 #

8.15.1. 声明数组类型
8.15.2. 数组值输入
8.15.3. 访问数组元素
8.15.4. 修改数组内容
8.15.5. 在数组中搜索检索
8.15.6. 数组输入与输出语法

PostgreSQL 允许将表的字段列定义为变长多维数组。可以创建任何内置或用户自定义基本类型、枚举类型、复合类型、范围类型或域类型的数组。

8.15.1. 声明数组类型 #

为了演示数组类型的用法,我们创建如下数据表:

CREATE TABLE sal_emp (
    name            text,
    pay_by_quarter  integer[],
    schedule        text[][]
);

如上所示,数组数据类型的命名方式是在底层元素类型名称后附加方括号([])。上述命令将创建名为 sal_emp 的表,包含一个 text 字符串列(name)、一个表示员工按季度发放薪水的一维 integer 数组(pay_by_quarter),以及一个表示员工每周工作日程的二维 text 数组(schedule)。

CREATE TABLE 的语法允许指定数组的确切大小维度,例如:

CREATE TABLE tictactoe (
    squares   integer[3][3]
);

然而,当前的实现会忽略任何显式指定的数组大小上限,即其运行时行为与未指定长度的数组完全相同。

当前的实现同样不强制校验声明的维度数量。特定元素类型的数组均被视作同一数据类型,而无论其尺寸大小或维度数量如何。因此,在 CREATE TABLE 中声明数组大小或维度数量仅起到文档说明的作用,完全不影响运行时行为。

另一种符合 SQL 标准的替代语法是使用关键字 ARRAY,该语法可用于一维数组。pay_by_quarter 可以定义为:

    pay_by_quarter  integer ARRAY[4],

或者在不指定数组大小时:

    pay_by_quarter  integer ARRAY,

如同前述规则,PostgreSQL 在任何情况下均不强制该大小限制。

8.15.2. 数组值输入 #

若要将数组值书写为字面常量,可将各元素值包裹在花括号内部并用逗号隔开(类似于 C 语言中初始化结构体的语法)。可以使用双引号包裹任意元素值,当元素内部包含逗号或花括号时则必须使用双引号包裹。因此,数组常量的通用格式如下:

'{ val1 delim val2 delim ... }'

其中 delimiter 是该元素数据类型的分隔符字符(在其 pg_type 目录项中记录)。在 PostgreSQL 内置的标准类型中,绝大多数使用逗号(,),唯独 box 几何类型使用分号(;)。每个 val 可以是数组元素类型的常量,也可以是子数组。数组常量的典型范例如下:

'{{1,2,3},{4,5,6},{7,8,9}}'

该常量是一个二维的 3x3 数组,由三个包含整型数值的子数组构成。

若要将数组常量中的某个元素设为 NULL,只需将该元素值写作 NULL(大小写均可)。如果希望将其作为实际的字符串字面值 “NULL”,则必须用双引号将其包裹。

(此类数组常量在本质上是 Section 4.1.2.7 中讨论的泛型常量的特例。字面常量最初被视作字符串并传递给数组输入转换例程,在某些场景下可能需要显式声明类型修饰符)。

现在我们可以执行若干 INSERT 语句:

INSERT INTO sal_emp
    VALUES ('Bill',
    '{10000, 10000, 10000, 10000}',
    '{{"meeting", "lunch"}, {"training", "presentation"}}');

INSERT INTO sal_emp
    VALUES ('Carol',
    '{20000, 25000, 25000, 25000}',
    '{{"breakfast", "consulting"}, {"meeting", "lunch"}}');

前述两次插入的结果如下所示:

SELECT * FROM sal_emp;
 name  |      pay_by_quarter       |                 schedule
-------+---------------------------+-------------------------------------------
 Bill  | {10000,10000,10000,10000} | {{meeting,lunch},{training,presentation}}
 Carol | {20000,25000,25000,25000} | {{breakfast,consulting},{meeting,lunch}}
(2 rows)

多维数组在每个维度上必须具有相匹配的长度范围。若维度不匹配则会引发报错,例如:

INSERT INTO sal_emp
    VALUES ('Bill',
    '{10000, 10000, 10000, 10000}',
    '{{"meeting", "lunch"}, {"meeting"}}');
ERROR:  multidimensional arrays must have array expressions with matching dimensions

同样可以使用 ARRAY 构造器语法:

INSERT INTO sal_emp
    VALUES ('Bill',
    ARRAY[10000, 10000, 10000, 10000],
    ARRAY[['meeting', 'lunch'], ['training', 'presentation']]);

INSERT INTO sal_emp
    VALUES ('Carol',
    ARRAY[20000, 25000, 25000, 25000],
    ARRAY[['breakfast', 'consulting'], ['meeting', 'lunch']]);

请注意,数组元素是普通的 SQL 常量或表达式;例如字符串字面量使用单引号包裹,而不是像数组字面量中那样使用双引号。关于 ARRAY 构造器语法的详细讨论请参阅 Section 4.2.12。

8.15.3. 访问数组元素 #

现在我们可以在该表上执行查询。首先演示如何访问数组中的单一元素。以下查询检索第二季度工作计划发生变化的员工姓名:

SELECT name FROM sal_emp WHERE pay_by_quarter[1] <> pay_by_quarter[2];

 name
-------
 Carol
(1 row)

数组下标数字书写在方括号内部。在默认情况下,PostgreSQL 采用以 1 为基准的数组编号规则,即包含 n 个元素的数组从 array[1] 开始至 array[n] 结束。

以下查询检索所有员工在第三季度的薪资:

SELECT pay_by_quarter[3] FROM sal_emp;

 pay_by_quarter
----------------
          10000
          25000
(2 rows)

我们同样可以访问数组的任意矩形切片(Subarrays)。数组切片通过在一个或多个维度上书写 lower-bound:upper-bound 来表示。例如以下查询检索 Bill 在本周前两天日程中的第一个事项:

SELECT schedule[1:2][1:1] FROM sal_emp WHERE name = 'Bill';

        schedule
------------------------
 {{meeting},{training}}
(1 row)

若任何维度被书写为切片(即包含冒号),则所有维度都会被当作切片处理,即便某些维度只写了一个单一数字也是如此。任何只书写了单一数字(不含冒号)的维度会被视为下界为 1、上界等于给定数字。例如 [2] 会被视为 [1:2]。为了避免与非切片情况产生混淆,最佳实践是对所有维度均显式采用切片语法,例如使用 [1:2][1:1] 而非 [2][1:1]。

切片修饰符中的 lower-bound(下界)或 upper-bound(上界)均可省略;缺失的边界会被自动替换为该数组下标的最低或最高有效边界。例如以下查询检索出与上述完全相同的结果:

SELECT schedule[1:2][2] FROM sal_emp WHERE name = 'Bill';

                 schedule
-------------------------------------------
 {{meeting,lunch},{training,presentation}}
(1 row)

若数组本身或任何下标表达式为 null,数组下标表达式将直接返回 null。此外,若下标超出数组的实际边界,同样返回 null(该情况不会引发报错异常)。例如若 schedule 当前的维度范围为 [1:3][1:2],引用 schedule[3][3] 将产出 NULL。类似地,当下标数量与数组维度不符时同样返回 null 而不是抛错。

类似地,若数组本身或任何切片下标表达式为 null,数组切片表达式同样返回 null。但在其他场景下(例如选取的数组切片完全位于当前数组边界之外),切片表达式会产出一个空数组(零维数组)而非 null。若请求的切片与数组边界部分重叠,系统会静默收缩为仅返回重叠部分的数据。

任何数组值的当前物理维度大小均可通过函数 array_dims 检索:

SELECT array_dims(schedule) FROM sal_emp WHERE name = 'Carol';

 array_dims
------------
 [1:2][1:2]
(1 row)

array_dims 返回一个 text 文本结果,非常便于人类阅读。在程序中也可以使用 array_upper 与 array_lower 分别获取指定维度的上限与下限:

SELECT array_upper(schedule, 1) FROM sal_emp WHERE name = 'Carol';

 array_upper
-------------
           2
(1 row)

函数 array_length 返回指定维度的元素个数长度:

SELECT array_length(schedule, 1) FROM sal_emp WHERE name = 'Carol';

 array_length
--------------
            2
(1 row)

函数 cardinality 返回数组跨所有维度的元素总基数。该数值等价于对其调用 unnest 行展开后吐出的总行数:

SELECT cardinality(schedule) FROM sal_emp WHERE name = 'Carol';

 cardinality
-------------
           4
(1 row)

8.15.4. 修改数组内容 #

数组值可以被全量更新替换:

UPDATE sal_emp SET pay_by_quarter = '{25000,25000,27000,27000}'
    WHERE name = 'Carol';

或者使用 ARRAY 表达式语法:

UPDATE sal_emp SET pay_by_quarter = ARRAY[25000,25000,27000,27000]
    WHERE name = 'Carol';

数组同样支持针对单一元素进行就地局部更新:

UPDATE sal_emp SET pay_by_quarter[4] = 15000
    WHERE name = 'Bill';

或者对数组切片进行批量局部更新:

UPDATE sal_emp SET pay_by_quarter[1:2] = '{27000,27000}'
    WHERE name = 'Carol';

存量数组值可以通过向尚未存在的元素位置赋值来进行动态扩容。原有元素与新赋值元素之间的空隙位置将自动以 NULL 填充。例如若数组 myarray 当前包含 4 个元素,赋值给 myarray[6] 后其将扩容为 6 个元素,其中 myarray[5] 将为 NULL。当前此类扩容仅允许在一维数组上执行。

当下标赋值时,系统允许创建不以 1 为基准下标的数组。例如可以赋值给 myarray[-2:7] 以创建下标范围从 -2 到 7 的数组。

可以使用拼接操作符 || 构建全新的数组值:

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

SELECT ARRAY[5,6] || ARRAY[[1,2],[3,4]];
      ?column?
---------------------
 {{5,6},{1,2},{3,4}}
(1 row)

拼接操作符允许将单个元素推入一维数组的开头或末尾。它同样接受两个 N 维数组的拼接,或者一个 N 维数组与一个 N+1 维数组的拼接。

当单个元素被推入一维数组的开头或末尾时,生成结果数组保留原数组操作数的下界下标。例如:

SELECT array_dims(1 || '[0:1]={2,3}'::int[]);
 array_dims
------------
 [0:2]
(1 row)

SELECT array_dims(ARRAY[1,2] || 3);
 array_dims
------------
 [1:3]
(1 row)

当拼接两个维度相同的数组时,结果保留左侧操作数外层维度的下界下标。结果数组的外层维度由左侧操作数的各元素紧接着右侧操作数的各元素串联而成:

SELECT array_dims(ARRAY[1,2] || ARRAY[3,4,5]);
 array_dims
------------
 [1:5]
(1 row)

SELECT array_dims('[-1:0]={1,2}'::int[] || ARRAY[3,4,5]);
 array_dims
------------
 [-1:3]
(1 row)

当将一个 N 维数组推入 N+1 维数组的头部或尾部时,处理机制类似于上述元素入组的情况。每个 N 维子数组在本质上充当 N+1 维数组外层维度的一个独立元素。例如:

SELECT array_dims(ARRAY[1,2] || ARRAY[[3,4],[5,6]]);
 array_dims
------------
 [1:3][1:2]
(1 row)

同样可以使用函数 array_prepend、array_append 或 array_cat 构造数组。前两者仅支持一维数组,而 array_cat 支持多维数组:

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

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

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

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

在简单场景下,推荐使用拼接操作符替代直接调用上述函数。但由于拼接操作符被多重重载以兼容多种情况,在特定歧义场景下使用显式函数更为稳妥:

SELECT ARRAY[1, 2] || '{3, 4}';
 ?column?
-----------
 {1,2,3,4}
(1 row)

SELECT ARRAY[1, 2] || '7';
ERROR:  malformed array literal: "7"

在上述示例中,解析器在拼接操作符一侧看到整型数组,在另一侧看到未确定类型的常量。解析器默认推断其与另一操作数类型相同(即整型数组)。因此拼接操作符被默认解析为 array_cat 而非 array_append。使用显式函数 array_append 能够完全消除歧义。

8.15.5. 在数组中搜索检索 #

若要在数组中检索特定数值,必须逐项核对每个元素。如果预先已知数组大小,可以手动逐项对比。例如:

SELECT * FROM sal_emp WHERE pay_by_quarter[1] = 10000 OR
                            pay_by_quarter[2] = 10000 OR
                            pay_by_quarter[3] = 10000 OR
                            pay_by_quarter[4] = 10000;

然而对于大型数组,这种写法繁琐且在数组尺寸未知时无法工作。Section 9.25 介绍了一种基于 ANY 的高效替代语法:

SELECT * FROM sal_emp WHERE 10000 = ANY (pay_by_quarter);

此外,可以使用 ALL 语法查找数组中所有元素均等于 10000 的数据行:

SELECT * FROM sal_emp WHERE 10000 = ALL (pay_by_quarter);

或者也可以使用函数 generate_subscripts,例如:

SELECT * FROM
   (SELECT pay_by_quarter,
           generate_subscripts(pay_by_quarter, 1) AS s
      FROM sal_emp) AS foo
 WHERE pay_by_quarter[s] = 10000;

同样可以使用 && 重叠操作符检索数组,该操作符用于检查左侧操作数与右侧操作数是否存在共有交集元素:

SELECT * FROM sal_emp WHERE pay_by_quarter && ARRAY[10000];

请注意该操作符要求两侧操作数均为数组类型。可以使用单元素 ARRAY[...] 结构来查找单一目标值。

还可以使用函数 array_position 与 array_positions 在数组中查找特定值所在的物理下标位置。前者返回该值首次出现的位置下标;后者返回包含所有出现位置下标的数组:

SELECT array_position(ARRAY['sun','mon','tue','wed','thu','fri','sat'], 'mon');
 array_position
----------------
              2
(1 row)

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

Tip

数组并非关系集合;频繁在数组内部检索特定元素往往是数据库设计不良的反模式信号。建议将每个数组元素拆分为独立子表中的数据行,这不仅便于建立常规索引检索,而且在元素数量极大时具有显著更好的可扩展性。

8.15.6. 数组输入与输出语法 #

数组值的外部文本表示形式由若干子项构成,这些子项按照该数组元素类型的 I/O 转换规则进行解释,并附加指示数组结构的修饰符。修饰符由包裹整个数组的花括号({ 与 })以及相邻项之间的分隔符构成。分隔符通常为逗号(,),由该元素类型的 typdelim 属性决定。在多维数组中,每个维度均拥有其专属层级的花括号。

若元素值为空字符串、包含花括号、分隔符、双引号、反斜杠或空白符,或者匹配单词 NULL,数组输出例程会自动用双引号将该元素值包裹。嵌在元素值内部的双引号与反斜杠会自动进行反斜杠转义。对于数值数据类型通常不会出现双引号,但在处理文本类型时应当准备好兼容带引号与不带引号的情况。

在默认情况下,数组各维度的下界索引基准值被设为 1。为了表示其他下界基准的数组,可以在书写数组内容之前显式声明下标范围。该修饰由包裹每个维度下界与上界的方括号([])及中间的冒号(:)构成,其后紧跟一个等号(=)。例如:

SELECT f1[1][-2][3] AS e1, f1[1][-1][5] AS e2
  FROM (SELECT '[1:1][-2:-1][3:5]={{{1,2,3},{4,5,6}}}'::int[] AS f1) AS ss;

 e1 | e2
----+----
  1 |  6
(1 row)

仅当一个或多个维度的下界不为 1 时,下标范围修饰符才会出现在数组输出结果中。

若为某个元素书写的值是 NULL(无论大小写),该元素将被视作 NULL。任何双引号或反斜杠修饰都会禁用此行为,从而允许输入文本字面值 “NULL”。

如前所述,在书写数组字面值时,可以使用双引号包裹任意单个数组元素。当元素值可能引起数组解析器混淆时,则必须使用双引号包裹。例如包含花括号、逗号(或类型分隔符)、双引号、反斜杠或首尾空白符的元素必须用双引号包裹。空字符串以及匹配单词 NULL 的字符串同样必须使用双引号包裹。若要在带引号的元素中输入双引号或反斜杠,需在其前面加上反斜杠。或者也可以不使用引号,而直接使用反斜杠转义来保护特殊字符。

可以在左花括号前或右花括号后添加空白符,亦可在任意单个项字符串前后添加空白符。在所有这些位置出现的空白符都会被自动忽略。然而,双引号内部的空白符,或者两侧被非空白字符包围的空白符绝不会被忽略。

Tip

在书写常量数组值时,使用 ARRAY 构造器语法(参见 Section 4.2.12)通常比纯数组字面值语法更加直观简便。在构造器中,每个独立元素值的书写方式与作为普通非数组常量完全相同。