Postgers数据库,在很早(应该是从7.4版本)就提供了数组类型。虽然数组在大部分信息系统、开发平台和语言中都有非常广泛的应用,但不知什么原因,好像在数据库系统中的应用并不广泛。同时SQL标准,应该也还没有包括数组类型和相关的操作方式,相当于是一个pg自己的扩展功能。
这是因为觉得数据库本身就已经可以很好的操作数据集合,不需要这个特性了吗?还是SQL语句处理起数组来比较麻烦? 这些问题笔者没有答案,也希望有谁能够很好的解释一下。
不管如何,PG是提供数组相关的功能和操作方式的,本文就来探讨一下它的实现思路、方式,还有一些使用的经验和场景笔者也想分享一些,希望能对大家的工作有所帮助。

数组操作的常规
笔者理解的数组,就是一个数据结构,它包括数据或者信息的集合,并具备以下特性:
- 有序,可以按照在数组中的位置来访问数组元素
- 有限,数组成员数量,一般情况下在创建时就已经确定
- 数组的成员,一般情况下数据类型是相同的(这应该是底层实现和内存存储方式的一个限制)
所以,一套数组操作的方法,应当包括:
- 获取长度(length)
- 可以按照索引或位置访问元素(at)
- 在特定位置增加数组元素(add)
其实对于数组的基本操作,以上三点应该就够了。其他操作一般都是基本操作的组合和扩展,当然直接提供相关的功能可以大幅度简化实现代码。这些方法通常包括:
- 构建子数组,数组分割(subarray, slice)
- 数组合并(concat)
- 查找特定元素在数组中的存在性和位置(contains, indexOf)
再考虑比较高级的特性和操作可以包括:
- 使用指定分隔符将数组转换为字符串,或者从字符串转为数组(split, join)
- 在一个数组中,支持不同类型的数据
- 数组排序,并且可以定义排序规则(sort)
- 随机化,打乱数组中的元素
- 多维数组和多维数组展平(flat)
Postgre数组类型
可以想象,postgres对数组类型设置是比较严格的。在使用之前,比如定义表结构的时候,需要指定数组要使用的数据类型。而且,在进行相关操作的时候,要时刻注意相关的数组的操作,也是相同的数据类型,如果不一致,可能需要进行显示的数据类型转换。
pg的很多基本数据类型都支持数组,使用和转换的方式也比较简单。比如下面就可以为数据表增加一个字段,它是一个数组,基本类型为字符类型:
alter table tb_test add column flags char[];
Postgres数组操作
Postgres主要通过提供一系列函数来支撑数组数据的操作。完整的信息参见其官方手册:
www.postgresql.org/docs/16/fun...
下面我们简单列举其中一些比较重要的:
- 构造数组和数组的值
在pg中,可以使用Array符号或者指定的格式来构造数组,如果编写SQL,也可以使用类似的方法。我们下面构造一个虚拟的临时表,后面的示例会用到:
sql
with T(id, vals) as (values
(1, array[1,2,3,5]),
(2, array[1,2]),
(3, array[6,2,3]),
(4, '{5,1,3}'::int[])
)
- 数组长度
使用array_length函数:
select 1, array_length(vals,1) from T;
注意有第二个参数1,这个方法是支持多维数组的,1表示取第一维的数组长度。另外如果数组长度为0,方法会返回null而不是0。
- 检索数组元素
可以像一般的编程语言类似,使用方括号索引的方式,获得特定索引位置的元素:
select id, vals[0], vals[1], vals[9] from T;
注意,pg的数组索引是从1开始的,但如果超界,不会出错,而是返回Null。
前面是使用索引来查询元素的值,也可以使用array_position通过值来查找元素在数组中的位置:
select id, array_position(vals, 2) from T;
如果有多个位置,可以使用array_positions来查询。
- 数组扩展
可以使用"||"运算符来扩展数组,并且选择扩展方向:
select id, 11 || vals || array[41,42] v2 from T;
也可以使用array_append,array_cat方法来扩展和合并数组。
- 数组赋值
在update语句中,可以直接通过索引修改数组中某个元素的值:
update T set(vals[1,], vals[2]) = (vals[2], vals[1]);
还提供了array_replace方法,可以将数组中的值,替换成其他的值(支持重复值):
select array_replace(ARRAY[1,2,5,4,5], 5, 3);
- 移除元素
可以使用array_remove方法,通过指定值,来从数组中移除元素:
select id, array_remove(vals, 2) from T;
此方法的返回结果是去除了特定值的新数组。
比如我们有一个需求是,要修改一些记录,在数组字段中增加一个值,但又不希望有重复值(原来的数组中可能已经包含了这个值)。这里有两个思路。一是在修改(通常用append)前使用值得存在性检查,过滤掉已包含值得记录;还有就是可以先从数组中移除然后扩展就可以了,如:
select array_remove(ARRAY[2,3,7], 3) || 3, array_remove(ARRAY[2,4,7], 3) || 3;
显然,前者的操作效率要高一些,但后者有个好处是修改的值总是在数组的最后。
- 子数组
有趣的是,postgres没有按照惯例,提供类似array_subarray的函数。要获取一个数组的一部分,可以直接使用上下索引:
select id,vals[2:4], vals[ array_length(vals,1)-1 : array_length(vals,1)+1 ] from T;
注意方括号里面索引的分隔是冒号,第二个参数也是索引位置而非长度, 还可以使用计算量作为索引位置。
- 存在性和包含性条件查询
这个经常用在Where子句的条件判断操作中。
sql
-- 单一值可以使用any函数
select * from T where 1 = any(vals);
select * from T where not (1 = any(vals));
-- 多值可以使用包含符号<@, 包含所有值
select * from T where array[1] <@ vals;
select * from T where array[1,2] <@ vals;
-- 包含任意值
select * from T where array[1,2] && vals;
-- 判断两个数组完全相同
select array[1,2] = array[1,2], array[1,2] = array[2,1], array[1,2] = array[1,2,3];
- 其他函数
下面的函数也是pg提供的,但感觉使用和场景比较少。
array_fill: 数组填充
array_dims, array_ndims : 数组维度信息
array_sample: 数组随机采样
array_shuffle: 数组随机化
cardinality: 多维数组元素总数
trim_array: 裁剪数组,去除后面n个元素
array_lower, array_upper: 数组上下边界
其他特别操作
作为数据库系统,pg提供了一些常规数组以外的和数据库相关的操作方式:
- unnest (反嵌套)
使用unnest方法,可以将数组转换为记录:
select id, unnest(vals) from T;
unnest还可以通过传入多个数组作为参数,扩展成一个行的集合。
select * from unnest(ARRAY[1,2],ARRAY['foo','bar','baz']);
- array_agg (数组聚合)
也可以将记录的值聚合后转换为数组:
select array_agg(id) idlist from T;
这种操作涉及到聚合计算,所以可能需要group by 关键字。
- distinct(去重)
postgres好像没有直接提供数组去重的函数,可能需要使用组合unnest和disctinct记录操作等方式,达成类似的效果。
SELECT array_agg(DISTINCT x) FROM unnest(array[1,3,2,2,1]) as x;
- array_to_string (数组转为字符串)
可以将数组转换为字符串(不需要聚合,无条件执行),可以指定连接字符:
select id, array_to_string(vals,',') from T;
- string_to_array (字符串转换为数组)
字符串转换为数组操作,需要指定连接字符,可选指定替换为空的字符串:
select string_to_array('6,3,4,7,2',',');
应用场景
Postgres提供了数组类型,就提供了更多的数据操作的灵活性和可能性。原来一些不太容易实现的应用场景,可能基于数组,就可以简单而明了的进行处理了。通常是可以简化数据结构和表的设计,也简化相关信息的操作。
笔者在业务系统开发中,就将这个特性应用到了一些场景中:
- 基于数组元素的多对多信息关联
笔者另有一篇博文,比较细致的讨论了这种使用方式。简单来讲,就是将关联的ID放在主表的数组里面,然后使用数组的any方法作为关联查询条件,这样可以省去那个关联中间表,简化管理和操作。
- 时间标识
信息项目管理中,经常使用一个字段来记录某个操作的时间,如果需要记录多种操作或者状态修改的时间,可以使用数组,就不用增加字段。数组的类型还可以直接使用时间转换的整数,更加简单。
- 属性标签
如可以使用字符,或者字符串数组,来保存记录的标签。这个标签数组是可以灵活扩展和管理的,就不需要增加额外的字段,或者专门的标签记录表了。
- 固定的序列项目管理
如果有一些项目信息是一个固定序列的话,可以考虑采用数组形式。一个典型的例子就是学生的考试成绩可以使用数组记录,这样可以简化在不同的情况下如不同的考试科目,来修改成绩数据表的结构。