5.12. 数组

PostgreSQL 允许记录的字段定义成 定长或不定长的多维数组.数组类型可以是任何基本类型或用户定义类型. 为说明这些用法,我们先创建一个由基本类型数组构成的表:

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

如上所示,一个数组类型是通过在数组元素类型名后面附加方括弧 ([]) 来命名的. 上面的命令将创建一个叫 sal_emp 的表,它的字段中有一个 text 类型字符串(name), 一个一维 integer型数组 (pay_by_quarter), 代表雇员的季度薪水和一个两维 text 类型数组(schedule), 表示雇员的周计划.

现在我们做一些 INSERT. 注意我们向数组字段追加数据时,观察我们写入数组数值的时候, 我们用花括弧把数值括起来并且用逗号将它们分开. 如果你懂 C,那么这与初始化一个结构很像。(更多细节见下文)

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"}}');

现在我们可以在sal_emp上运行一些查询。 首先,我们演示如何一次访问数组的一个元素. 这个查询检索在第二季度薪水变化的雇员名:

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 该周头两天的第一件计划.

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';

获取同样的结果。如果任何脚标写成 lower: upper 的形式,那么任何数组脚标操作 都当做一个数组片断对待.小于 1 的范围在任何脚标中都假设为 只声明了一个数值.

一个数组值可以完全被代替:

UPDATE sal_emp SET pay_by_quarter = '{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';

我们可以通过给一个和已存在的元素相邻元素赋值的, 或者是向已存在的数据相邻或覆盖的区域赋值的方法来扩大一个数组. 比如,如果一个数组当前有 4 个元素,那么如果我们给 array[5]赋值 后,它就有五个元素.目前,这样的扩大只允许多一维数组进行, 不能对多维数组进行操作.

数组片段赋值允许创建不使用一为基的下标的数组. 比如,我们可以给 array[-2:7] 赋值, 创建一个脚标值在 -2 和 7 之间的数组.

CREATE TABLE的语法允许定义固定长度的数组:

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

不过,目前的实现并不强加数组尺寸限制 --- 这个行为与未声明长度数组一样。

实际上,目前的实现也不强制声明维数.某种数组元素类型的数组都被认为 是同一种(数组)类型,而并不管它们的尺寸和维数是多少. 因此,在 CREATE TABLE 中声明维数和尺寸只是文档 上的功夫,并不影响运行时的行为.

目前的任何数组的维数都可以用 array_dims 函数检索出来:

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

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

array_dims 生成 text 结果, 这个结果可能便于人们读取但可能不便于编程. array_upperarray_lower 分别返回给出的数组维数的上界/下界范围。

要搜索一个数组中的数值,你必须检查该数组的每一个值. 你可以手工处理(如果你知道数组尺寸):

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;

不过,对于大数组而言,这个方法很快就会让人觉得无聊,并且如果你 不知道数组尺寸,那就没什么用了. 有一个 PostgreSQL的扩展 (尽管它不是主 PostgreSQL 发布的一部分,) 它定义了几个函数用于迭代数组值.使用这个工具, 上面的查询可以是:

SELECT * FROM sal_emp WHERE pay_by_quarter[1:4] *= 10000;

要搜索整个数组(而不只是声明的列),你可以用:

SELECT * FROM sal_emp WHERE pay_by_quarter *= 10000;

另外,你可以用下面的语句找出所有数组有值等于 10,000 的行:

SELECT * FROM sal_emp WHERE pay_by_quarter **= 10000;

要安装这个可选的模块,看看 PostgreSQL 源程序版本的 contrib/array 目录.

小技巧: 数组不是集合;象我们前面那些段落里描述的那样使用数组 通常表明你的库设计有问题.数组字段通常是可以分裂成独立的表. 很明显表要容易搜索得多.

注意: 目前的数组的实现的缺点是一个数组的独立元素不能为 SQL 的空值. 你可以把整个数组设置为空值,但是你不能有那种一部分是空值,而另外 一部分不是空值的数组.修补这个毛病的建议在 to-do 列表里.

数组输入和输出语法. 一个数组值的外部表现形式由一些根据该数组元素类型的 I/O 转换 规则分析的项组成,再加上一些标明该数组结构的修饰. 这些修饰由围绕在数组值周围的花括弧({}), 加上相临项之间的分隔字符组成.分隔字符通常是一个逗号(,), 但也可以是其它的东西∶它由该数组元素类型的 typdelim 设置 决定.(在 PostgreSQL 版本提供的标准 数据类型里,类型 box 使用分号 (;),但所有 其它的类型使用逗号.)在多维数组里,每个维(行,面,体,等)有自己级别的 花括弧,并且在同级相临的花括弧项之间必须写分隔符. 你可以在左花括弧,右花括弧,或者在任何独立的项字串之前写空白. 不过,在项后面的空白并不会被忽略∶在忽略前导空白之后,任何在 下一个右花括弧或者分隔符之前的东西都被当做项数值看待.

引起数组元素. 如上所述,在书写一个数组的数值的时候你要用双引号包围任意独立的 数组元素.如果元素数值可能令数组数值分析器产生歧意,那么你 必须这么做. 比如,那些包含花括弧,逗号(或者任何其它的分隔字符), 双引号,反斜扛,或者前导的空白元素都必须加 双引号.要把双引号或者反斜扛放到数组元素值里,给它们加一个反斜扛前缀. 另外,你可以用反斜扛逃逸的方法保护所有那些可能被当做数组语法的字符 或者可能被忽略的空白.

如果元素值是空字串或者包含花括弧,分隔符,双引号,反斜扛,或者空白, 那么数组输出过程将在元素值周围放上双引号. 在元素值内嵌的双引号和反斜扛将用反斜扛逃逸.对于数值类型, 我们可以安全地假设双引号从不出现,但是对于文本型数据类型, 我们就得准备对付引号的出现或者缺席.(这个行为和 7.2 以前的 PostgreSQL 版本已经不同了.)

技巧: 请记住你在 SQL 命令里写的任何东西都将首先解释成一个字串文本, 然后才是一个数组.这样就造成你所需要的反斜扛数量翻了翻. 比如,要插入一个包含反斜扛和双引号的 text 数组, 你需要这么写

INSERT ... VALUES ('{"\\\\","\\""}');

字串文本处理器去掉第一层反斜扛,然后省下的东西到了数组数值分析器的 时候看起来象 {"\\","\""}.接着,该字串传递给 text 数据类型的输入过程,分别变成 \". (如果我们用的数据类型对反斜扛也有特殊待遇,比如 bytea, 那么我们可能需要在命令里放多达八个反斜扛才能在存储态的数组元素中 得到一个反斜扛.)