8.10. 数组 #
PostgreSQL允许把表的列定义为变长的多维数组。可以创建任何内建类型或用户定义类型的数组。
8.10.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中声明维数或大小只是文档性的,并不影响运行时行为。
对于一维数组,也可以使用另一种 SQL99 标准语法。pay_by_quarter本可以定义为:
pay_by_quarter integer ARRAY[4],
这种语法要求用一个整数常量表示数组的大小。但和前面一样,PostgreSQL并不强制该大小限制。
8.10.2. 数组值输入
要把一个数组值写成文字常量,需要把元素值用花括号括起来并用逗号分隔。(如果你了解 C,这很像 C 中初始化结构的语法。)任何元素值都可以加双引号,而且当它包含逗号或花括号时必须加双引号。(更多细节见下文。)因此,数组常量的一般格式如下:
'{ val1 delim val2 delim ... }'
其中delim是该类型的分隔符字符,记录在它的pg_type项中。(对所有内建类型,这个字符都是逗号“,”。)每个
val要么是数组元素类型的一个常量,要么是一个子数组。数组常量的一个例子是
'{{1,2,3},{4,5,6},{7,8,9}}'这个常量是一个二维的 3×3 数组,由三个整数子数组组成。
(这类数组常量实际上只是第 4.1.2.4 节中讨论的通用类型常量的一个特例。该常量最初被当作字符串处理并传递给数组的输入转换例程。可能需要显式的类型说明。)
现在我们可以展示一些INSERT语句。
INSERT INTO sal_emp
VALUES ('Bill',
'{10000, 10000, 10000, 10000}',
'{{"meeting", "lunch"}, {}}');
INSERT INTO sal_emp
VALUES ('Carol',
'{20000, 25000, 25000, 25000}',
'{{"talk", "consult"}, {"meeting"}}');
当前数组实现的一个限制是:数组的单个元素不能是 SQL 空值。整个数组可以设置为空,但不能让数组的一部分元素为空而另一部分非空。
这可能导致令人惊讶的结果。例如,前面两次插入的结果如下:
SELECT * FROM sal_emp;
name | pay_by_quarter | schedule
-------+---------------------------+--------------------
Bill | {10000,10000,10000,10000} | {{meeting},{""}}
Carol | {20000,25000,25000,25000} | {{talk},{meeting}}
(2 rows)
由于schedule的[2][2]
元素在每条INSERT语句中都缺失,[1][2]元素被丢弃了。
注意
修复这个问题已列入待办事项。
也可以使用ARRAY表达式语法:
INSERT INTO sal_emp
VALUES ('Bill',
ARRAY[10000, 10000, 10000, 10000],
ARRAY[['meeting', 'lunch'], ['','']]);
INSERT INTO sal_emp
VALUES ('Carol',
ARRAY[20000, 25000, 25000, 25000],
ARRAY[['talk', 'consult'], ['meeting', '']]);
SELECT * FROM sal_emp;
name | pay_by_quarter | schedule
-------+---------------------------+-------------------------------
Bill | {10000,10000,10000,10000} | {{meeting,lunch},{"",""}}
Carol | {20000,25000,25000,25000} | {{talk,consult},{meeting,""}}
(2 rows)注意在这种语法下,多维数组的每个维度必须有匹配的长度。不匹配会导致报错,而不是像前面的情况那样静默地丢弃值。例如:
INSERT INTO sal_emp
VALUES ('Carol',
ARRAY[20000, 25000, 25000, 25000],
ARRAY[['talk', 'consult'], ['meeting']]);
ERROR: multidimensional arrays must have array expressions with matching dimensions
还要注意数组元素是普通的 SQL 常量或表达式;例如,字符串字面量用单引号,而不是像数组文字中那样用双引号。ARRAY表达式语法在第 4.2.10 节中有更详细的讨论。
8.10.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)
我们也可以访问数组的任意矩形切片或子数组。数组切片通过为一个或多个数组维度写
来表示。例如,下面的查询检索 Bill 周计划前两天的第一项:
lower-bound:upper-bound
SELECT schedule[1:2][1:1] FROM sal_emp WHERE name = 'Bill';
schedule
--------------------
{{meeting},{""}}
(1 row)我们也可以写成
SELECT schedule[1:2][1] FROM sal_emp WHERE name = 'Bill';
结果相同。只要任何一个下标写成了
的形式,数组下标操作就总是表示一个数组切片。对于只指定了一个值的下标,会假定其下界为 1,例如:
lower:upper
SELECT schedule[1:2][2] FROM sal_emp WHERE name = 'Bill';
schedule
---------------------------
{{meeting,lunch},{"",""}}
(1 row)
任何数组值的当前维度都可以用array_dims
函数检索:
SELECT array_dims(schedule) FROM sal_emp WHERE name = 'Carol'; array_dims ------------ [1:2][1:1] (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)
8.10.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';
通过向与已有元素相邻的元素赋值,或向与已有数据相邻或重叠的切片赋值,可以扩大已存储的数组值。例如,如果数组
myarray当前有 4 个元素,那么向myarray[5]
赋值的更新完成后它将有 5 个元素。目前,只允许以这种方式扩大一维数组,不允许扩大多维数组。
数组切片赋值允许创建不使用从 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 || ARRAY[2,3]); 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(ARRAY[[1,2],[3,4]] || ARRAY[[5,6],[7,8],[9,0]]); array_dims ------------ [1:5][1:2] (1 row)
当一个N维数组被压入一个N+1维数组的开头或末尾时,结果与上面的元素-数组情形类似。每个
N维子数组本质上是N+1维数组最外层维度的一个元素。例如:
SELECT array_dims(ARRAY[1,2] || ARRAY[[3,4],[5,6]]); array_dims ------------ [0:2][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_cat(ARRAY[5,6], ARRAY[[1,2],[3,4]]);
array_cat
---------------------
{{5,6},{1,2},{3,4}}
8.10.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;然而,对于大型数组这很快就会变得繁琐,而且在数组大小不确定时也帮不上忙。第 9.17 节中描述了一种替代方法。上面的查询可以替换为:
SELECT * FROM sal_emp WHERE 10000 = ANY (pay_by_quarter);
此外,你还可以找出数组中所有值都等于 10000 的行:
SELECT * FROM sal_emp WHERE 10000 = ALL (pay_by_quarter);
提示
数组不是集合;搜索特定数组元素可能表明数据库设计不当。考虑使用一个独立的表,其中每个原本会是数组元素的项对应一行。这样更容易搜索,而且在元素数量很大时可能有更好的可伸缩性。
8.10.6. 数组输入和输出语法
数组值的外部文本表示由若干项构成,这些项按照数组元素类型的
I/O 转换规则解释,外加指示数组结构的装饰。装饰由环绕数组值的花括号({和})以及相邻项之间的分隔符字符组成。分隔符通常是逗号(,),但也可以是别的:它由数组元素类型的typdelim设置决定。(在
PostgreSQL发行版提供的标准数据类型中,box类型使用分号(;),其他所有类型都使用逗号。)在多维数组中,每个维度(行、平面、立方体等)都有自己的一层花括号,并且在同层的、由花括号括起的相邻各项之间必须写上分隔符。你可以在左花括号之前、右花括号之后或任何单个项字符串之前写空白。但项后面的空白不会被忽略:跳过前导空白后,直到下一个右花括号或分隔符之前的所有内容都被当作项的值。
如前所述,写数组值时可以给任何单个数组元素加双引号。如果元素值会导致数组值解析器混淆,你必须这样做。例如,包含花括号、逗号(或任何分隔符字符)、双引号、反斜线或前导空白的元素必须加双引号。要在加了引号的数组元素值中放入双引号或反斜线,需要在它前面加一个反斜线。另外,你也可以使用反斜线转义来保护所有会被当作数组语法或可忽略空白的数据字符。
如果元素值是空字符串或者包含花括号、分隔符字符、双引号、反斜线或空白,数组输出例程会给它们加上双引号。元素值中内嵌的双引号和反斜线将被反斜线转义。对于数字数据类型,可以放心地假设永远不会出现双引号,但对于文本数据类型,应当准备同时应对有无引号两种情况。(这是与 7.2 之前的 PostgreSQL版本相比的一个行为变化。)
注意
请记住,你在 SQL 命令中写的内容会先被解释为字符串字面量,然后再被解释为数组。这会使你需要的反斜线数量加倍。例如,要插入一个包含一个反斜线和一个双引号的text数组值,你需要写
INSERT ... VALUES ('{"\\\\","\\""}');
字符串字面量处理器会去掉一层反斜线,因此到达数组值解析器的内容看起来像{"\\","\""}。反过来,送给text
数据类型输入例程的字符串分别变成\和"。(如果我们使用的是其输入例程也特殊处理反斜线的数据类型,例如bytea,那么要在存储的数组元素中放入一个反斜线,命令中可能需要多达八个反斜线。)
提示
在 SQL 命令中写数组值时,ARRAY构造器语法往往比数组文字语法更容易使用。在ARRAY中,单个元素值的写法与它们不是数组成员时的写法相同。