Skip to content

数组声明与初始化 ​

数组(Array)是 PostgreSQL 中最实用的复合类型之一:它允许在单个列中存储同类型的多个值,省去额外的关联表,并支持包含、重叠、切片等原生操作。本节讲解数组类型的声明语法、字面量写法、ARRAY[] 构造器、多维数组声明,以及 array_length / array_dims 的入门用法。

前置知识

阅读本节前,建议先了解:JSONB 应用场景

基础概念 ​

数组(Array) 是同一数据类型元素的有序集合,存储在单个列值中。它不同于关系表:数组元素没有独立的行标识,不能让其他表通过外键引用某个元素。

概念术语说明
数组Array同一类型元素的有序集合,存于单列
元素类型Element Type数组内所有元素的数据类型,必须一致
维度Dimension数组的嵌套层数,一维/二维/…
下标Subscript访问元素的位置序号,PostgreSQL 从 1 开始
数组字面量Array Literal'{1,2,3}' 形式的文本表示
数组构造器Array ConstructorARRAY[1,2,3] 形式的表达式
空数组Empty Array零个元素的数组,写作 '{}',不是 NULL
数组类型名Array Type Nameinteger[] 或 _int4(内部名)

必须先理解数组与 NULL 的两层区别:

情形写法含义
整列为空NULL该列没有值,未知
零元素数组'{}'该列有一个值,这个值是空集合
元素为空'{1,NULL,3}'数组有 3 个位置,中间那个未知
sql
-- 三者语义完全不同
SELECT
  NULL::int[]                AS null_array,      -- 列值为 NULL
  '{}'::int[]                AS empty_array,     -- 零元素数组
  '{1,NULL,3}'::int[]        AS with_null,       -- 含 NULL 元素
  array_length('{}'::int[], 1) AS empty_len,     -- 空数组长度为 NULL
  array_length(NULL::int[], 1) AS null_len;      -- NULL 数组长度也是 NULL
text
 null_array | empty_array | with_null  | empty_len | null_len
------------+-------------+------------+-----------+----------
            | {}          | {1,NULL,3} |           |
(1 row)

array_length 对空数组返回 NULL 而非 0,这是很多人第一次使用时踩到的坑。要判断"数组是否为空",应该用 cardinality(arr) = 0 或 arr = '{}'。

语法与用法 ​

数组类型的声明有多种等价写法:

写法示例说明
类型名 + 方括号integer[]最常用,推荐
类型名 + 方括号 + 长度integer[3]长度仅作文档,不强制
前置方括号[3][3] integer兼容旧写法,不推荐
ARRAY 关键字ARRAY可写作 float ARRAY[4],冗长
内部类型名_int4系统目录中的真实名字

重要:integer[3] 中的 3 不会限制数组长度,PostgreSQL 不强制数组维度约束。

sql
-- 四种等价的一维数组声明
CREATE TABLE t_array_syntax (
  col_a integer[],          -- 推荐写法
  col_b integer[3],         -- 长度不强制,仅作说明
  col_c integer ARRAY,      -- 标准 SQL 写法
  col_d integer ARRAY[3]    -- 标准 SQL + 长度
);

-- 插入 5 个元素也能成功,证明 [3] 不生效
INSERT INTO t_array_syntax (col_a, col_b, col_c, col_d)
VALUES ('{1,2,3,4,5}', '{1,2,3,4,5}', '{1,2,3,4,5}', '{1,2,3,4,5}')
RETURNING col_a, col_b;
text
   col_a   |   col_b
-----------+-----------
 {1,2,3,4,5} | {1,2,3,4,5}
(1 row)

最简可运行示例——数组列建表与插入:

