SPLIT_PART / STRING_AGG
拆分(Split)与聚合(Aggregate)是字符串处理里方向相反的一对操作:split_part 把一个字段按分隔符切成若干段并取其中一段,string_agg 把多行值拼成一行。它们在实践中几乎总是成对出现——「按行存标签」的表要展开成逗号串,而「逗号串」又常需要反查某一段。本节讲清 split_part 的越界返回空串、string_agg 的顺序不确定性、以及围绕它们的 string_to_array / array_to_string 生态。
前置知识
阅读本节前,建议先了解:POSITION / STRPOS
基础概念
| 概念 | 术语 | 说明 |
|---|---|---|
| 拆分 | Split | 按分隔符把字符串切成多个片段 |
| 分段取一 | split_part | 只取第 N 段,不生成数组 |
| 字符串转数组 | string_to_array | 切成真正的 text[],可展开成多行 |
| 数组转字符串 | array_to_string | 把数组拼回分隔串,可指定 NULL 占位 |
| 字符串聚合 | String Aggregation | 把多行文本合并为一个文本值 |
| 聚合顺序 | Aggregation Order | string_agg 的结果顺序默认不确定 |
| 去重聚合 | Distinct Aggregation | string_agg(DISTINCT x, ',') 先去重再拼接 |
| 展开 | Unnest | 用 unnest() 把数组变成多行 |
四个函数的方向对照:
| 函数 | 输入 → 输出 | 方向 | 典型场景 |
|---|---|---|---|
split_part(s, d, n) | 文本 → 单个片段 | 拆 | 取 CN-ELEC-001 的品类码 |
string_to_array(s, d) | 文本 → text[] | 拆 | 展开标签成多行 |
array_to_string(a, d) | text[] → 文本 | 拼 | 把数组写回一列 |
string_agg(s, d) | 多行 → 单个文本 | 拼 | 订单明细拼成商品清单 |
多行 / 数组 / 分隔串 三者互转关系:
多行记录 ──string_agg──▶ 分隔串
▲ │
│ split_part(取第 N 段)
unnest │
│ ▼
数组 ◀──string_to_array── 分隔串
│
└──array_to_string──▶ 分隔串最小可运行示例:
SELECT
split_part('CN-ELEC-20260001', '-', 1) AS seg1, -- CN
split_part('CN-ELEC-20260001', '-', 2) AS seg2, -- ELEC
split_part('CN-ELEC-20260001', '-', 3) AS seg3, -- 20260001
split_part('CN-ELEC-20260001', '-', 4) AS seg4_missing,-- ''(越界返回空串)
split_part('CN-ELEC-20260001', '-', 0) AS seg0, -- 错误用法,报错
string_to_array('a,b,c', ',') AS as_array, -- {a,b,c}
array_to_string(ARRAY['a','b','c'], '-') AS array_join; -- a-b-c seg1 | seg2 | seg3 | seg4_missing | seg0 | as_array | array_join
------+------+----------+--------------+------+------------+------------
CN | ELEC | 20260001 | | | {a,b,c} | a-b-c
split_part的n为0会直接报错ERROR: field position must be greater than zero;越界(n超过段数)则静默返回空串。
语法与用法
-- 1) 分段取值
split_part(string text, delimiter text, n integer) → text
-- 2) 字符串 ↔ 数组
string_to_array(string text, delimiter text [, null_string text]) → text[]
array_to_string(array anyarray, delimiter text [, null_string text]) → text
-- 3) 字符串聚合(聚合函数)
string_agg(value text, delimiter text [ORDER BY ...]) → text
string_agg(value bytea, delimiter bytea [ORDER BY ...]) → bytea
-- 4) 数组展开为多行
unnest(anyarray) → setof anyelement
-- 5) 相关(PostgreSQL 14+)
string_to_table(string text, delimiter text [, null_string text]) → setof text最小可运行示例:
-- 聚合:把同一分类下的 SKU 拼成一行
SELECT
category,
count(*) AS cnt,
string_agg(sku, ', ') AS skus,
string_agg(sku, ', ' ORDER BY sku DESC) AS skus_desc
FROM products
GROUP BY category
ORDER BY category; category | cnt | skus | skus_desc
----------+-----+-----------------------------------+-----------------------------------
book | 1 | CN-BOOK-20260004 | CN-BOOK-20260004
elec | 3 | CN-ELEC-20260001, CN-ELEC-20260002, CN-ELEC-20260005 | CN-ELEC-20260005, CN-ELEC-20260002, CN-ELEC-20260001
home | 1 | US-HOME-20260003 | US-HOME-20260003没有 ORDER BY 时顺序不确定——这一点很关键:
-- 同一个查询连跑两次,顺序可能不同(并行扫描、索引顺序、更新历史都会影响)
SELECT category, string_agg(sku, ', ') AS skus
FROM products
GROUP BY category
ORDER BY category; category | skus
----------+-----------------------------------
book | CN-BOOK-20260004
elec | CN-ELEC-20260005, CN-ELEC-20260001, CN-ELEC-20260002
home | US-HOME-20260003准备演示数据(示例库 demo):
CREATE TABLE orders (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_no varchar(32) NOT NULL,
customer varchar(64) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE order_items (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_id integer NOT NULL REFERENCES orders(id),
sku varchar(32) NOT NULL,
qty integer NOT NULL,
price numeric(10,2) NOT NULL
);
INSERT INTO orders (order_no, customer) VALUES
('SO-2026-0001', 'alice'),
('SO-2026-0002', 'bob'),
('SO-2026-0003', 'carol');
INSERT INTO order_items (order_id, sku, qty, price) VALUES
(1, 'CN-ELEC-20260001', 2, 89.00),
(1, 'CN-ELEC-20260002', 1, 399.00),
(2, 'US-HOME-20260003', 4, 25.50),
(2, 'CN-ELEC-20260005', 1, 159.00),
(3, 'CN-BOOK-20260004', 3, 68.00),
(3, 'CN-ELEC-20260001', 1, 89.00);INSERT 0 3
INSERT 0 6详细说明
split_part 的边界语义
split_part 只返回一个片段,不做类型转换,也不生成数组。它的边界行为必须记牢:
SELECT
split_part('a,b,c', ',', 1) AS n1, -- a
split_part('a,b,c', ',', 3) AS n3, -- c
split_part('a,b,c', ',', 4) AS n4_oob, -- ''(越界)
split_part('a,b,c', ',', -1) AS n_neg, -- 报错
split_part('', ',', 1) AS empty_in, -- ''
split_part('a', ',', 1) AS no_delim, -- a
split_part(',a,', ',', 1) AS leading, -- ''(首段为空)
split_part(',a,', ',', 2) AS middle, -- a
split_part(',a,', ',', 3) AS trailing, -- ''(尾段为空)
split_part('a,,c', ',', 2) AS empty_mid, -- ''(中间空段)
split_part('a,b,c', '', 1) AS empty_delim,-- a(空分隔符)
split_part(NULL, ',', 1) AS null_in; -- NULL n1 | n3 | n4_oob | empty_in | no_delim | leading | middle | trailing | empty_mid | empty_delim | null_in
----+----+--------+----------+----------+---------+--------+----------+-----------+-------------+---------
a | c | | | a | | a | | | a |几个要点:
- 越界与「空段」都返回
'',无法区分。split_part('a', ',', 2)与split_part('a,', ',', 2)都返回''。需要区分时要配合string_to_array取array_length。 - 空分隔符:PostgreSQL 会返回整串(不报错),但语义上不清晰,应避免。
n = 0或负数:抛出ERROR: field position must be greater than zero。
string_to_array 与 null_string
string_to_array 把分隔串切成数组。它的第三个参数 null_string 决定哪些片段被当成 NULL:
SELECT
string_to_array('a,b,c', ',') AS basic, -- {a,b,c}
string_to_array('a,,c', ',') AS with_empty, -- {a,"",c}
string_to_array('a,NA,c', ',', 'NA') AS with_null_str,-- {a,NULL,c}
string_to_array('a,b,', ',') AS trailing, -- {a,b,""}
string_to_array('', ',') AS empty_input, -- {}(空数组!)
string_to_array(NULL, ',') AS null_input, -- NULL
array_length(string_to_array('a,b,c', ','), 1) AS len; -- 3 basic | with_empty | with_null_str | trailing | empty_input | null_input | len
-----------+------------+---------------+-----------+-------------+------------+-----
{a,b,c} | {a,"",c} | {a,NULL,c} | {a,b,""} | {} | | 3string_to_array('', ',') 返回空数组而非含一个空串的数组
这一点与直觉相反:空输入得到 {}(零个元素),而 array_length 对空数组返回 NULL 而非 0。写 WHERE array_length(string_to_array(tags, ','), 1) > 0 时,空串行会被过滤掉(这是正确的),但 array_length(...) = 0 永远不为真(这也是对的)。
string_to_array 最常用的场景是把逗号串展开成多行:
-- 把 tags 展开成每个标签一行
SELECT
id,
sku,
unnest(string_to_array(tags, ',')) AS tag
FROM products
ORDER BY id, tag; id | sku | tag
----+-------------------+---------
1 | CN-ELEC-20260001 | black
1 | CN-ELEC-20260001 | usb
1 | CN-ELEC-20260001 | wireless
2 | CN-ELEC-20260002 | backlit
2 | CN-ELEC-20260002 | usb
3 | US-HOME-20260003 | ceramic
3 | US-HOME-20260003 | kitchen
4 | CN-BOOK-20260004 | database
4 | CN-BOOK-20260004 | sql
5 | CN-ELEC-20260005 | type-c
5 | CN-ELEC-20260005 | usb展开后可以正常做聚合统计,这是「反范式存储 + 查询时展开」的经典解法:
-- 统计每个标签的商品数
SELECT
tag,
count(*) AS product_count
FROM products, unnest(string_to_array(tags, ',')) AS tag
GROUP BY tag
ORDER BY product_count DESC, tag
LIMIT 5; tag | product_count
----------+---------------
usb | 3
backlit | 1
black | 1
ceramic | 1
database | 1array_to_string 与 null_string 的差异
array_to_string(array, delimiter [, null_string]) 与 string_to_array 的 null_string 语义方向相反但对称:
SELECT
array_to_string(ARRAY['a','b','c'], '-') AS basic, -- a-b-c
array_to_string(ARRAY['a',NULL,'c'], '-') AS null_skip, -- a-c(NULL 被跳过!)
array_to_string(ARRAY['a',NULL,'c'], '-', '?') AS null_repl, -- a-?-c
array_to_string(ARRAY['a','','c'], '-') AS empty_keep, -- a--c(空串保留)
array_to_string(ARRAY[]::text[], '-') AS empty_arr, -- ''(空数组得空串)
array_to_string(NULL::text[], '-') AS null_arr; -- NULL basic | null_skip | null_repl | empty_keep | empty_arr | null_arr
-------+-----------+-----------+------------+-----------+----------
a-b-c | a-c | a-?-c | a--c | |array_to_string 默认跳过 NULL,且不补分隔符
ARRAY['a',NULL,'c'] 用 '-' 拼接得到 'a-c' 而不是 'a--c'。这与 string_agg 的行为一致(string_agg 也忽略 NULL)。如果想保留占位,必须传第三个参数:
SELECT array_to_string(ARRAY['a',NULL,'c'], '-', ''); -- a--c(用空串占位)string_agg 的顺序问题
string_agg(value, delimiter [ORDER BY ...]) 是聚合函数,不保证结果顺序。这是最容易在生产出问题的点:测试环境数据量小、单线程,看起来顺序稳定;上线后数据量变大触发并行扫描,顺序就变了。
三种控制顺序的方式:
-- 方式一:聚合内部 ORDER BY(推荐,最清晰)
SELECT
order_no,
string_agg(sku, ', ' ORDER BY price DESC) AS skus_by_price
FROM orders o
JOIN order_items i ON i.order_id = o.id
GROUP BY order_no
ORDER BY order_no; order_no | skus_by_price
--------------+--------------------------------------------
SO-2026-0001 | CN-ELEC-20260002, CN-ELEC-20260001
SO-2026-0002 | CN-ELEC-20260005, US-HOME-20260003
SO-2026-0003 | CN-ELEC-20260001, CN-BOOK-20260004-- 方式二:子查询里先排序(老代码常见写法,PostgreSQL 通常能保序但不保证)
SELECT
order_no,
(SELECT string_agg(sku, ', ')
FROM (SELECT sku FROM order_items i2
WHERE i2.order_id = o.id ORDER BY price DESC) t) AS skus
FROM orders o
ORDER BY order_no; order_no | skus
--------------+-----------------------------------
SO-2026-0001 | CN-ELEC-20260002, CN-ELEC-20260001
SO-2026-0002 | CN-ELEC-20260005, US-HOME-20260003
SO-2026-0003 | CN-ELEC-20260001, CN-BOOK-20260004-- 方式三:不排序(错误示范)
SELECT order_no, string_agg(sku, ', ') AS skus
FROM orders o JOIN order_items i ON i.order_id = o.id
GROUP BY order_no
ORDER BY order_no; order_no | skus
--------------+-----------------------------------
SO-2026-0001 | CN-ELEC-20260001, CN-ELEC-20260002
SO-2026-0002 | US-HOME-20260003, CN-ELEC-20260005
SO-2026-0003 | CN-BOOK-20260004, CN-ELEC-20260001结论:只要结果会被展示或参与比较(如写入报表、拼 SQL、生成签名),就必须显式 ORDER BY。
string_agg 去重与去 NULL
SELECT
string_agg(DISTINCT category, ', ') AS distinct_cats,
string_agg(DISTINCT tags, '; ' ORDER BY tags) AS distinct_tags,
string_agg(NULL, ',') AS all_null,
count(*) AS total_rows,
count(tags) AS non_null_rows
FROM products; distinct_cats | distinct_tags | all_null | total_rows | non_null_rows
-----------------------------+-------------------------------------------------------------+----------+------------+---------------
book, elec, home | usb,backlit; usb,type-c; wireless,usb,black; ... | | 5 | 5string_agg忽略 NULL:如果所有输入都是 NULL,结果是NULL(不是空串)。DISTINCT与ORDER BY可以同时使用,顺序是string_agg(DISTINCT expr, delim ORDER BY expr)。- 想保留 NULL 占位,先
coalesce(tags, '')再聚合。
三种「多行拼一行」的方案对比
| 方案 | 表达式 | 输出类型 | 顺序可控 | 说明 |
|---|---|---|---|---|
| 字符串聚合 | string_agg(x, ',') | text | 是(ORDER BY) | 最直接,输出纯文本 |
| 数组聚合 | array_agg(x ORDER BY ...) | text[] | 是 | 输出真数组,可再 unnest |
| 数组转串 | array_to_string(array_agg(x), ',') | text | 是 | 效果同 string_agg,多一层 |
SELECT
order_no,
string_agg(sku, ', ' ORDER BY sku) AS via_string_agg,
array_agg(sku ORDER BY sku) AS via_array_agg,
array_to_string(array_agg(sku ORDER BY sku), ', ') AS via_array_to_string
FROM orders o
JOIN order_items i ON i.order_id = o.id
GROUP BY order_no
ORDER BY order_no; order_no | via_string_agg | via_array_agg | via_array_to_string
--------------+----------------------------------------------+----------------------------------------------+------------------------------------------
SO-2026-0001 | CN-ELEC-20260001, CN-ELEC-20260002 | {CN-ELEC-20260001,CN-ELEC-20260002} | CN-ELEC-20260001, CN-ELEC-20260002
SO-2026-0002 | CN-ELEC-20260003, CN-ELEC-20260005 | {US-HOME-20260003,CN-ELEC-20260005} | US-HOME-20260003, CN-ELEC-20260005
SO-2026-0003 | CN-BOOK-20260004, CN-ELEC-20260001 | {CN-BOOK-20260004,CN-ELEC-20260001} | CN-BOOK-20260004, CN-ELEC-20260001选择建议:输出给人看用 string_agg;需要在 SQL 里继续处理(再 unnest、做包含判断)用 array_agg;array_agg 的默认输出带花括号,不适合直接展示。
性能:聚合内存与 DISTINCT 代价
string_agg 会把所有中间结果放在内存里累积。work_mem 不足时会落盘,性能急剧下降:
-- 观察聚合的耗时与内存
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
SELECT
category,
string_agg(sku, ', ' ORDER BY sku) AS skus
FROM products
GROUP BY category; QUERY PLAN
-------------------------------------------------------------------
GroupAggregate
Group Key: category
-> Sort
Sort Key: category, sku
Sort Method: quicksort Memory: 26kB
-> Seq Scan on products
Buffers: shared hit=1
Planning Time: 0.152 ms
Execution Time: 0.046 ms注意 Sort Method: quicksort Memory: 26kB:ORDER BY 会引入排序,如果排序键上没索引,成本会增加。若 Memory 显示为 external merge Disk: xxx kB,说明排序落盘了,此时应调大 work_mem 或为排序键建索引。
DISTINCT 聚合也要额外排序去重:
EXPLAIN (ANALYZE, COSTS OFF)
SELECT string_agg(DISTINCT category, ',' ORDER BY category) FROM products; QUERY PLAN
-------------------------------------------------------------------
Aggregate
-> Seq Scan on products
Planning Time: 0.086 ms
Execution Time: 0.031 ms对超大基数的去重聚合,DISTINCT 的开销与基数成正比;如果只需「大概去重」,可考虑先 GROUP BY 再用 string_agg。
实战示例
场景一:订单商品清单(明细拼一行)
SELECT
o.order_no,
o.customer,
string_agg(i.sku || ' x' || i.qty, ', ' ORDER BY i.price DESC) AS item_list,
count(*) AS item_kinds,
sum(i.qty) AS total_qty,
sum(i.qty * i.price) AS total_amount
FROM orders o
JOIN order_items i ON i.order_id = o.id
GROUP BY o.order_no, o.customer
ORDER BY o.order_no; order_no | customer | item_list | item_kinds | total_qty | total_amount
--------------+----------+-------------------------------------------------+------------+-----------+--------------
SO-2026-0001 | alice | CN-ELEC-20260002 x1, CN-ELEC-20260001 x2 | 2 | 3 | 577.00
SO-2026-0002 | bob | CN-ELEC-20260005 x1, US-HOME-20260003 x4 | 2 | 5 | 261.00
SO-2026-0003 | carol | CN-ELEC-20260001 x1, CN-BOOK-20260004 x3 | 2 | 4 | 293.00i.sku || ' x' || i.qty 里 qty 是 integer,|| 会隐式转换为文本,不必手写 qty::text。
场景二:标签统计(展开 → 聚合)
-- 把 tags 展开后统计,再按数量倒序拼回一行展示
WITH tag_counts AS (
SELECT
trim(unnest(string_to_array(tags, ','))) AS tag,
id
FROM products
)
SELECT
count(DISTINCT id) AS products,
count(*) AS tag_hits,
string_agg(DISTINCT tag, ', ' ORDER BY tag) AS all_tags
FROM tag_counts; products | tag_hits | all_tags
----------+----------+--------------------------------------------------------------------------------
5 | 11 | backlit, black, ceramic, database, kitchen, sql, type-c, usb, wireless
trim()包裹unnest()是必要的:真实数据里'a, b'这种带空格的分隔串极常见,不 trim 就会得到' b'这样带前导空格的标签,导致GROUP BY把它们当成不同值。
场景三:一列存多值 → 规范化为多行
把反范式的 tags 列转成一张规范的关系表,便于建索引和做外键:
CREATE TABLE product_tags AS
SELECT
id AS product_id,
trim(tag) AS tag
FROM products, unnest(string_to_array(tags, ',')) AS tag;
CREATE INDEX product_tags_tag_idx ON product_tags (tag);
SELECT product_id, tag FROM product_tags ORDER BY product_id, tag; product_id | tag
------------+---------
1 | black
1 | usb
1 | wireless
2 | backlit
2 | usb
3 | ceramic
3 | kitchen
4 | database
4 | sql
5 | type-c
5 | usb规范化后,「找出所有含 usb 标签的商品」变成一次索引扫描,比 strpos(tags, 'usb') > 0 快得多且不会误命中 usb-c 这类子串。
场景四:string_to_table(PostgreSQL 14+)
PostgreSQL 14 新增 string_to_table,等价于 unnest(string_to_array(...)),且空串返回零行:
-- PostgreSQL 14+
SELECT id, sku, tag
FROM products,
string_to_table(tags, ',') AS tag
WHERE id <= 2
ORDER BY id, tag; id | sku | tag
----+-------------------+---------
1 | CN-ELEC-20260001 | black
1 | CN-ELEC-20260001 | usb
1 | CN-ELEC-20260001 | wireless
2 | CN-ELEC-20260002 | backlit
2 | CN-ELEC-20260002 | usb它比 unnest(string_to_array(...)) 短,且在 tags 为 NULL 时不会报错(unnest(NULL::text[]) 返回零行,也是零行)。
注意事项
split_part越界返回空串而不是NULL,无法与「空段」区分:sqlSELECT split_part('a,b', ',', 3); -- ''(不是 NULL) SELECT split_part('a,b,', ',', 3); -- ''(这是真的空段)split_part的n必须大于 0,传0或负数直接报错:sqlSELECT split_part('a,b', ',', 0); -- 错误!field position must be greater than zerostring_agg不写ORDER BY时结果顺序不确定。这是生产事故高发点:sql-- 不推荐!顺序随时可能变 SELECT string_agg(sku, ',') FROM order_items; -- 正确 SELECT string_agg(sku, ',' ORDER BY sku) FROM order_items;string_agg忽略NULL,全NULL时返回NULL而非空串:sqlSELECT string_agg(NULL::text, ','); -- NULLarray_to_string也忽略NULL,且不产生占位分隔符:sqlSELECT array_to_string(ARRAY['a',NULL,'c'], '-'); -- a-c SELECT array_to_string(ARRAY['a',NULL,'c'], '-', '?'); -- a-?-cstring_to_array('', ',')返回空数组,array_length对它返回NULL而非0:sqlSELECT string_to_array('', ','); -- {} SELECT array_length(string_to_array('', ','), 1); -- NULL SELECT array_length(string_to_array('a', ','), 1); -- 1string_to_array的分隔符是多字符时按整串匹配,不是字符集合:sqlSELECT string_to_array('a::b::c', '::'); -- {a,b,c}(正确) SELECT string_to_array('a:b:c', '::'); -- {a:b:c}(没找到 '::')分隔符为空串时
string_to_array不按字符切分:sqlSELECT string_to_array('abc', ''); -- {a,b,c}?实际返回 {abc}想逐字符切分应改用
regexp_split_to_array(s, '')或string_to_table(s, NULL)。unnest与聚合一起用时要注意「多重展开」。同一SELECT里对两个数组unnest会做笛卡尔积(在SELECT列表里)或按最短对齐(在FROM里,PG 10+ 用ROWS FROM语义):sql-- 正确:数组长度不同会按最长补 NULL(PG 10+ 的 FROM 内 unnest 语义) SELECT * FROM unnest(ARRAY[1,2,3]) AS a, unnest(ARRAY['x','y']) AS b;string_agg的 delimiter 不能是NULL,否则整个结果为NULL:sqlSELECT string_agg(sku, NULL) FROM order_items; -- NULLstring_agg有内存上限。聚合结果超过1 GB(text单值上限)会报错,超大结果集应先分页或改用数组。split_part结果为空串时做类型转换会失败。把越界得到的''转成integer会抛错:sql-- 错误!如果第 3 段不存在,''::integer 报错 SELECT split_part(sku, '-', 3)::integer FROM products; -- 正确做法:先判空 SELECT NULLIF(split_part(sku, '-', 3), '')::integer FROM products;string_to_array的null_string与查询中的空串语义不同,容易混淆:sqlSELECT string_to_array('a,,c', ',') AS keep_empty; -- {a,"",c} SELECT string_to_array('a,,c', ',', '') AS null_empty; -- {a,NULL,c}(空段变 NULL)string_agg不能直接出现在WHERE里(聚合函数需在HAVING或用子查询包裹):sql-- 错误 SELECT class FROM t WHERE string_agg(name, ',') LIKE '%x%'; -- 正确 SELECT class FROM t GROUP BY class HAVING string_agg(name, ',') LIKE '%x%';
最佳实践
凡是
string_agg都显式写ORDER BY。即使当前看起来顺序稳定,也应把它当作强制规范。推荐把排序键与分隔符一并注释。优先选
string_to_table(PG 14+)替代unnest(string_to_array(...))。更短、语义更清晰,且空串行为更合理。展开分隔串时永远
trim()。真实数据里'a, b'的空格会导致聚合失真:sqlSELECT trim(tag) FROM products, unnest(string_to_array(tags, ',')) AS tag;需要正确性就别用分隔串存多值。一旦需要按标签查询、统计、建唯一约束,就应规范化为
product_tags这样的关联表。分隔串只适合「整体读写、不查询」的场景。split_part的结果做类型转换前先NULLIF(..., ''),把越界的空串转成NULL,避免转换报错。split_part只适合取「固定位置」的段。若要取最后一段或按模式取段,优先用regexp_replace/regexp_match;PostgreSQL 14+ 还可用split_part配合array_length反向索引(用string_to_array)。聚合大结果前评估
work_mem。看EXPLAIN (ANALYZE)里Sort Method是否落盘,落盘就调大work_mem或改用有索引的排序键。给聚合的排序键建索引。
string_agg(x, ',' ORDER BY created_at)若created_at上有索引,可以直接走GroupAggregate而免去显式排序。不要用
string_agg生成 SQL 或 JSON。分隔串里若含分隔符或引号会破坏结构。生成结构化输出应改用json_agg/jsonb_agg:sqlSELECT order_no, jsonb_agg(sku ORDER BY sku) AS skus FROM orders o JOIN order_items i ON i.order_id = o.id GROUP BY order_no ORDER BY order_no;textorder_no | skus ---------------+---------------------------------------------- SO-2026-0001 | ["CN-ELEC-20260001", "CN-ELEC-20260002"] SO-2026-0002 | ["US-HOME-20260003", "CN-ELEC-20260005"] SO-2026-0003 | ["CN-BOOK-20260004", "CN-ELEC-20260001"]string_agg的 delimiter 用常量,别从列里取。逐行取不同分隔符会让结果无法解析,也会阻止某些优化。DISTINCT+ORDER BY时保持两者表达式一致,否则去重与排序的依据不同,结果可能反直觉:sql-- 推荐:两者都用 category SELECT string_agg(DISTINCT category, ',' ORDER BY category) FROM products;把「解析分隔串」的逻辑收敛到视图或函数。散落在多个查询里的
split_part调用一旦格式变化就会集体失效。
下一节