Skip to content

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 Orderstring_agg 的结果顺序默认不确定
去重聚合Distinct Aggregationstring_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)多行 → 单个文本拼订单明细拼成商品清单
text
多行 / 数组 / 分隔串 三者互转关系:

  多行记录 ──string_agg──▶ 分隔串
      ▲                      │
      │                   split_part(取第 N 段)
   unnest                     │
      │                      ▼
  数组 ◀──string_to_array── 分隔串
      │
      └──array_to_string──▶ 分隔串

最小可运行示例:

sql
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
text
 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 超过段数)则静默返回空串。

语法与用法 ​

sql
-- 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

最小可运行示例:

sql
-- 聚合:把同一分类下的 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;
text
 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 时顺序不确定——这一点很关键:

sql
-- 同一个查询连跑两次,顺序可能不同(并行扫描、索引顺序、更新历史都会影响)
SELECT category, string_agg(sku, ', ') AS skus
FROM products
GROUP BY category
ORDER BY category;
text
 category |               skus
----------+-----------------------------------
 book     | CN-BOOK-20260004
 elec     | CN-ELEC-20260005, CN-ELEC-20260001, CN-ELEC-20260002
 home     | US-HOME-20260003

准备演示数据(示例库 demo):

sql
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);
text
INSERT 0 3
INSERT 0 6

详细说明 ​

split_part 的边界语义 ​

split_part 只返回一个片段,不做类型转换,也不生成数组。它的边界行为必须记牢:

sql
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
text
 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:

sql
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
text
   basic   | with_empty | with_null_str | trailing  | empty_input | null_input | len
-----------+------------+---------------+-----------+-------------+------------+-----
 {a,b,c}   | {a,"",c}   | {a,NULL,c}    | {a,b,""}  | {}          |            |   3

string_to_array('', ',') 返回空数组而非含一个空串的数组

这一点与直觉相反:空输入得到 {}(零个元素),而 array_length 对空数组返回 NULL 而非 0。写 WHERE array_length(string_to_array(tags, ','), 1) > 0 时,空串行会被过滤掉(这是正确的),但 array_length(...) = 0 永远不为真(这也是对的)。

string_to_array 最常用的场景是把逗号串展开成多行:

sql
-- 把 tags 展开成每个标签一行
SELECT
  id,
  sku,
  unnest(string_to_array(tags, ',')) AS tag
FROM products
ORDER BY id, tag;
text
 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

展开后可以正常做聚合统计,这是「反范式存储 + 查询时展开」的经典解法:

sql
-- 统计每个标签的商品数
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;
text
   tag    | product_count
----------+---------------
 usb      |             3
 backlit  |             1
 black    |             1
 ceramic  |             1
 database |             1

array_to_string 与 null_string 的差异 ​

array_to_string(array, delimiter [, null_string]) 与 string_to_array 的 null_string 语义方向相反但对称:

sql
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
text
 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)。如果想保留占位,必须传第三个参数:

sql
SELECT array_to_string(ARRAY['a',NULL,'c'], '-', '');   -- a--c(用空串占位)

string_agg 的顺序问题 ​

string_agg(value, delimiter [ORDER BY ...]) 是聚合函数,不保证结果顺序。这是最容易在生产出问题的点:测试环境数据量小、单线程,看起来顺序稳定;上线后数据量变大触发并行扫描,顺序就变了。

三种控制顺序的方式:

sql
-- 方式一:聚合内部 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;
text
   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
sql
-- 方式二:子查询里先排序(老代码常见写法,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;
text
   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
sql
-- 方式三:不排序(错误示范)
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;
text
   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 ​

sql
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;
text
        distinct_cats        |                          distinct_tags                           | all_null | total_rows | non_null_rows
-----------------------------+-------------------------------------------------------------+----------+------------+---------------
 book, elec, home            | usb,backlit; usb,type-c; wireless,usb,black; ...             |          |          5 |             5
  • string_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,多一层
sql
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;
text
   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 不足时会落盘,性能急剧下降:

sql
-- 观察聚合的耗时与内存
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
SELECT
  category,
  string_agg(sku, ', ' ORDER BY sku) AS skus
FROM products
GROUP BY category;
text
                            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 聚合也要额外排序去重:

sql
EXPLAIN (ANALYZE, COSTS OFF)
SELECT string_agg(DISTINCT category, ',' ORDER BY category) FROM products;
text
                            QUERY PLAN
-------------------------------------------------------------------
 Aggregate
   ->  Seq Scan on products
 Planning Time: 0.086 ms
 Execution Time: 0.031 ms

对超大基数的去重聚合,DISTINCT 的开销与基数成正比;如果只需「大概去重」,可考虑先 GROUP BY 再用 string_agg。

实战示例 ​

场景一:订单商品清单(明细拼一行) ​

sql
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;
text
   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.00

i.sku || ' x' || i.qty 里 qty 是 integer,|| 会隐式转换为文本,不必手写 qty::text。

场景二:标签统计(展开 → 聚合) ​

sql
-- 把 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;
text
 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 列转成一张规范的关系表,便于建索引和做外键:

sql
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;
text
 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(...)),且空串返回零行:

sql
-- PostgreSQL 14+
SELECT id, sku, tag
FROM products,
     string_to_table(tags, ',') AS tag
WHERE id <= 2
ORDER BY id, tag;
text
 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[]) 返回零行,也是零行)。