sql
-- 事件表:一个事件可以有多个标签
CREATE TABLE events (
  id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name       TEXT        NOT NULL,
  tags       TEXT[]      NOT NULL DEFAULT '{}',   -- 空数组作为默认值
  scores     INTEGER[]   NOT NULL DEFAULT '{}',
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 三种插入数组的方式
INSERT INTO events (name, tags, scores) VALUES
  ('发布会', ARRAY['tech', 'launch', 'online'], ARRAY[90, 85, 78]),  -- ARRAY 构造器
  ('培训营', '{edu,offline,workshop}', '{88,92,95}'),                -- 字面量
  ('分享会', ARRAY['tech', 'offline'], ARRAY[70, 75]);               -- ARRAY 构造器

SELECT id, name, tags, scores FROM events ORDER BY id;
text
 id |  name  |           tags            |   scores
----+--------+---------------------------+------------
  1 | 发布会 | {tech,launch,online}      | {90,85,78}
  2 | 培训营 | {edu,offline,workshop}    | {88,92,95}
  3 | 分享会 | {tech,offline}            | {70,75}
(3 rows)

数组支持的类型几乎涵盖所有内置类型,包括自定义类型、枚举、复合类型:

sql
-- 各种元素类型的数组
CREATE TABLE t_element_types (
  id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  tags_num    INTEGER[]     NOT NULL DEFAULT '{}',   -- 整数
  tags_text   TEXT[]        NOT NULL DEFAULT '{}',   -- 文本
  prices      NUMERIC(10,2)[] NOT NULL DEFAULT '{}', -- 精确小数
  days        DATE[]        NOT NULL DEFAULT '{}',   -- 日期
  flags       BOOLEAN[]     NOT NULL DEFAULT '{}',   -- 布尔
  stamps      TIMESTAMPTZ[] NOT NULL DEFAULT '{}'    -- 带时区时间戳
);

INSERT INTO t_element_types (tags_num, tags_text, prices, days, flags, stamps)
VALUES (
  '{1,2,3}',
  '{a,b}',
  '{19.90,299.00}',
  '{2026-01-01,2026-02-14}',
  '{true,false,true}',
  '{"2026-01-01 10:00:00+08"}'
)
RETURNING tags_num, prices, days, flags;
text
 tags_num |     prices     |         days          |   flags
----------+----------------+-----------------------+------------
 {1,2,3}  | {19.90,299.00} | {2026-01-01,2026-02-14} | {t,f,t}
(1 row)

详细说明 ​

数组字面量的书写规则 ​

字面量用花括号包裹、逗号分隔。文本元素在含特殊字符时需要转义:

元素内容写法说明
普通文本'{a,b}'无需引号
含空格'{"hello world",b}'双引号包裹
含逗号'{"a,b",c}'双引号包裹
含双引号'{"say \"hi\"",c}'反斜杠转义内层双引号
含花括号'{"{x}",y}'双引号包裹
NULL 元素'{1,NULL,3}'大写 NULL 表示空元素
字符串 'NULL''{"NULL"}'加引号表示普通文本 NULL
sql
-- 各种转义场景
SELECT
  '{a,b,c}'::text[]              AS plain,
  '{"hello world","x,y"}'::text[] AS quoted,
  '{"say \"hi\""}'::text[]       AS escaped_quote,
  '{1,NULL,3}'::int[]            AS with_null,
  '{"NULL"}'::text[]             AS literal_null_text;
text
        plain        |       quoted        | escaped_quote | with_null  | literal_null_text
---------------------+---------------------+---------------+------------+-------------------
 {a,b,c}             | {"hello world","x,y"} | {"say \"hi\""} | {1,NULL,3} | {"NULL"}
(1 row)

反斜杠本身在标准符合字符串中需要转义。推荐对含特殊字符的数组使用 ARRAY[] 构造器,避免手工转义:

sql
-- 用 ARRAY[] 更安全,无需关心转义规则
SELECT ARRAY['hello world', 'x,y', 'say "hi"'] AS constructed;

-- 用字符串拼接时容易出错
-- 错误!未加引号导致元素被拆开
SELECT '{hello world}'::text[];   -- 会尝试解析为 2 个元素,实际报错
text
ERROR:  malformed array literal: "{hello world}"
DETAIL:  Unexpected " " character.

ARRAY 构造器 ​

ARRAY[...] 是表达式,与字面量不同:它接受任意表达式作为元素,能直接引用列,也能用子查询产出数组。

sql
-- 元素是表达式,不是字面文本
SELECT ARRAY[1 + 1, 2 * 3, upper('abc')] AS from_expr;

-- 引用列构造数组
SELECT name, ARRAY[name, upper(name), length(name)::text] AS derived
FROM events
ORDER BY id;
text
  name  |          derived
--------+----------------------------
 发布会 | {发布会,发布会,3}
 培训营 | {培训营,培训营,3}
 分享会 | {分享会,分享会,3}
(3 rows)

长 3 的中文是长度 3 的字符数(length 返回字符数,不是字节数)。

sql
-- 子查询产出数组
SELECT ARRAY(SELECT name FROM events ORDER BY id) AS all_names;
text
       all_names
------------------------
 {发布会,培训营,分享会}
(1 row)

ARRAY(SELECT ...) 会自动折叠多行为一个数组,等价于 array_agg,但更简洁:

sql
-- 两种等价写法
SELECT ARRAY(SELECT name FROM events ORDER BY id) AS via_subquery;

SELECT array_agg(name ORDER BY id) AS via_agg FROM events;
sql
-- 空结果集产出空数组,而不是 NULL
SELECT ARRAY(SELECT name FROM events WHERE false) AS empty_result;
text
 empty_result
--------------
 {}
(1 row)

多维数组的声明与初始化 ​

多维数组(Multidimensional Array) 在类型声明上和普通数组一样——维度不是类型的一部分。任意数组列都能存任意维度的值:

sql
-- 同一个列可以存 1 维、2 维、3 维数组
CREATE TABLE t_matrix (
  id   BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  grid INTEGER[]
);

INSERT INTO t_matrix (grid) VALUES
  ('{1,2,3}'),                          -- 1 维
  ('{{1,2},{3,4}}'),                    -- 2 维
  ('{{{1,2},{3,4}},{{5,6},{7,8}}}');    -- 3 维

SELECT id, grid, array_dims(grid) AS dims, array_ndims(grid) AS ndims
FROM t_matrix
ORDER BY id;
text
 id |              grid               |     dims     | ndims
----+---------------------------------+--------------+-------
  1 | {1,2,3}                         | [1:3]        |     1
  2 | {{1,2},{3,4}}                   | [1:2][1:2]   |     2
  3 | {{{1,2},{3,4}},{{5,6},{7,8}}}   | [1:2][1:2][1:2] |  3
(3 rows)

用 ARRAY[] 构造多维数组时需要嵌套:

sql
-- 嵌套构造器产出 2 维数组
SELECT ARRAY[[1, 2], [3, 4]] AS nested,
       array_dims(ARRAY[[1, 2], [3, 4]]) AS dims;

-- 二维数组乘 10(逐元素运算)
SELECT ARRAY[[1, 2], [3, 4]] * 10 AS scaled;
text
     nested     |   dims
----------------+----------
 {{1,2},{3,4}}  | [1:2][1:2]
(1 row)

     scaled
----------------
 {{10,20},{30,40}}
(1 row)

声明时可显式写出维度(同样不强制):

sql
CREATE TABLE t_grid_decl (
  id   BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  m1   INTEGER[],        -- 等价,推荐:不写维度
  m2   INTEGER[][]       -- 冗余写法,PostgreSQL 视为 INTEGER[]
);

-- 两者在系统目录中完全相同
SELECT column_name, data_type, udt_name
FROM information_schema.columns
WHERE table_name = 't_grid_decl'
ORDER BY ordinal_position;
text
 column_name | data_type | udt_name
-------------+-----------+----------
 id          | bigint    | int8
 m1          | ARRAY     | _int4
 m2          | ARRAY     | _int4
(2 rows)

查看数组长度与维度 ​

长度与维度查询函数一览:

函数返回示例输入返回结果
array_length(arr, dim)指定维度的长度array_length('{{1,2},{3,4}}', 1)2
cardinality(arr)元素总数(所有维度)cardinality('{{1,2},{3,4}}')4
array_dims(arr)维度范围文本array_dims('{{1,2},{3,4}}')[1:2][1:2]
array_ndims(arr)维度数量array_ndims('{{1,2},{3,4}}')2
array_lower(arr, dim)下界array_lower('{1,2,3}', 1)1
array_upper(arr, dim)上界array_upper('{1,2,3}', 1)3
sql
-- 长度与维度对比演示
SELECT
  array_length('{{1,2},{3,4}}', 1)  AS dim1_len,     -- 第一维长度
  array_length('{{1,2},{3,4}}', 2)  AS dim2_len,     -- 第二维长度
  cardinality('{{1,2},{3,4}}')      AS total_elems,  -- 元素总数
  array_dims('{{1,2},{3,4}}')       AS dims,         -- 维度范围
  array_ndims('{{1,2},{3,4}}')      AS ndims;        -- 维度数
text
 dim1_len | dim2_len | total_elems |     dims     | ndims
----------+----------+-------------+--------------+-------
        2 |        2 |           4 | [1:2][1:2]   |     2
(1 row)

array_length 与 cardinality 的关键区别:

sql
SELECT
  array_length('{}'::int[], 1)  AS len_empty,       -- NULL(空数组)
  cardinality('{}'::int[])      AS card_empty,      -- 0(明确表示零个)
  array_length('{1,2,3}', 2)    AS len_no_dim2,     -- NULL(维度不存在)
  cardinality('{1,2,3}')        AS card_1d;         -- 3
text
 len_empty | card_empty | len_no_dim2 | card_1d
-----------+------------+-------------+---------
           |          0 |             |       3
(1 row)

据此可以总结判空的最佳写法:

sql
-- 推荐:cardinality 对空数组返回 0,可用于比较运算
SELECT count(*) FROM events WHERE cardinality(tags) = 0;

-- 危险:array_length 返回 NULL,条件永不成立
SELECT count(*) FROM events WHERE array_length(tags, 1) = 0;   -- 恒为 0 行

-- 也可以直接与空数组比较
SELECT count(*) FROM events WHERE tags = '{}';

非 1 起始下标的数组(由切片产生)会让 array_length 与直觉不符:

sql
-- 切片后下标不再从 1 开始
SELECT
  ('{a,b,c,d,e}'::text[])[2:4]                    AS sliced,
  array_dims(('{a,b,c,d,e}'::text[])[2:4])        AS slice_dims,
  array_length(('{a,b,c,d,e}'::text[])[2:4], 1)   AS slice_len;
text
   sliced   | slice_dims | slice_len
------------+------------+-----------
 {b,c,d}    | [2:4]      |         3
(1 row)

实战示例 ​

场景:为内容平台设计文章表,用数组存储标签与封面图列表,并把已有数据迁移为聚合数组。

第 1 步:建表

sql
-- 文章表:标签多值、封面图有序列表
CREATE TABLE articles (
  id           BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  title        TEXT        NOT NULL,
  slug         TEXT        NOT NULL UNIQUE,
  tags         TEXT[]      NOT NULL DEFAULT '{}',
  cover_images TEXT[]      NOT NULL DEFAULT '{}',
  view_counts  INTEGER[]   NOT NULL DEFAULT '{}',   -- 近 N 日浏览趋势
  published_at DATE,
  CONSTRAINT chk_tags_len CHECK (cardinality(tags) <= 10)   -- 用 cardinality 限制数量
);

-- 限制标签数量的正确方式(array_length 遇到空数组返回 NULL 会绕过约束)
-- 不推荐:CHECK (array_length(tags, 1) <= 10)  -- 空数组时条件为 NULL,视为通过
text
CREATE TABLE

第 2 步:插入数据

sql
INSERT INTO articles (title, slug, tags, cover_images, view_counts, published_at) VALUES
  ('PostgreSQL 18 新特性一览', 'pg18-features', ARRAY['database', 'postgresql', 'release'],
   ARRAY['/img/pg18-1.png', '/img/pg18-2.png'], ARRAY[1200, 1350, 980], DATE '2026-01-10'),
  ('数组类型实战指南', 'array-guide', ARRAY['database', 'postgresql', 'tutorial'],
   ARRAY['/img/array-1.png'], ARRAY[520, 610, 720, 880], DATE '2026-02-05'),
  ('范围类型与排期系统', 'range-scheduling', ARRAY['database', 'tutorial'],
   ARRAY['/img/range-1.png', '/img/range-2.png'], ARRAY[300, 410], DATE '2026-03-01'),
  ('草稿:未发布内容', 'draft-post', '{}', '{}', '{}', NULL)
RETURNING id, title, cardinality(tags) AS tag_cnt, cardinality(cover_images) AS img_cnt;
text
 id |           title            | tag_cnt | img_cnt
----+----------------------------+---------+---------
  1 | PostgreSQL 18 新特性一览   |       3 |       2
  2 | 数组类型实战指南           |       3 |       1
  3 | 范围类型与排期系统         |       2 |       2
  4 | 草稿:未发布内容           |       0 |       0
(4 rows)

第 3 步:查询与统计

sql
-- 统计各标签出现次数(需要先展开数组)
SELECT tag, count(*) AS article_cnt
FROM articles, unnest(tags) AS tag
GROUP BY tag
ORDER BY article_cnt DESC, tag;
text
    tag      | article_cnt
-------------+-------------
 database    |           3
 tutorial    |           2
 postgresql  |           2
 release     |           1
(4 rows)
sql
-- 用数组长度做业务筛选举例
SELECT
  id,
  title,
  cardinality(tags)              AS tag_cnt,
  cardinality(view_counts)       AS days,
  view_counts[1]                 AS first_day_views    -- 下标从 1 开始
FROM articles
WHERE cardinality(view_counts) > 0
  AND published_at IS NOT NULL
ORDER BY cardinality(view_counts) DESC;
text
 id |          title           | tag_cnt | days | first_day_views
----+--------------------------+---------+------+-----------------
  2 | 数组类型实战指南         |       3 |    4 |             520
  1 | PostgreSQL 18 新特性一览 |       3 |    3 |            1200
  3 | 范围类型与排期系统       |       2 |    2 |             300
(3 rows)

第 4 步:从关系表迁移为数组

把"文章-标签"多对多关系聚合成数组:

sql
-- 模拟原有关系表
CREATE TABLE article_tags (
  article_id BIGINT NOT NULL,
  tag        TEXT   NOT NULL,
  position   INTEGER NOT NULL,           -- 保留顺序
  PRIMARY KEY (article_id, tag)
);

INSERT INTO article_tags VALUES
  (1, 'database', 1), (1, 'postgresql', 2), (1, 'release', 3),
  (2, 'database', 1), (2, 'postgresql', 2), (2, 'tutorial', 3);

-- 用 array_agg 聚合成数组,按 position 保序
SELECT
  a.id,
  a.title,
  array_agg(t.tag ORDER BY t.position) AS tags_ordered
FROM articles a
JOIN article_tags t ON t.article_id = a.id
GROUP BY a.id, a.title
ORDER BY a.id;
text
 id |          title           |        tags_ordered
----+--------------------------+-----------------------------
  1 | PostgreSQL 18 新特性一览 | {database,postgresql,release}
  2 | 数组类型实战指南         | {database,postgresql,tutorial}
(2 rows)

第 5 步:清理

sql
DROP TABLE article_tags;
DROP TABLE articles;
DROP TABLE events;
DROP TABLE t_array_syntax;
DROP TABLE t_element_types;
DROP TABLE t_matrix;
DROP TABLE t_grid_decl;

注意事项 ​

  • 下标从 1 开始,不是 0。这是与多数编程语言最大的差异。
sql
-- 从 1 开始
SELECT ('{a,b,c}'::text[])[1] AS first, ('{a,b,c}'::text[])[3] AS third;

-- 错误!下标 0 返回 NULL,不报错,最容易埋下静默 bug
SELECT ('{a,b,c}'::text[])[0] AS zeroth;   -- NULL
  • integer[3] 不限制长度。PostgreSQL 忽略声明中的维度长度,需要限制请用 CHECK (cardinality(col) <= 3)。
sql
-- 不推荐:以为 [3] 能限制长度
CREATE TABLE t_bad_limit (tags TEXT[3]);

-- 推荐:用 CHECK + cardinality
CREATE TABLE t_good_limit (
  tags TEXT[] NOT NULL DEFAULT '{}' CHECK (cardinality(tags) <= 3)
);
  • array_length 对空数组返回 NULL,与 cardinality 返回 0 不同。做长度比较时优先用 cardinality。
sql
-- 错误!空数组时条件为 NULL,CHECK 视为通过,约束失效
ALTER TABLE t_good_limit ADD CONSTRAINT chk_bad CHECK (array_length(tags, 1) <= 3);

-- 正确
ALTER TABLE t_good_limit ADD CONSTRAINT chk_good CHECK (cardinality(tags) <= 3);
  • 数组元素类型必须一致。混合类型会被隐式转换或直接报错。
sql
-- 报错:无法把文本转为整数
SELECT ARRAY[1, 2, 'abc'];

-- 可以:统一转为 text
SELECT ARRAY[1, 2, 'abc']::text[] AS mixed_as_text;
text
ERROR:  invalid input syntax for type integer: "abc"
  • 不能直接对数组的某个元素建索引。CREATE INDEX ON t ((arr[1])) 是表达式索引,不是"元素索引";数组的集合查询请用 GIN 索引。
sql
-- 不推荐:对单元素建普通 btree 索引,无法支撑 @> 查询
CREATE INDEX idx_bad ON events ((tags[1]));

-- 推荐:数组集合查询用 GIN
CREATE INDEX idx_events_tags_gin ON events USING gin (tags);
  • 数组不适合存大量元素。元素过多会撑大行宽,触发 TOAST 压缩,且更新任一元素都要重写整行。参见 TOAST 机制。元素数超过几十个时,应改用关系表。
  • 默认值不要用 NULL 当"空数组"。语义混乱且需要到处判空,统一用 DEFAULT '{}'。
sql
-- 不推荐
tags TEXT[]

-- 推荐
tags TEXT[] NOT NULL DEFAULT '{}'
  • 文本数组字面量含空格或逗号必须加双引号,否则会解析失败或元素被拆错。
sql
-- 错误!空格导致解析失败
SELECT '{hello world}'::text[];

-- 正确
SELECT '{"hello world"}'::text[];
-- 更推荐
SELECT ARRAY['hello world'];
  • unnest 在多数组并列使用时按位置配对(PostgreSQL 9.4+ 起对 FROM 中的多个 unnest 采用 ROWS FROM 语义),长度不同时短的一方补 NULL:
sql
-- 两个数组按位置配对展开
SELECT * FROM unnest(ARRAY['a','b','c'], ARRAY[1,2]) AS t(letter, num);
text
 letter | num
--------+-----
 a      |   1
 b      |   2
 c      |
(3 rows)

最佳实践 ​

  • 优先考虑"真的需要数组吗"。数组适合:元素数量少(一般不超过几十个)、不需要独立查询/更新单个元素、不需要外键引用元素的场景。否则用关系表。
  • 数组列一律 NOT NULL DEFAULT '{}',把"没有值"统一为"空数组",查询代码不必再判 NULL。
  • 用 cardinality 而非 array_length 做长度判断与约束,避免空数组返回 NULL 造成约束失效。
  • 限制数组元素数量,用 CHECK (cardinality(col) <= N),防止单行无限膨胀。
sql
CREATE TABLE products (
  id       BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  sku      TEXT   NOT NULL UNIQUE,
  tags     TEXT[] NOT NULL DEFAULT '{}' CHECK (cardinality(tags) <= 10),
  image_urls TEXT[] NOT NULL DEFAULT '{}' CHECK (cardinality(image_urls) <= 8)
);
  • 需要"标签去重"时在写入前处理。数组不去重,{a,a,b} 是合法值。
sql
-- 写入前在应用层去重,或用 SQL 归一化
INSERT INTO products (sku, tags)
VALUES ('SKU-1001', ARRAY(SELECT DISTINCT unnest(ARRAY['a', 'a', 'b'])))
RETURNING sku, tags;
  • 给需要集合查询的数组列建 GIN 索引,但要注意 GIN 会带来写放大,写入密集的表需权衡。
sql
CREATE INDEX idx_products_tags_gin ON products USING gin (tags);

-- 验证索引被使用
EXPLAIN (ANALYZE, COSTS OFF)
SELECT * FROM products WHERE tags @> ARRAY['database'];
  • 需要保持元素顺序时显式排序。array_agg 不保证顺序,必须写 ORDER BY。
sql
-- 推荐:显式保序
SELECT array_agg(tag ORDER BY position) FROM article_tags WHERE article_id = 1;

-- 不推荐:顺序随机
SELECT array_agg(tag) FROM article_tags WHERE article_id = 1;
  • 迁移关系表到数组前先评估查询模式。若未来仍需"按标签反查、按标签分页、按标签统计",关系表 + 索引往往比数组更灵活。
  • 展示给用户前用 array_to_string 拼接,避免直接把 {a,b,c} 这种内部格式暴露到界面。
sql
SELECT title, array_to_string(tags, ' / ') AS tag_text FROM articles ORDER BY id;

下一节

继续学习:数组访问与修改

参考链接 ​