ARRAY 构造函数与操作
数组不只是"一列里存多个值"的容器——它有一整套构造函数与操作函数,让 SQL 能在集合层面完成过滤、展开、聚合、比较与拼接。本节的视角与类型章节不同:不再讲"数组怎么声明",而是讲"数组怎么被函数生产出来、怎么被函数拆开、怎么与聚合互相转换"。
与「数组声明与初始化」的分工
数组声明与初始化 一章从类型视角出发,讲数组列怎么声明、字面量怎么写、维度与 array_length 的语义;本节与 数组访问与修改 互为补充,从函数视角出发,重点讲 ARRAY[] 与 ARRAY(SELECT ...) 构造器、unnest 展开、array_agg / string_agg 聚合、位置查找与集合运算符(@> / <@ / &&),以及这些函数在索引与查询改写中的用法。
前置知识
阅读本节前,建议先了解:NVL / IFNULL 兼容
基础概念
数组函数可以按"数据流向"分成四类:构造(从行或字面量产出数组)、展开(把数组变回行)、查询(在数组内部查找)、变换(拼接、增删、替换)。理清这条流向,函数选择就不再靠记忆。
| 概念 | 术语 | 说明 |
|---|---|---|
| 数组构造器 | Array Constructor | ARRAY[...],元素是任意表达式 |
| 子查询构造器 | Array Subquery | ARRAY(SELECT ...),把多行折叠成数组 |
| 展开 | Unnest | unnest(arr) 把数组变成一行一元素 |
| 集合聚合 | Array Aggregation | array_agg(x) 把多行折叠成数组 |
| 位置查找 | Position | array_position 返回首次出现的下标 |
| 集合包含 | Containment | @> / <@ 判断子集关系 |
| 集合重叠 | Overlap | && 判断是否至少共享一个元素 |
| 数组拼接 | Concatenation | array_cat 或 || 合并两个数组 |
| 元素追加 | Append / Prepend | array_append / array_prepend 在尾/首插入 |
| 下标切片 | Slice | arr[2:4] 取连续子数组 |
构造、展开、聚合三者的关系,是最容易绕晕的地方,先看一张"互转图":
多行 一个数组值
┌──────────────┐ ┌──────────────┐
│ 1 | 2 | 3 │ array_agg │ {1,2,3} │
│ (行) │ ─────────► │ (数组) │
│ │ ◄───────── │ │
└──────────────┘ unnest └──────────────┘
ARRAY(SELECT ...) ─────────►最小示例:
SELECT
ARRAY[1, 2, 3] AS built, -- 构造器
unnest(ARRAY[1, 2, 3]) AS unnested, -- 展开(返回 3 行)
array_append(ARRAY[1, 2], 3) AS appended, -- 追尾
array_prepend(0, ARRAY[1, 2]) AS prepended, -- 插头
array_cat(ARRAY[1, 2], ARRAY[3, 4]) AS concatenated -- 拼接
LIMIT 1;unnest 在 SELECT 列表里会把一行变成多行,因此上面的查询实际返回 3 行,LIMIT 1 只取第一行:
built | unnested | appended | prepended | concatenated
-------+----------+----------+-----------+--------------
{1,2,3} | 1 | {1,2,3} | {0,1,2} | {1,2,3,4}
(1 row)语法与用法
构造类函数
| 函数 / 语法 | 说明 | 示例 | 结果 |
|---|---|---|---|
ARRAY[e1, e2, ...] | 用表达式列表构造 | ARRAY[1+1, 2*3] | {2,6} |
ARRAY(SELECT ...) | 用子查询结果构造 | ARRAY(SELECT id FROM t) | 折叠成一个数组 |
array_agg(x) | 聚合函数,折叠多行 | array_agg(name ORDER BY id) | 保序数组 |
array_fill(v, dims) | 用同一值填充到指定维度 | array_fill(0, ARRAY[3]) | {0,0,0} |
展开类函数
| 函数 | 说明 | 典型位置 |
|---|---|---|
unnest(arr) | 把数组展开成多行 | SELECT ... FROM unnest(tags) AS tag |
unnest(arr1, arr2) | 多数组按位置配对展开 | FROM unnest(a, b) AS t(x, y) |
generate_subscripts(arr, dim) | 生成合法下标序列 | 需要下标时用它而不是 generate_series |
查询类函数
| 函数 / 运算符 | 说明 | 示例 | 结果 |
|---|---|---|---|
array_position(arr, v) | 首次出现的下标 | array_position(ARRAY['a','b','a'], 'a') | 1 |
array_position(arr, v, start) | 从 start 开始找 | array_position(ARRAY['a','b','a'], 'a', 2) | 3 |
array_positions(arr, v) | 所有出现的下标 | array_positions(ARRAY['a','b','a'], 'a') | {1,3} |
arr @> arr2 | arr 是否包含 arr2 | ARRAY[1,2,3] @> ARRAY[2] | true |
arr <@ arr2 | arr 是否被 arr2 包含 | ARRAY[2] <@ ARRAY[1,2,3] | true |
arr && arr2 | 是否至少共享一个元素 | ARRAY[1,2] && ARRAY[2,3] | true |
v = ANY(arr) | 元素是否等于 v(等价于包含) | 2 = ANY(ARRAY[1,2,3]) | true |
cardinality(arr) | 元素总数 | cardinality(ARRAY[1,2,3]) | 3 |
@> / <@ / && 与 = ANY(...) 在单元素判断上等价,但语义侧重不同:
-- 下面三种写法对"tags 里有没有 tech"等价
SELECT
('{tech,online}'::text[] @> ARRAY['tech']) AS via_contains,
('{tech,online}'::text[] && ARRAY['tech']) AS via_overlap,
('tech' = ANY('{tech,online}'::text[])) AS via_any; via_contains | via_overlap | via_any
--------------+-------------+---------
t | t | t
(1 row)三者的关键差别在于索引与多值语义:@> 与 && 能走 GIN 索引;= ANY(...) 是逐行扫描中的表达式,通常用不上 GIN。多值场景下,@> 表示"全部包含",&& 表示"包含任意一个",别混用:
-- 要求三个标签全部命中:@>
SELECT '{tech,online,launch}'::text[] @> ARRAY['tech','online'] AS all_hit; -- true
-- 只要求命中任意一个:&&
SELECT '{tech,online,launch}'::text[] && ARRAY['tech','sales'] AS any_hit; -- true all_hit | any_hit
---------+---------
t | t
(1 row)变换类函数
| 函数 | 说明 | 示例 | 结果 |
|---|---|---|---|
array_append(arr, v) | 尾部追加一个元素 | array_append(ARRAY[1,2], 3) | {1,2,3} |
array_prepend(v, arr) | 头部插入一个元素 | array_prepend(0, ARRAY[1,2]) | {0,1,2} |
array_cat(a, b) | 拼接两个数组 | array_cat(ARRAY[1], ARRAY[2]) | {1,2} |
array_remove(arr, v) | 删除所有等于 v 的元素 | array_remove(ARRAY[1,2,1], 1) | {2} |
array_replace(a, old, new) | 替换所有等于 old 的元素 | array_replace(ARRAY[1,2], 2, 9) | {1,9} |
array_to_string(arr, sep) | 用分隔符拼接成文本 | array_to_string(ARRAY[1,2], '-') | 1-2 |
string_to_array(s, sep) | 按分隔符拆成数组 | string_to_array('a,b', ',') | {a,b} |
array_shrink(arr, n) | 只保留前 n 个元素(17+) | array_shrink(ARRAY[1,2,3], 2) | {1,2} |
注意 array_to_string 会跳过 NULL 元素(不产生分隔符),这是与 string_agg 行为一致的地方:
-- NULL 元素被跳过,不会变成 "1--3"
SELECT array_to_string(ARRAY[1, NULL, 3], '-') AS joined; joined
--------
1-3
(1 row)详细说明
ARRAY 构造器与 ARRAY(SELECT ...) 的区别
两者外观相似,但求值方式完全不同:
| 维度 | ARRAY[e1, e2] | ARRAY(SELECT ...) |
|---|---|---|
| 元素来源 | 当前行的表达式 | 子查询的每一行 |
| 行数影响 | 不改变行数 | 不改变行数(折叠为单值) |
| 空结果 | 不适用 | 产出 {},不是 NULL |
| 排序 | 按书写顺序 | 需显式 ORDER BY |
| 等价聚合 | — | array_agg(x ORDER BY ...) |
ARRAY(SELECT ...) 与 array_agg 的等价关系值得记住,因为 ARRAY(SELECT ...) 更简洁且没有 GROUP BY 的负担:
-- 两种写法,结果一致
SELECT ARRAY(SELECT name FROM t ORDER BY id) AS via_subquery;
SELECT array_agg(name ORDER BY id) AS via_agg FROM t; via_subquery
--------------
{a,b,c}
(1 row)但有一个重要差异:ARRAY(SELECT ...) 在结果包含 NULL 时会保留 NULL 元素,而它的行为在 PostgreSQL 中与 array_agg 一致——都保留 NULL。若要去掉,需显式过滤:
-- 子查询里过滤掉 NULL
SELECT ARRAY(SELECT nickname FROM users WHERE nickname IS NOT NULL ORDER BY id) AS nicks;unnest 的展开语义与 ROWS FROM
unnest 是最常被误用的数组函数。它出现在 FROM 中时,是把一个数组值"竖起来"成多行:
SELECT tag FROM unnest(ARRAY['a', 'b', 'c']) AS tag; tag
-----
a
b
c
(3 rows)而出现在 SELECT 列表中时,它仍然会改变输出的行数,这是很多人没意识到的:
-- 单行输入,三行输出
SELECT id, unnest(tags) AS tag FROM articles; id | tag
----+-------
1 | tech
1 | online
2 | edu
(3 rows)多数组并列展开时,PostgreSQL 采用 ROWS FROM 语义,按位置配对,短的一方补 NULL:
SELECT * FROM unnest(ARRAY['a','b','c'], ARRAY[1,2]) AS t(letter, num); letter | num
--------+-----
a | 1
b | 2
c |
(3 rows)要保留"元素在原数组中的下标",用 WITH ORDINALITY,它会额外产出一列从 1 开始的行号——注意这是展开后的行号,与数组下标在标准情况下一致:
SELECT tag, ord
FROM unnest(ARRAY['tech', 'online', 'launch']) WITH ORDINALITY AS t(tag, ord); tag | ord
--------+-----
tech | 1
online | 2
launch | 3
(3 rows)如果数组是从切片产生的(下标不从 1 开始),行号仍从 1 开始,此时想拿真实下标应使用 generate_subscripts:
SELECT i, arr[i]
FROM (SELECT ('{a,b,c,d,e}'::text[])[2:4] AS arr) s,
generate_subscripts(('{a,b,c,d,e}'::text[])[2:4], 1) AS i; i | arr
---+-----
2 | b
3 | c
4 | d
(3 rows)array_agg 与 string_agg 的取舍
两者都是"把多行折叠成一个值",区别只在输出形态:
| 维度 | array_agg(x) | string_agg(x, sep) |
|---|---|---|
| 返回类型 | 数组 | text |
| 是否需分隔符 | 否 | 必须 |
| NULL 元素 | 保留 | 跳过 |
| 能否再展开 | 能,unnest | 需 string_to_array 拆回 |
| 元素类型 | 任意 | 需能转 text |
| 典型用途 | 后续做集合运算 | 直接展示给用户 |
-- 同一组数据,两种落法
SELECT
array_agg(name ORDER BY id) AS as_array,
string_agg(name, ', ' ORDER BY id) AS as_text
FROM (VALUES ('甲', 1), ('乙', 2), ('丙', 3)) AS t(name, id); as_array | as_text
--------------------+------------
{甲,乙,丙} | 甲, 乙, 丙
(1 row)选择规则:如果结果还要在 SQL 里继续做集合判断(比如再与另一个数组比 @>),用 array_agg;如果结果只用于展示或写日志,用 string_agg 更省事。把 array_agg 的结果直接 array_to_string 是常见的折中写法,但要记得它跳过 NULL。
array_agg 的去重、排序与 NULL 处理
array_agg 支持 DISTINCT 与 ORDER BY,但两者组合时必须把排序键写进 ORDER BY:
-- 去重 + 保序
SELECT array_agg(DISTINCT tag ORDER BY tag) AS uniq_tags
FROM unnest(ARRAY['b', 'a', 'b', 'c']) AS tag; uniq_tags
-----------
{a,b,c}
(1 row)array_agg 会保留 NULL 元素;要去掉 NULL 需在聚合前过滤:
SELECT
array_agg(x) AS with_null, -- {1,NULL,3}
array_agg(x) FILTER (WHERE x IS NOT NULL) AS no_null, -- {1,3}
array_remove(array_agg(x), NULL) AS removed -- {1,3}
FROM (VALUES (1), (NULL), (3)) AS t(x); with_null | no_null | removed
------------+---------+---------
{1,NULL,3} | {1,3} | {1,3}
(1 row)三种写法里,FILTER 最直观,array_remove(..., NULL) 最紧凑,但要注意 array_remove 对 NULL 的处理依赖元素的等值比较,对不支持 = 的类型可能不适用。
位置查找:array_position 与 array_positions
array_position 找不到元素时返回 NULL 而不是 0 或报错,这与多数语言不同:
SELECT
array_position(ARRAY['a','b','a'], 'a') AS first_a, -- 1
array_position(ARRAY['a','b','a'], 'a', 2) AS second_a, -- 3
array_position(ARRAY['a','b'], 'z') AS not_found, -- NULL
array_positions(ARRAY['a','b','a'], 'a') AS all_a; -- {1,3} first_a | second_a | not_found | all_a
---------+----------+-----------+-------
1 | 3 | | {1,3}
(1 row)array_position 返回 NULL 的坑:WHERE array_position(tags, 'x') > 0 对不存在的元素会因 NULL > 0 为 NULL 而过滤掉——看似正确,但如果用 <> 0 判断就会出错:
-- 错误!元素不存在时 NULL <> 0 也是 NULL,仍被过滤
-- 但若写成 NOT (array_position(tags,'x') = 0) 则两个 NULL 分支都会放行
-- 推荐:直接用 @> 或 = ANY(),语义清晰
SELECT count(*) FROM articles WHERE tags @> ARRAY['x'];集合运算符与 GIN 索引
@>、<@、&& 是数组列的索引友好运算符,配合 GIN 索引能让"标签包含"类查询走索引:
CREATE INDEX idx_articles_tags_gin ON articles USING gin (tags);
-- 能走 GIN
EXPLAIN (ANALYZE, COSTS OFF)
SELECT * FROM articles WHERE tags @> ARRAY['database'];
-- 也支持数组列对数组列
SELECT * FROM articles WHERE tags && ARRAY['database', 'tutorial']; QUERY PLAN
-----------------------------------------------------------------------------
Bitmap Heap Scan on articles
Recheck Cond: (tags @> '{database}'::text[])
-> Bitmap Index Scan on idx_articles_tags_gin
Index Cond: (tags @> '{database}'::text[])
(4 rows)GIN 的默认操作符类 array_ops 支持 &&、@>、<@、= 四种运算符。注意 = ANY(...) 与 array_position 不走 GIN,改写成 @> / && 是常见的优化手段:
-- 优化前:无法使用 GIN
SELECT * FROM articles WHERE 'database' = ANY(tags);
-- 优化后:等价且可使用 GIN
SELECT * FROM articles WHERE tags @> ARRAY['database'];数组与集合的转换范式
很多"数组问题"本质上是"集合问题",先把数组展开成行、用关系运算处理、再聚合回数组,往往比堆砌数组函数更清晰:
-- 范式一:去重
SELECT ARRAY(SELECT DISTINCT v FROM unnest(ARRAY['b','a','b']) AS v ORDER BY v) AS deduped;
-- {a,b}
-- 范式二:交集(两个数组的共同元素)
SELECT ARRAY(
SELECT v FROM unnest(ARRAY[1,2,3]) AS v
INTERSECT
SELECT v FROM unnest(ARRAY[2,3,4]) AS v
ORDER BY v
) AS intersect_arr;
-- {2,3}
-- 范式三:差集(在 a 不在 b)
SELECT ARRAY(
SELECT v FROM unnest(ARRAY[1,2,3]) AS v
EXCEPT
SELECT v FROM unnest(ARRAY[2]) AS v
ORDER BY v
) AS except_arr;
-- {1,3} deduped | intersect_arr | except_arr
---------+---------------+------------
{a,b} | {2,3} | {1,3}
(1 row)数组与字符串的互转
string_to_array 与 array_to_string 是互逆的一对,但有两个不对称点:
| 方向 | 函数 | 不对称点 |
|---|---|---|
| 字符串 → 数组 | string_to_array(s, sep) | s 为 NULL 时结果为 NULL;空串分割结果依分隔符而定 |
| 数组 → 字符串 | array_to_string(arr, sep) | 跳过 NULL 元素;不支持 NULL 用替代符时需 array_to_string(arr, sep, null_str) |
SELECT
string_to_array('a,b,c', ',') AS split_comma, -- {a,b,c}
string_to_array('a|b|c', '|') AS split_pipe, -- {a,b,c}
string_to_array('', ',') AS split_empty, -- {}
array_to_string(ARRAY['a','b','c'], '-') AS join_dash, -- a-b-c
array_to_string(ARRAY['a',NULL,'c'], '-', '?') AS join_with_null; -- a-?-c split_comma | split_pipe | split_empty | join_dash | join_with_null
-------------+------------+-------------+-----------+----------------
{a,b,c} | {a,b,c} | {} | a-b-c | a-?-c
(1 row)第三个参数是 NULL 替代符(PostgreSQL 9.1+),这是 array_to_string 特有的能力,string_agg 没有对应功能。
注意 string_to_array('', ',') 返回 {},而 string_to_array('a', '') 会按每个字符拆分——空分隔符是特例,容易埋坑:
SELECT string_to_array('abc', '') AS per_char; -- {a,b,c}数组元素与 DISTINCT ON、GROUP BY 的配合
数组列本身可以参与 GROUP BY(按整个数组分组)和 DISTINCT(按整个数组去重),这在"多标签组合去重"场景很方便:
-- 按标签组合统计出现次数
SELECT tags, count(*) AS cnt
FROM articles
GROUP BY tags
ORDER BY cnt DESC;但要小心:GROUP BY tags 比的是数组的完整内容与顺序,{a,b} 与 {b,a} 是两组。要按"集合"分组需先排序或去重:
-- 归一化后再分组:先排序去重,把 {b,a} 变成 {a,b}
SELECT ARRAY(SELECT DISTINCT v FROM unnest(tags) AS v ORDER BY v) AS tag_set,
count(*) AS cnt
FROM articles
GROUP BY 1
ORDER BY cnt DESC;实战示例
场景:为电商商品表设计 SKU 规格数组,用数组函数完成筛选、统计、聚合与索引优化。
第 1 步:建表与索引
DROP TABLE IF EXISTS products;
CREATE TABLE products (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
tags TEXT[] NOT NULL DEFAULT '{}',
colors TEXT[] NOT NULL DEFAULT '{}',
prices NUMERIC(10,2)[] NOT NULL DEFAULT '{}',
CONSTRAINT chk_tags CHECK (cardinality(tags) <= 10),
CONSTRAINT chk_colors CHECK (cardinality(colors) <= 20)
);
-- 标签集合查询用 GIN
CREATE INDEX idx_products_tags_gin ON products USING gin (tags);CREATE TABLE
CREATE INDEX第 2 步:插入数据
INSERT INTO products (sku, name, tags, colors, prices) VALUES
('SKU-1001', '速干运动T恤', ARRAY['sport','summer'], ARRAY['black','white','red'], ARRAY[99.00, 109.00]),
('SKU-1002', '防水登山鞋', ARRAY['sport','outdoor'], ARRAY['brown','black'], ARRAY[499.00, 529.00]),
('SKU-1003', '纯棉基础衬衫', ARRAY['office','casual'], ARRAY['white','blue'], ARRAY[159.00]),
('SKU-1004', '轻量羽绒服', ARRAY['outdoor','winter'], ARRAY['red','blue','green'], ARRAY[799.00, 859.00, 899.00]),
('SKU-1005', '沙滩短裤', ARRAY['summer','casual'], ARRAY['blue'], ARRAY[69.00])
RETURNING sku, cardinality(tags) AS tag_cnt, cardinality(colors) AS color_cnt; sku | tag_cnt | color_cnt
----------+---------+-----------
SKU-1001 | 2 | 3
SKU-1002 | 2 | 2
SKU-1003 | 2 | 2
SKU-1004 | 2 | 3
SKU-1005 | 2 | 1
(5 rows)第 3 步:集合包含与重叠查询
-- 同时带 sport 和 outdoor 标签(全部命中)
SELECT sku, name, tags
FROM products
WHERE tags @> ARRAY['sport', 'outdoor'];
-- 带 sport 或 summer 任一标签(任意命中)
SELECT sku, name, tags
FROM products
WHERE tags && ARRAY['sport', 'summer']
ORDER BY sku; sku | name | tags
----------+------------+---------------------
SKU-1002 | 防水登山鞋 | {sport,outdoor}
(1 row)
sku | name | tags
----------+--------------+---------------------
SKU-1001 | 速干运动T恤 | {sport,summer}
SKU-1002 | 防水登山鞋 | {sport,outdoor}
SKU-1003 | 纯棉基础衬衫 | {office,casual}
SKU-1004 | 轻量羽绒服 | {outdoor,winter}
SKU-1005 | 沙滩短裤 | {summer,casual}
(5 rows)第 4 步:展开数组做标签统计
SELECT tag, count(*) AS product_cnt
FROM products, unnest(tags) AS tag
GROUP BY tag
ORDER BY product_cnt DESC, tag; tag | product_cnt
---------+-------------
summer | 2
casual | 2
outdoor | 2
sport | 2
office | 1
winter | 1
(6 rows)第 5 步:把颜色聚合成数组(跨行折叠)
-- 找出每个标签下涉及的所有颜色(去重 + 排序)
SELECT
tag,
ARRAY(SELECT DISTINCT c FROM unnest(array_agg(colors)) AS c ORDER BY c) AS all_colors
FROM products, unnest(tags) AS tag
GROUP BY tag
ORDER BY tag; tag | all_colors
---------+-----------------------
casual | {blue,white}
office | {blue,white}
outdoor | {black,blue,brown,green,red}
sport | {black,brown,red,white}
summer | {black,blue,red,white}
winter | {blue,green,red}
(6 rows)这里 unnest(array_agg(colors)) 是"先折叠成二维数组、再拉平"的常用套路:array_agg 把多行数组聚成一个"数组的数组",unnest 把它拉平成一维元素流。
-- 验证中间结果:array_agg(colors) 产出的是一个数组的数组
SELECT array_agg(colors) AS nested_arrays
FROM products
WHERE tags @> ARRAY['sport']; nested_arrays
--------------------------------------
{"{black,white,red}","{brown,black}"}
(1 row)第 6 步:用 WITH ORDINALITY 保留下标
-- 展开价格数组,同时给出"第几档价格"
SELECT p.sku, pr.ord AS price_tier, pr.value AS price
FROM products p
CROSS JOIN LATERAL unnest(p.prices) WITH ORDINALITY AS pr(value, ord)
WHERE p.sku = 'SKU-1004'
ORDER BY pr.ord; sku | price_tier | price
----------+------------+--------
SKU-1004 | 1 | 799.00
SKU-1004 | 2 | 859.00
SKU-1004 | 3 | 899.00
(3 rows)第 7 步:清理
DROP TABLE products;DROP TABLE注意事项
unnest在SELECT列表中会改变行数,不只是"展开一列"。若只想取值不想增行,应改用下标arr[1]或cardinality。
-- 一行变三行
SELECT id, unnest(tags) FROM articles;
-- 只取第一项,不增行
SELECT id, tags[1] AS first_tag FROM articles;array_position找不到时返回NULL而非0,用> 0判断安全,用= 0判断会漏。
-- 安全
SELECT * FROM articles WHERE array_position(tags, 'x') > 0;
-- 等价且更清晰(可走 GIN)
SELECT * FROM articles WHERE tags @> ARRAY['x'];@>是"全部包含",&&是"任意一个",多值查询选错会返回错误结果集。
-- 同时含 a 和 b
WHERE tags @> ARRAY['a', 'b']
-- 含 a 或 b
WHERE tags && ARRAY['a', 'b']- 数组的相等比较是"内容 + 顺序 + 维度"三重一致,
{a,b}与{b,a}不相等。
SELECT '{a,b}'::text[] = '{b,a}'::text[] AS same_content; -- false same_content
--------------
f
(1 row)array_agg不保证顺序,需要确定顺序时必须写ORDER BY,且DISTINCT与ORDER BY同时出现时排序表达式要在聚合参数中。
-- 推荐
array_agg(tag ORDER BY position)
-- 不推荐(顺序随执行计划变化)
array_agg(tag)array_agg保留 NULL,string_agg跳过 NULL,两者行为不同,展示场景要统一口径。
SELECT array_agg(x) AS arr, string_agg(x::text, ',') AS str
FROM (VALUES ('a'), (NULL), ('c')) AS t(x); arr | str
----------+-------
{a,NULL,c} | a,c
(1 row)ARRAY(SELECT ...)的子查询在整条 SQL 里是关联子查询时会按行执行,可能变成 N 次扫描,性能远差于JOIN + array_agg。
-- 危险:每行都执行一次子查询
SELECT p.id, ARRAY(SELECT c.name FROM colors c WHERE c.product_id = p.id) AS colors
FROM products p;
-- 推荐:一次连接 + 聚合
SELECT p.id, array_agg(c.name ORDER BY c.name) AS colors
FROM products p
LEFT JOIN colors c ON c.product_id = p.id
GROUP BY p.id;array_to_string跳过 NULL,不产生多余分隔符,若需占位要传第三个参数。
SELECT array_to_string(ARRAY['a', NULL, 'c'], '-', '(空)') AS with_placeholder; with_placeholder
------------------
a-(空)-c
(1 row)- GIN 索引只支持
&&、@>、<@、=,= ANY(...)、array_position都不走索引,需要改写。
-- 不走索引
WHERE 'x' = ANY(tags)
-- 走索引
WHERE tags @> ARRAY['x']- 数组元素写回是整行重写。
array_append在UPDATE中使用会重写整行并可能触发 TOAST,高频追加场景应改用关系表。
-- 每次追加都会读改写整行
UPDATE products SET tags = array_append(tags, 'new') WHERE id = 1;- 多维数组无法直接
unnest成一维,unnest只展开最外层,内层仍是数组。
-- 外层展开后每行仍是一个数组
SELECT unnest(ARRAY[[1,2],[3,4]]) AS row_arr; row_arr
---------
{1,2}
{3,4}
(2 rows)- 空数组不等于 NULL,
cardinality('{}') = 0而cardinality(NULL) = NULL,判空要统一口径。
SELECT
cardinality('{}'::int[]) AS empty_card, -- 0
cardinality(NULL::int[]) AS null_card; -- NULL empty_card | null_card
------------+-----------
0 |
(1 row)最佳实践
- 能用集合运算符就别用逐元素函数。
tags @> ARRAY['x']比array_position(tags,'x') > 0更快、更清晰、可走 GIN。
-- 推荐
SELECT * FROM products WHERE tags @> ARRAY['sport'];- "展开—处理—聚合"是数组的通用范式,遇到复杂的数组变换(去重、交并差、排序),先
unnest成行再聚合回来。
SELECT ARRAY(SELECT DISTINCT v FROM unnest(tags) AS v ORDER BY v) FROM products;- 给数组列建 GIN 索引,但只在真的有集合查询时才建,因为 GIN 有写放大代价。
CREATE INDEX idx_products_tags_gin ON products USING gin (tags);- 数组列统一
NOT NULL DEFAULT '{}',避免NULL与空数组两种"没有值"的表示并存。
tags TEXT[] NOT NULL DEFAULT '{}'- 用
cardinality做数量判断与约束,array_length对空数组返回NULL会让CHECK失效。
CONSTRAINT chk_tags CHECK (cardinality(tags) <= 10)array_agg一律显式写ORDER BY,顺序稳定才能被测试与缓存依赖。
array_agg(tag ORDER BY position)- 需要下标时用
WITH ORDINALITY,而不是先generate_series再数组取值,前者只扫一次。
CROSS JOIN LATERAL unnest(p.prices) WITH ORDINALITY AS pr(value, ord)- 展示给用户前转成文本,不要直接把
{a,b,c}这种内部格式丢到界面。
SELECT array_to_string(tags, ' / ') AS tag_text FROM products;ARRAY(SELECT ...)只用于非关联的简单折叠,任何带WHERE 外列的子查询都应改写成JOIN + array_agg。
-- 推荐
SELECT p.id, array_agg(c.name ORDER BY c.name)
FROM products p LEFT JOIN colors c ON c.product_id = p.id
GROUP BY p.id;- 迁移数组到关系表前先看查询模式。一旦需要"按标签分页、按标签建外键、按标签做多表联查",数组会迅速成为负担。