注意事项 ​

  1. split_part 越界返回空串而不是 NULL,无法与「空段」区分:

    sql
    SELECT split_part('a,b', ',', 3);    -- ''(不是 NULL)
    SELECT split_part('a,b,', ',', 3);   -- ''(这是真的空段)
  2. split_part 的 n 必须大于 0,传 0 或负数直接报错:

    sql
    SELECT split_part('a,b', ',', 0);    -- 错误!field position must be greater than zero
  3. string_agg 不写 ORDER BY 时结果顺序不确定。这是生产事故高发点:

    sql
    -- 不推荐!顺序随时可能变
    SELECT string_agg(sku, ',') FROM order_items;
    -- 正确
    SELECT string_agg(sku, ',' ORDER BY sku) FROM order_items;
  4. string_agg 忽略 NULL,全 NULL 时返回 NULL 而非空串:

    sql
    SELECT string_agg(NULL::text, ',');   -- NULL
  5. array_to_string 也忽略 NULL,且不产生占位分隔符:

    sql
    SELECT array_to_string(ARRAY['a',NULL,'c'], '-');       -- a-c
    SELECT array_to_string(ARRAY['a',NULL,'c'], '-', '?');  -- a-?-c
  6. string_to_array('', ',') 返回空数组,array_length 对它返回 NULL 而非 0:

    sql
    SELECT string_to_array('', ',');                                  -- {}
    SELECT array_length(string_to_array('', ','), 1);                 -- NULL
    SELECT array_length(string_to_array('a', ','), 1);                -- 1
  7. string_to_array 的分隔符是多字符时按整串匹配,不是字符集合:

    sql
    SELECT string_to_array('a::b::c', '::');    -- {a,b,c}(正确)
    SELECT string_to_array('a:b:c', '::');      -- {a:b:c}(没找到 '::')
  8. 分隔符为空串时 string_to_array 不按字符切分:

    sql
    SELECT string_to_array('abc', '');   -- {a,b,c}?实际返回 {abc}

    想逐字符切分应改用 regexp_split_to_array(s, '') 或 string_to_table(s, NULL)。

  9. 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;
  10. string_agg 的 delimiter 不能是 NULL,否则整个结果为 NULL:

    sql
    SELECT string_agg(sku, NULL) FROM order_items;   -- NULL
  11. string_agg 有内存上限。聚合结果超过 1 GB(text 单值上限)会报错,超大结果集应先分页或改用数组。

  12. split_part 结果为空串时做类型转换会失败。把越界得到的 '' 转成 integer 会抛错:

    sql
    -- 错误!如果第 3 段不存在,''::integer 报错
    SELECT split_part(sku, '-', 3)::integer FROM products;
    -- 正确做法:先判空
    SELECT NULLIF(split_part(sku, '-', 3), '')::integer FROM products;
  13. string_to_array 的 null_string 与查询中的空串语义不同,容易混淆:

    sql
    SELECT string_to_array('a,,c', ',')          AS keep_empty;  -- {a,"",c}
    SELECT string_to_array('a,,c', ',', '')      AS null_empty;  -- {a,NULL,c}(空段变 NULL)
  14. 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%';

最佳实践 ​

  1. 凡是 string_agg 都显式写 ORDER BY。即使当前看起来顺序稳定,也应把它当作强制规范。推荐把排序键与分隔符一并注释。

  2. 优先选 string_to_table(PG 14+)替代 unnest(string_to_array(...))。更短、语义更清晰,且空串行为更合理。

  3. 展开分隔串时永远 trim()。真实数据里 'a, b' 的空格会导致聚合失真:

    sql
    SELECT trim(tag) FROM products, unnest(string_to_array(tags, ',')) AS tag;
  4. 需要正确性就别用分隔串存多值。一旦需要按标签查询、统计、建唯一约束,就应规范化为 product_tags 这样的关联表。分隔串只适合「整体读写、不查询」的场景。

  5. split_part 的结果做类型转换前先 NULLIF(..., ''),把越界的空串转成 NULL,避免转换报错。

  6. split_part 只适合取「固定位置」的段。若要取最后一段或按模式取段,优先用 regexp_replace / regexp_match;PostgreSQL 14+ 还可用 split_part 配合 array_length 反向索引(用 string_to_array)。

  7. 聚合大结果前评估 work_mem。看 EXPLAIN (ANALYZE) 里 Sort Method 是否落盘,落盘就调大 work_mem 或改用有索引的排序键。

  8. 给聚合的排序键建索引。string_agg(x, ',' ORDER BY created_at) 若 created_at 上有索引,可以直接走 GroupAggregate 而免去显式排序。

  9. 不要用 string_agg 生成 SQL 或 JSON。分隔串里若含分隔符或引号会破坏结构。生成结构化输出应改用 json_agg / jsonb_agg:

    sql
    SELECT 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;
    text
        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"]
  10. string_agg 的 delimiter 用常量,别从列里取。逐行取不同分隔符会让结果无法解析,也会阻止某些优化。

  11. DISTINCT + ORDER BY 时保持两者表达式一致,否则去重与排序的依据不同,结果可能反直觉:

    sql
    -- 推荐:两者都用 category
    SELECT string_agg(DISTINCT category, ',' ORDER BY category) FROM products;
  12. 把「解析分隔串」的逻辑收敛到视图或函数。散落在多个查询里的 split_part 调用一旦格式变化就会集体失效。

参考链接 ​