文章目录
-
- 一、数组基础
-
- [1.1 创建含数组的表](#1.1 创建含数组的表)
- [1.2 插入数组数据](#1.2 插入数组数据)
- [1.3 性能对比与选择建议](#1.3 性能对比与选择建议)
- [1.4 使用建议](#1.4 使用建议)
- 二、数组查询(SELECT)
-
- [2.1 基本访问](#2.1 基本访问)
- [2.2 条件查询:判断元素是否存在](#2.2 条件查询:判断元素是否存在)
- [2.3 数组长度与空值判断](#2.3 数组长度与空值判断)
- [2.4 展开数组为行(UNNEST)](#2.4 展开数组为行(UNNEST))
- 三、数组更新(UPDATE)
-
- [3.1 替换整个数组](#3.1 替换整个数组)
- [3.2 追加元素](#3.2 追加元素)
- [3.3 删除指定元素](#3.3 删除指定元素)
- [3.4 更新特定位置元素](#3.4 更新特定位置元素)
- 四、数组索引优化
-
- [4.1 GIN 索引(通用倒排索引)](#4.1 GIN 索引(通用倒排索引))
- [4.2 GiST 索引(空间/近似匹配)](#4.2 GiST 索引(空间/近似匹配))
- [4.3 表达式索引(针对特定模式)](#4.3 表达式索引(针对特定模式))
- [4.4 部分索引(过滤空数组)](#4.4 部分索引(过滤空数组))
- 五、数组常见应用场景
- 六、避坑指南与限制
-
- [6.1 数组 vs 关联表:何时使用?](#6.1 数组 vs 关联表:何时使用?)
- [6.2 空数组 vs NULL](#6.2 空数组 vs NULL)
- [6.3 下标从1开始](#6.3 下标从1开始)
- [6.4 写入性能](#6.4 写入性能)
- [6.5 JSONB 替代方案](#6.5 JSONB 替代方案)
- 七、高级技巧
-
- [7.1 数组合并去重](#7.1 数组合并去重)
- [7.2 数组转字符串(用于展示)](#7.2 数组转字符串(用于展示))
- [7.3 初始化空数组](#7.3 初始化空数组)
在 PostgreSQL 中,数组(Array)是一种强大而灵活的内置数据类型,允许单个字段存储多个同类型元素。它广泛应用于标签系统、权限列表、多值属性等场景。然而,不当使用可能导致性能下降或查询复杂化。
一、数组基础
1.1 创建含数组的表
sql
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT,
tags TEXT[], -- 文本数组
scores INTEGER[], -- 整数数组
dimensions FLOAT[3] -- 固定长度为3的浮点数组(可选)
);
注意:
TEXT[]表示一维文本数组;TEXT[][]为二维数组。固定长度(如[3])仅为文档提示,PostgreSQL 不强制限制实际长度。
1.2 插入数组数据
sql
-- 方式1:使用花括号字面量(推荐)
INSERT INTO products (name, tags, scores)
VALUES ('Laptop', '{electronics, portable, high-end}', '{95, 88, 92}');
-- 方式2:使用 ARRAY 构造器
INSERT INTO products (name, tags, scores)
VALUES ('Phone', ARRAY['mobile', 'android'], ARRAY[85, 90]);
字符串元素若含逗号、空格或特殊字符,需用双引号包裹:
sql'{ "wireless, bluetooth", "water-resistant" }'
1.3 性能对比与选择建议
| 查询模式 | 推荐操作符 | 是否走 GIN 索引 |
|---|---|---|
| 元素存在(单值) | 'val' = ANY(col) |
是 |
| 包含子集 | col @> ARRAY[...] |
是 |
| 交集非空 | col && ARRAY[...] |
是 |
| 精确相等 | col = ARRAY[...] |
否(需 B-tree 索引) |
| 长度判断 | array_length(col,1) = N |
否(需表达式索引) |
注意:
col = ARRAY[...]无法使用 GIN 索引,若需频繁精确匹配,可考虑:
sqlCREATE INDEX idx_tags_exact ON products (tags); -- B-tree 索引
1.4 使用建议
- 优先使用 GIN 索引 加速
ANY,@>,&&查询。 - 单元素存在性检查用
= ANY(col),多元素用@>。 - 追加用
||,删除用array_remove()。 - 明确区分空数组
{}和 NULL。 - 避免在高写入场景滥用数组,评估关联表方案。
- 简单同类型列表用数组,复杂结构用 JSONB。
- 下标从1开始,切片语法
[start:end]。 - 用
unnest()展开分析,用array_agg()聚合生成。
通过合理设计与索引优化,PostgreSQL 数组能显著简化数据模型并提升查询效率。但在关系型数据库中,规范化仍是默认准则------仅在明确收益时才引入数组。
二、数组查询(SELECT)
2.1 基本访问
sql
-- 获取整个数组
SELECT tags FROM products;
-- 访问第1个元素(PostgreSQL 数组下标从1开始!)
SELECT tags[1] FROM products;
-- 访问切片(第2到第3个元素)
SELECT scores[2:3] FROM products;
2.2 条件查询:判断元素是否存在
sql
-- 方法1:使用 ANY()
SELECT * FROM products WHERE 'electronics' = ANY(tags);
-- 方法2:使用 @> 操作符(包含)
SELECT * FROM products WHERE tags @> ARRAY['electronics'];
-- 方法3:使用 && 操作符(重叠,至少一个公共元素)
SELECT * FROM products WHERE tags && ARRAY['electronics', 'gaming'];
推荐:
- 单元素存在性检查 →
= ANY(array_col)- 多元素子集检查 →
array_col @> ARRAY[...]
2.3 数组长度与空值判断
sql
-- 数组长度
SELECT array_length(tags, 1) AS tag_count FROM products;
-- 判断是否为空数组
SELECT * FROM products WHERE tags = '{}';
-- 判断是否为 NULL(注意:空数组 ≠ NULL)
SELECT * FROM products WHERE tags IS NULL;
2.4 展开数组为行(UNNEST)
sql
-- 将每个标签展开为独立行
SELECT id, name, unnest(tags) AS tag
FROM products;
-- 保留空数组(使用 LEFT JOIN LATERAL)
SELECT p.id, p.name, t.tag
FROM products p
LEFT JOIN LATERAL unnest(p.tags) AS t(tag) ON true;
三、数组更新(UPDATE)
3.1 替换整个数组
sql
UPDATE products
SET tags = '{"updated", "tag"}'
WHERE id = 1;
3.2 追加元素
sql
-- 追加到末尾(使用 || 操作符)
UPDATE products
SET tags = tags || 'new_tag'
WHERE id = 1;
-- 追加数组
UPDATE products
SET tags = tags || ARRAY['tag1', 'tag2']
WHERE id = 1;
注意:
||对数组是连接操作,对字符串是拼接,上下文自动区分。
3.3 删除指定元素
PostgreSQL 无直接删除函数,需通过数组构造实现:
sql
-- 删除所有值为 'old_tag' 的元素
UPDATE products
SET tags = array_remove(tags, 'old_tag')
WHERE id = 1;
-- 删除第一个匹配的元素(PostgreSQL 13+)
UPDATE products
SET tags = array_replace(tags, 'old_tag', NULL)
WHERE id = 1;
-- 然后移除 NULL(需额外步骤,不推荐)
推荐:使用
array_remove()(移除所有匹配项)。
3.4 更新特定位置元素
sql
-- 将第2个标签改为 'replaced'
UPDATE products
SET tags[2] = 'replaced'
WHERE id = 1 AND array_length(tags, 1) >= 2;
注意:若下标超出范围,会插入 NULL 填充,需谨慎。
四、数组索引优化
数组查询若无索引,将导致全表扫描。以下是关键优化策略。
4.1 GIN 索引(通用倒排索引)
适用于 ANY(), @>, && 等操作。
sql
-- 创建 GIN 索引
CREATE INDEX idx_products_tags_gin ON products USING GIN (tags);
-- 查询可走索引
SELECT * FROM products WHERE tags @> ARRAY['electronics'];
SELECT * FROM products WHERE 'gaming' = ANY(tags);
优势:支持任意元素查询、子集、交集。
劣势:写入开销较大,索引体积大。
4.2 GiST 索引(空间/近似匹配)
较少用于普通数组,多用于几何、全文搜索。
4.3 表达式索引(针对特定模式)
若常查询数组长度:
sql
CREATE INDEX idx_products_tag_count ON products ((array_length(tags, 1)));
4.4 部分索引(过滤空数组)
sql
CREATE INDEX idx_products_nonempty_tags ON products USING GIN (tags)
WHERE tags != '{}' AND tags IS NOT NULL;
五、数组常见应用场景
场景1:商品标签系统
sql
-- 快速查找带"sale"和"electronics"标签的商品
SELECT * FROM products
WHERE tags @> ARRAY['sale', 'electronics'];
-- GIN 索引高效支持
场景2:用户角色权限
sql
-- 用户拥有 admin 或 moderator 角色
SELECT * FROM users
WHERE roles && ARRAY['admin', 'moderator'];
场景3:多值过滤(前端筛选)
sql
-- 用户选择颜色 ['red', 'blue'],查找包含任一颜色的商品
SELECT * FROM items
WHERE colors && ARRAY['red', 'blue'];
六、避坑指南与限制
6.1 数组 vs 关联表:何时使用?
- 使用数组当 :
- 数据简单、无独立属性
- 查询模式固定(如仅存在性检查)
- 写少读多
- 使用关联表当 :
- 需要独立管理元素(如标签有创建时间、状态)
- 元素需被多个主记录共享
- 需要外键约束或复杂查询
反例:若标签需统计使用次数、支持层级,应建
tags表 +product_tags关联表。
6.2 空数组 vs NULL
-
{}是空数组(有效值) -
NULL表示未知/未设置 -
查询时需明确区分:
sqlWHERE tags IS NOT NULL AND tags != '{}'
6.3 下标从1开始
这是 SQL 标准,但易与编程语言(0起始)混淆:
sql
tags[1] -- 第一个元素
6.4 写入性能
GIN 索引在高并发写入场景下可能成为瓶颈。若写多读少,评估是否值得用数组。
6.5 JSONB 替代方案
对于结构更复杂或多类型的多值字段,可考虑 JSONB:
sql
ALTER TABLE products ADD COLUMN metadata JSONB;
-- 支持更灵活查询,且有专用 GIN 索引
但若仅为同类型列表,数组更轻量、高效。
七、高级技巧
7.1 数组合并去重
sql
-- 合并两个数组并去重
SELECT ARRAY(
SELECT DISTINCT UNNEST(tags || ARRAY['new', 'tag'])
ORDER BY 1
);
7.2 数组转字符串(用于展示)
sql
SELECT array_to_string(tags, ', ') AS tag_list
FROM products;
7.3 初始化空数组
sql
-- 避免 NULL,初始化为空数组
INSERT INTO products (name, tags)
VALUES ('New Product', '{}');