数组访问与修改
本节讲解如何用下标访问数组元素、用切片读取子数组,以及如何用 array_append、array_prepend、array_cat、array_remove 等函数增删元素,最后给出"整体替换 vs 局部修改"的取舍建议。学完本节,你将能熟练地对数组列做读写。
前置知识
阅读本节前,建议先了解:数组声明与初始化
基础概念
下标(Subscript) 是访问数组元素的位置序号。PostgreSQL 的数组下标默认从 1 开始,这与 C、Java、Python 等语言的 0 起始下标完全不同,也是最常见的踩坑点。
| 概念 | 术语 | 说明 |
|---|---|---|
| 下标 | Subscript | 元素位置序号,默认从 1 开始 |
| 下标越界 | Out of Bounds | 访问不存在的位置,返回 NULL 而不报错 |
| 切片 | Slice | 用 arr[下界:上界] 读取连续子数组 |
| 下界 | Lower Bound | 数组第一个元素的下标 |
| 上界 | Upper Bound | 数组最后一个元素的下标 |
| 整行重写 | Row Rewrite | 更新数组元素时 PostgreSQL 写入全新行版本 |
下标访问与切片的核心差异:
| 操作 | 语法 | 返回值类型 | 越界行为 |
|---|---|---|---|
| 单元素访问 | arr[i] | 元素类型 | 返回 NULL |
| 切片 | arr[i:j] | 数组类型 | 自动裁剪到有效范围 |
| 多维单元素 | arr[i][j] | 元素类型 | 返回 NULL |
| 多维切片 | arr[i:j][k:l] | 数组类型 | 自动裁剪 |
-- 下标越界不报错,静默返回 NULL —— 这是最容易埋 bug 的行为
SELECT
('{a,b,c}'::text[])[1] AS in_range, -- 有效
('{a,b,c}'::text[])[3] AS last, -- 有效
('{a,b,c}'::text[])[4] AS out_of_range, -- NULL,不报错
('{a,b,c}'::text[])[0] AS zero_index; -- NULL,不报错 in_range | last | out_of_range | zero_index
----------+------+--------------+------------
a | c | |
(1 row)切片越界则会被裁剪,而不是返回 NULL:
-- 切片超出范围会自动收缩到有效区间
SELECT
('{a,b,c}'::text[])[2:4] AS slice_cropped, -- {b,c},4 被裁掉
('{a,b,c}'::text[])[4:6] AS slice_empty, -- {},完全超出
('{a,b,c}'::text[])[0:2] AS slice_from_zero; -- {a,b},0 被裁掉 slice_cropped | slice_empty | slice_from_zero
---------------+-------------+-----------------
{b,c} | {} | {a,b}
(1 row)语法与用法
数组访问与修改的操作一览:
| 操作 | 语法 / 函数 | 说明 |
|---|---|---|
| 读取单元素 | arr[i] | 越界返回 NULL |
| 读取切片 | arr[i:j] | 返回子数组 |
| 写入单元素 | UPDATE t SET arr[i] = v | 下标必须已存在 |
| 整体替换 | UPDATE t SET arr = '{...}' | 替换整个数组 |
| 尾部追加 | array_append(arr, v) | 返回新数组 |
| 头部插入 | array_prepend(v, arr) | 注意参数顺序 |
| 拼接 | array_cat(a, b) 或 a || b | 数组 + 数组 |
| 移除元素 | array_remove(arr, v) | 移除所有等于 v 的元素 |
| 替换元素 | array_replace(arr, from, to) | 替换所有匹配元素 |
| 指定位置插入 | 无内置函数 | 需用切片手工拼接 |
最简可运行示例:
-- 拜访记录表
CREATE TABLE visits (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL,
pages TEXT[] NOT NULL DEFAULT '{}' -- 访问过的页面路径序列
);
INSERT INTO visits (user_id, pages) VALUES
(1001, ARRAY['/home', '/products', '/cart', '/checkout']),
(1002, ARRAY['/home', '/about'])
RETURNING id, user_id, pages; id | user_id | pages
----+---------+----------------------------------------
1 | 1001 | {/home,/products,/cart,/checkout}
2 | 1002 | {/home,/about}
(2 rows)-- 读取首末页、中间切片
SELECT
user_id,
pages[1] AS first_page,
pages[2] AS second_page,
pages[2:3] AS middle_slice,
pages[cardinality(pages)] AS last_page,
cardinality(pages) AS page_cnt
FROM visits
ORDER BY user_id; user_id | first_page | second_page | middle_slice | last_page | page_cnt
---------+------------+-------------+----------------------+-----------+----------
1001 | /home | /products | {/products,/cart} | /checkout | 4
1002 | /home | /about | {/about} | /about | 2
(2 rows)详细说明
下标访问的细节
用 cardinality 或 array_length 安全地访问最后一个元素,避免硬编码下标:
-- 错误!硬编码下标越界会静默返回 NULL
SELECT user_id, pages[4] AS four_page FROM visits; -- 1002 返回 NULL
-- 推荐:用 cardinality 动态取末位
SELECT user_id, pages[cardinality(pages)] AS last_page FROM visits ORDER BY user_id;多维数组的下标是逐维书写的,不用逗号:
-- 二维数组访问
SELECT
('{{1,2},{3,4}}'::int[])[1][1] AS r1c1, -- 1
('{{1,2},{3,4}}'::int[])[1][2] AS r1c2, -- 2
('{{1,2},{3,4}}'::int[])[2][1] AS r2c1, -- 3
('{{1,2},{3,4}}'::int[])[1] AS row1; -- {1,2},只写一维得到子数组 r1c1 | r1c2 | r2c1 | row1
------+------+------+-------
1 | 2 | 3 | {1,2}
(1 row)错误写法:arr[1,2] 这种逗号语法在 PostgreSQL 中不合法。
-- 错误!PostgreSQL 不支持逗号下标
SELECT ('{{1,2},{3,4}}'::int[])[1,2];ERROR: syntax error at or near ","对切片产生的数组,下界不再从 1 开始,此时用 array_length 与直觉不符:
SELECT
a,
array_dims(a) AS dims,
a[1] AS idx1, -- 用绝对下标会取错
a[array_lower(a, 1)] AS real_first -- 正确:按下界取
FROM (SELECT ('{a,b,c,d,e}'::text[])[2:4] AS a) t; a | dims | idx1 | real_first
---------+------+------+------------
{b,c,d} | [2:4]| b | b
(1 row)这个例子中下标恰好对齐,但若切片是 [3:5],则 a[1] 会返回 NULL:
SELECT
('{a,b,c,d,e}'::text[])[3:5] AS sliced,
('{a,b,c,d,e}'::text[])[3:5][1] AS wrong_idx, -- NULL,下界是 3 不是 1
('{a,b,c,d,e}'::text[])[3:5][3] AS right_idx; -- c sliced | wrong_idx | right_idx
---------+-----------+-----------
{c,d,e} | | c
(1 row)更新单个元素
用 UPDATE ... SET arr[i] = v 修改已存在的下标位置。下标必须已存在,否则报错(这是与"读取越界返回 NULL"的重要区别):
-- 修改第 2 个页面
UPDATE visits SET pages[2] = '/products-v2' WHERE user_id = 1001
RETURNING user_id, pages; user_id | pages
---------+----------------------------------------
1001 | {/home,/products-v2,/cart,/checkout}
(1 row)-- 错误!写入不存在的下标会报错
UPDATE visits SET pages[10] = '/new' WHERE user_id = 1001;ERROR: array subscript out of range如果需要"按下标赋值时自动扩展",可以先用 array_fill 或直接构造新数组:
-- 手工扩展到指定长度后再赋值
UPDATE visits
SET pages = (pages || array_fill(NULL::text, ARRAY[10 - cardinality(pages)]))[1:10]
WHERE user_id = 1002
RETURNING user_id, pages, cardinality(pages) AS len; user_id | pages | len
---------+--------------------------------------------------------------+-----
1002 | {/home,/about,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL} | 10
(1 row)这种写法可读性很差,通常应在应用层构造完整数组后整体替换。
切片赋值
PostgreSQL 支持对切片赋值:新数组长度可以与原切片长度不同,长度不同时相当于"替换并伸缩":
CREATE TABLE seq_demo (id INT, arr TEXT[]);
INSERT INTO seq_demo VALUES (1, ARRAY['a', 'b', 'c', 'd', 'e']);
-- 等长替换
UPDATE seq_demo SET arr[2:3] = ARRAY['X', 'Y'] WHERE id = 1
RETURNING arr; -- {a,X,Y,d,e} arr
---------------
{a,X,Y,d,e}
(1 row)-- 新切片更长:结果数组变长
UPDATE seq_demo SET arr[2:3] = ARRAY['X', 'Y', 'Z'] WHERE id = 1
RETURNING arr, cardinality(arr) AS len; arr | len
----------------+-----
{a,X,Y,Z,d,e} | 6
(1 row)-- 新切片更短:结果数组变短
UPDATE seq_demo SET arr[2:4] = ARRAY['Q'] WHERE id = 1
RETURNING arr, cardinality(arr) AS len; arr | len
------------+-----
{a,Q,e} | 3
(1 row)切片赋值是把双刃剑:灵活但极易写出长度不可控的代码,生产环境建议改用显式的整体替换。
追加与前置元素
| 函数 | 签名 | 返回 | 注意 |
|---|---|---|---|
array_append | array_append(arr, elem) | 新数组,元素在末尾 | 元素类型需匹配 |
array_prepend | array_prepend(elem, arr) | 新数组,元素在开头 | 元素在前,数组在后 |
array_cat | array_cat(a, b) | 新数组 | 两个数组拼接 |
-- 追加:新的浏览记录写进数组尾部
UPDATE visits SET pages = array_append(pages, '/order-confirm')
WHERE user_id = 1001
RETURNING user_id, pages; user_id | pages
---------+---------------------------------------------------
1001 | {/home,/products-v2,/cart,/checkout,/order-confirm}
(1 row)-- 前置:参数顺序是 元素 在前
UPDATE visits SET pages = array_prepend('/landing', pages)
WHERE user_id = 1002
RETURNING user_id, pages; user_id | pages
---------+------------------------------
1002 | {/landing,/home,/about,...}
(1 row)参数顺序记法是:函数名里的动作词修饰的对象放在最后。array_prepend(x, arr) 读作"把 x 前置到 arr"。
-- 错误!参数顺序反了会导致类型不匹配或语义错乱
-- SELECT array_prepend(ARRAY['/a'], ARRAY['/b','/c']); -- 返回 {/a,/b,/c},看似正常
-- SELECT array_prepend('/a', ARRAY['/b']); -- 正确:{/a,/b}拼接用 array_cat 或 || 操作符:
-- 两个数组拼接
SELECT
array_cat(ARRAY[1, 2], ARRAY[3, 4]) AS via_func,
ARRAY[1, 2] || ARRAY[3, 4] AS via_operator,
ARRAY[1, 2] || 3 AS arr_plus_scalar, -- 标量也可拼接
0 || ARRAY[1, 2] AS scalar_plus_arr; via_func | via_operator | arr_plus_scalar | scalar_plus_arr
----------+--------------+-----------------+-----------------
{1,2,3,4}| {1,2,3,4} | {1,2,3} | {0,1,2}
(1 row)注意 || 的歧义:当两个操作数都是数组时是数组拼接,当元素类型是文本时可能被解析为字符串连接。显式用 array_cat 可避免歧义。
-- 危险:当元素恰好是 text 数组,|| 的语义容易混淆
SELECT ARRAY['a','b'] || 'c' AS arr_and_text; -- {a,b,c},数组拼接
SELECT 'a' || 'b' AS text_concat; -- 'ab',字符串连接
SELECT '{a,b}'::text[] || '{c}'::text[] AS arr_arr; -- {a,b,c}移除与替换元素
| 函数 | 作用 | 行为 |
|---|---|---|
array_remove(arr, v) | 移除所有等于 v 的元素 | 用 = 比较,NULL 不会被移除 |
array_replace(arr, from, to) | 把所有 from 替换为 to | 若无匹配则原样返回 |
array_position(arr, v) | 返回首个匹配位置 | 未找到返回 NULL |
array_positions(arr, v) | 返回所有匹配位置数组 | 未找到返回 {} |
-- 移除元素
SELECT
array_remove(ARRAY['a', 'b', 'a', 'c'], 'a') AS removed_all, -- 移除全部 a
array_replace(ARRAY['a', 'b', 'a'], 'a', 'X') AS replaced_all, -- 替换全部 a
array_position(ARRAY['a', 'b', 'a'], 'a') AS first_pos, -- 1
array_positions(ARRAY['a', 'b', 'a'], 'a') AS all_pos; -- {1,3} removed_all | replaced_all | first_pos | all_pos
-------------+--------------+-----------+---------
{b,c} | {X,b,X} | 1 | {1,3}
(1 row)array_remove 无法移除 NULL 元素,因为内部用 = 比较而 NULL = NULL 不为真:
-- 错误!NULL 元素不会被 array_remove 移除
SELECT array_remove(ARRAY['a', NULL, 'b'], NULL) AS still_has_null; -- {a,NULL,b}
-- 正确:用 array_position + 过滤,或 array_agg 重建
SELECT array_agg(x) FILTER (WHERE x IS NOT NULL) AS no_null
FROM unnest(ARRAY['a', NULL, 'b']) AS x; still_has_null | no_null
----------------+---------
{a,NULL,b} | {a,b}
(1 row)"在指定位置插入元素"没有内置函数,需要用切片拼接实现:
-- 在第 3 个位置插入元素(不含删除原元素)
CREATE OR REPLACE FUNCTION array_insert_at(arr anyarray, pos int, elem anyelement)
RETURNS anyarray AS $$
SELECT arr[1:pos-1] || elem || arr[pos:array_length(arr, 1)]
$$ LANGUAGE sql IMMUTABLE;
SELECT array_insert_at(ARRAY['a', 'b', 'c'], 2, 'X') AS inserted; inserted
------------------
{a,X,b,c}
(1 row)整体替换 vs 局部修改
数组的存储特性决定了:任何局部修改本质上都是整行重写。PostgreSQL 的更新会写入全新的行版本(MVCC 机制),旧版本留给 VACUUM 清理。
| 对比项 | 局部修改 arr[i] = v | 整体替换 arr = '{...}' |
|---|---|---|
| 可读性 | 简洁 | 需构造完整数组 |
| 下标处理 | 必须已存在,否则报错 | 无下标约束 |
| 并发冲突 | 可能覆盖他人的修改 | 同样可能覆盖 |
| 底层代价 | 整行重写(与整体替换相同) | 整行重写 |
| 适用场景 | 已知下标、单点小改 | 批量修改、需要重新排序 |
-- 场景:把标签中的 old-tag 换成 new-tag
-- 写法一:整体替换(推荐,语义清晰)
UPDATE articles
SET tags = array_replace(tags, 'old-tag', 'new-tag')
WHERE tags @> ARRAY['old-tag'];
-- 写法二:局部修改(只能改一个位置,无法处理重复元素)
-- 需要先知道下标,且多个 old-tag 时只改一个,语义不完整并发场景下推荐用"基于当前值的原子更新"写法,避免读-改-写竞态:
-- 推荐:把追加逻辑写进一条 UPDATE,避免应用层读后再写
UPDATE visits
SET pages = array_append(pages, '/new-page')
WHERE user_id = 1001
RETURNING pages;
-- 不推荐:应用层先 SELECT 再拼接再 UPDATE,两次并发会丢失一次修改实战示例
场景:购物车表用数组存储商品 SKU 与数量,演示增删改查全流程。
第 1 步:建表
CREATE TABLE carts (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL UNIQUE,
skus TEXT[] NOT NULL DEFAULT '{}',
quantities INTEGER[] NOT NULL DEFAULT '{}',
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- 两个数组长度必须一致
CONSTRAINT chk_len_match CHECK (cardinality(skus) = cardinality(quantities))
);
INSERT INTO carts (user_id, skus, quantities) VALUES
(1001, ARRAY['SKU-1001', 'SKU-1002'], ARRAY[2, 1]),
(1002, ARRAY['SKU-1003'], ARRAY[5]);CREATE TABLE
INSERT 0 2长度一致性约束会拦住不匹配的写入:
-- 触发约束,写入失败
INSERT INTO carts (user_id, skus, quantities) VALUES (1003, ARRAY['SKU-1'], ARRAY[1, 2]);ERROR: new row for relation "carts" violates check constraint "chk_len_match"第 2 步:加入购物车(追加或累加数量)
-- 情况 A:商品不在购物车中,追加到末尾
UPDATE carts
SET skus = array_append(skus, 'SKU-1005'),
quantities = array_append(quantities, 3),
updated_at = now()
WHERE user_id = 1001
AND NOT (skus @> ARRAY['SKU-1005'])
RETURNING user_id, skus, quantities; user_id | skus | quantities
---------+-----------------------------+------------
1001 | {SKU-1001,SKU-1002,SKU-1005} | {2,1,3}
(1 row)-- 情况 B:商品已在购物车中,按位置累加数量(用 array_position 定位)
UPDATE carts
SET quantities[array_position(skus, 'SKU-1002')] =
quantities[array_position(skus, 'SKU-1002')] + 4,
updated_at = now()
WHERE user_id = 1001
AND skus @> ARRAY['SKU-1002']
RETURNING user_id, skus, quantities; user_id | skus | quantities
---------+-----------------------------+------------
1001 | {SKU-1001,SKU-1002,SKU-1005} | {2,5,3}
(1 row)这里连续调用了两次 array_position,可读性一般。更清晰的写法是用 FROM 子句先算下标(PostgreSQL 支持在 UPDATE ... FROM 中引用):
-- 更清晰的写法:先在 FROM 中定位下标
UPDATE carts c
SET quantities[pos] = quantities[pos] + 1
FROM (SELECT array_position(skus, 'SKU-1001') AS pos FROM carts WHERE user_id = 1001) t
WHERE c.user_id = 1001
RETURNING c.user_id, c.skus, c.quantities; user_id | skus | quantities
---------+-----------------------------+------------
1001 | {SKU-1001,SKU-1002,SKU-1005} | {3,5,3}
(1 row)第 3 步:删除商品
删除时要同时从两个数组中移除同一位置的元素,这是数组建模最容易出错的地方:
-- 关键:先算出要删除的下标,再一次性移除
WITH target AS (
SELECT
array_position(skus, 'SKU-1002') AS pos,
skus, quantities
FROM carts
WHERE user_id = 1001
)
UPDATE carts c
SET skus = (SELECT skus[:pos-1] || skus[pos+1:] FROM target),
quantities = (SELECT quantities[:pos-1] || quantities[pos+1:] FROM target),
updated_at = now()
WHERE c.user_id = 1001
RETURNING c.user_id, c.skus, c.quantities; user_id | skus | quantities
---------+-----------------------+------------
1001 | {SKU-1001,SKU-1005} | {3,3}
(1 row)如果改用 array_remove 分别处理两个数组,就会出现下标错位的严重 bug:
-- 错误!两个数组各自移除,长度可能不再对应
UPDATE carts
SET skus = array_remove(skus, 'SKU-1002'),
quantities = array_remove(quantities, 1) -- 假设同时移除数量 1,逻辑不成立
WHERE user_id = 1001;第 4 步:读取完整购物车
用 unnest 按位置配对展开两个数组:
SELECT
c.user_id,
t.sku,
t.qty,
t.idx
FROM carts c,
unnest(c.skus, c.quantities) WITH ORDINALITY AS t(sku, qty, idx)
WHERE c.user_id = 1001
ORDER BY t.idx; user_id | sku | qty | idx
---------+-----------+-----+-----
1001 | SKU-1001 | 3 | 1
1001 | SKU-1005 | 3 | 2
(2 rows)WITH ORDINALITY 是常用的辅助子句,它给 unnest 的每一行附加一个从 1 开始的序号,正好对应数组位置。
第 5 步:清空购物车
-- 重置为初始状态:整体替换比逐个 remove 更清晰
UPDATE carts
SET skus = '{}', quantities = '{}', updated_at = now()
WHERE user_id = 1001
RETURNING user_id, skus, quantities, cardinality(skus) AS len; user_id | skus | quantities | len
---------+------+------------+-----
1001 | {} | {} | 0
(1 row)第 6 步:清理
DROP TABLE carts;
DROP TABLE visits;
DROP TABLE seq_demo;
DROP FUNCTION IF EXISTS array_insert_at(anyarray, int, anyelement);注意事项
- 下标从 1 开始,
arr[0]返回NULL而不是第一个元素。这是从其他语言迁移过来时最常犯的错误。
-- 错误!以为 arr[0] 是第一个元素
SELECT ('{a,b,c}'::text[])[0]; -- NULL
-- 正确
SELECT ('{a,b,c}'::text[])[1]; -- a- 读取越界返回
NULL,写入越界报错。两者行为不一致,读取时务必做长度校验。
-- 读取:静默返回 NULL,可能把 bug 藏很久
SELECT ('{a,b}'::text[])[5]; -- NULL
-- 写入:直接报错
-- UPDATE t SET arr[5] = 'x'; -- ERROR: array subscript out of rangearray_remove无法移除NULL。因为内部用=比较,而NULL = NULL结果为未知。
-- 无效:NULL 仍然在数组里
SELECT array_remove(ARRAY[1, NULL, 2], NULL); -- {1,NULL,2}
-- 有效:用 unnest + 过滤重建
SELECT array_agg(x) FILTER (WHERE x IS NOT NULL)
FROM unnest(ARRAY[1, NULL, 2]) AS x;array_position找不到元素时返回NULL,若直接拿它做下标会写入失败:
-- 危险:元素不存在时 pos 为 NULL,赋值报错
-- UPDATE carts SET quantities[array_position(skus, 'NOT-EXIST')] = 1;
-- 安全:先用 WHERE 确保存在
UPDATE carts SET quantities[array_position(skus, 'SKU-1001')] = 10
WHERE user_id = 1001 AND skus @> ARRAY['SKU-1001'];- 切片赋值会改变数组长度,容易写出长度失控的代码:
-- 危险:新切片长度与原切片不同会导致数组伸缩
UPDATE t SET arr[2:3] = ARRAY['a', 'b', 'c']; -- 数组变长
-- 推荐:需要改长度时显式整体替换
UPDATE t SET arr = ARRAY['a', 'b', 'c'];||操作符有歧义。文本数组与文本拼接时,语义取决于类型推断结果,显式用array_cat更安全。array_prepend参数顺序与array_append相反。array_prepend(elem, arr)是元素在前,很容易写反。
-- 记住:append 是"数组, 元素",prepend 是"元素, 数组"
SELECT array_append(ARRAY[1,2], 3); -- {1,2,3}
SELECT array_prepend(0, ARRAY[1,2]); -- {0,1,2}- 不要用
arr[i] = arr[i] + 1这种读-改-写更新数组做计数器。虽然单条 UPDATE 是原子的,但可读性差且无法处理"元素不存在需新增"的情况;计数场景应改用关系表或 JSONB。 - 数组更新永远是整行重写。即便只改一个元素,PostgreSQL 也会写入包含整个数组的新行版本,大数组上代价明显。
-- 观察行重写:更新一个元素后,旧版本仍然占用空间直到 VACUUM
UPDATE events SET tags[1] = 'x' WHERE id = 1;
-- 查看表的死元组数量
SELECT n_live_tup, n_dead_tup FROM pg_stat_user_tables WHERE relname = 'events';- 局部修改数组元素无法使用索引。数组上的 GIN 索引只服务于集合操作符(
@>、&&等),不能加速arr[i] = v这类单元素条件。
-- 这个条件不会走 tags 的 GIN 索引
SELECT * FROM events WHERE tags[1] = 'tech';最佳实践
- 优先整数组替换,慎用局部下标修改。整体替换的语义完整(能处理重复元素、能同时改多个位置),代码更易读。
-- 推荐:用 array_replace 处理"替换某值"的需求
UPDATE articles SET tags = array_replace(tags, 'old', 'new') WHERE tags @> ARRAY['old'];
-- 推荐:用 array_append / array_remove 处理增删
UPDATE articles SET tags = array_append(tags, 'new-tag') WHERE id = 1;
UPDATE articles SET tags = array_remove(tags, 'old-tag') WHERE id = 1;- 需要下标时用
array_position动态计算,绝不硬编码位置数字。
-- 推荐
SET quantities[array_position(skus, 'SKU-1001')] = 10
-- 不推荐
SET quantities[1] = 10- 多数组建模时加
CHECK (cardinality(a) = cardinality(b)),强制两个数组长度一致,避免下标错位。
CONSTRAINT chk_len_match CHECK (cardinality(skus) = cardinality(quantities))- 删除"配对数组"中的元素要按同一位置移除,用切片拼接,不要分别
array_remove。
-- 正确:同一下标
SET skus = skus[:pos-1] || skus[pos+1:],
quantities = quantities[:pos-1] || quantities[pos+1:]- 读取数组用
unnest ... WITH ORDINALITY,把数组转成"带位置的行",后续查询和连接都方便。
SELECT t.idx, t.page
FROM visits v, unnest(v.pages) WITH ORDINALITY AS t(page, idx)
WHERE v.user_id = 1001
ORDER BY t.idx;- 并发场景把修改逻辑写进单条
UPDATE,用array_append/array_remove/array_replace保证原子性,不要先SELECT再UPDATE。
-- 推荐:一条语句完成"不存在则追加"
UPDATE articles
SET tags = array_append(tags, 'featured')
WHERE id = 1 AND NOT (tags @> ARRAY['featured']);- 对即将更新的数组先判长度,防止写入越界。写入越界会抛错,而读取越界静默返回
NULL,两种行为都不应依赖。
-- 只在长度足够时修改该下标
UPDATE t SET arr[2] = 'x'
WHERE id = 1 AND cardinality(arr) >= 2;- 数组元素数量控制在几十个以内,超过就改关系表。数组越长,每次局部更新的整行重写代价越高,TOAST 压缩也可能被触发。
下一节
继续学习:数组操作符