目录

pgsql-13 数组与范围类型

1. 概述

数组和范围类型是 PostgreSQL 相对 MySQL 的独特优势。它们让某些原本需要额外关联表或复杂应用逻辑的场景,用一个字段就能优雅解决。

对比 MySQL:MySQL 没有原生数组类型(只能用 JSON 数组或逗号分隔字符串模拟,无法高效索引/查询),也没有范围类型。这两个类型是 PG 数据建模能力更强的体现。

2. 数组类型

2.1 定义与写入

CREATE TABLE articles (
    id      SERIAL PRIMARY KEY,
    title   TEXT,
    tags    TEXT[],           -- 一维文本数组
    ratings INTEGER[],        -- 整数数组
    matrix  INTEGER[][]       -- 多维数组
);

-- 两种字面量写法
INSERT INTO articles (title, tags, ratings) VALUES
    ('PostgreSQL 教程', ARRAY['database','postgresql'], ARRAY[5,4,5]),
    ('SQL 指南',        '{"sql","tutorial"}',           '{4,5,4}');

2.2 访问与操作(注意:下标从 1 开始)

SELECT tags[1] FROM articles;              -- 第一个元素(PG数组下标从1开始!)
SELECT tags[1:2] FROM articles;            -- 切片
SELECT array_length(tags, 1) FROM articles; -- 第1维长度
SELECT cardinality(tags) FROM articles;    -- 元素总数

-- 修改
UPDATE articles SET tags = array_append(tags, 'new');   -- 追加
UPDATE articles SET tags = array_remove(tags, 'sql');   -- 删除元素
UPDATE articles SET tags = tags || ARRAY['a','b'];      -- 拼接

2.3 包含/查询操作符

SELECT * FROM articles WHERE 'postgresql' = ANY(tags);   -- 含某元素
SELECT * FROM articles WHERE tags @> ARRAY['sql'];       -- 包含(数组含子集)
SELECT * FROM articles WHERE tags && ARRAY['sql','go'];  -- 有交集(任一匹配)
SELECT * FROM articles WHERE 5 = ALL(ratings);           -- 全部满足

2.4 展开与聚合

-- unnest:数组转行
SELECT id, unnest(tags) AS tag FROM articles;

-- array_agg:行转数组(分组聚合)
SELECT array_agg(title) FROM articles;
SELECT room_id, array_agg(id) FROM bookings GROUP BY room_id;

2.5 数组索引(GIN)

-- 让包含查询(@>, &&, =ANY)走索引
CREATE INDEX idx_articles_tags ON articles USING GIN (tags);
SELECT * FROM articles WHERE tags @> ARRAY['postgresql'];  -- 命中索引

这正是数组相对"逗号分隔字符串"的关键优势:可以用 GIN 索引高效查询"包含某标签",而字符串 LIKE '%tag%' 既慢又不准。

3. 范围类型

范围类型表示一个连续区间,内置几种,也可自定义。

类型 元素
int4range / int8range 整数范围
numrange 数值范围
tsrange / tstzrange 时间戳范围(后者带时区)
daterange 日期范围

3.1 边界表示

-- 方括号=闭区间(含端点),圆括号=开区间(不含端点)
SELECT int4range(1, 10, '[]');    -- 1到10,两端都含
SELECT daterange('2026-07-01', '2026-08-01', '[)'); -- 含7-1不含8-1(半开,最常用)
SELECT numrange(1.0, NULL);       -- 无上界(1到正无穷)

3.2 范围操作符

SELECT int4range(1,10) @> 5;                      -- 包含某值 → true
SELECT int4range(1,10) @> int4range(2,5);         -- 包含子范围
SELECT int4range(1,10) && int4range(5,15);        -- 是否重叠(&&) → true
SELECT int4range(1,5) -|- int4range(5,10);        -- 是否相邻 → true
SELECT lower(int4range(1,10)), upper(int4range(1,10)); -- 取下/上界

3.3 实战:会议室预订防重叠(杀手锏)

范围类型 + GiST 排他约束,在数据库层原子地防止时间段重叠——这是 MySQL 无法用约束实现的:

CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE room_bookings (
    id      SERIAL PRIMARY KEY,
    room_id INTEGER,
    during  TSRANGE,
    -- 同一房间(room_id相等)、时间段重叠(&&) 的两条记录不允许共存
    EXCLUDE USING GiST (room_id WITH =, during WITH &&)
);

INSERT INTO room_bookings (room_id, during)
    VALUES (101, tsrange('2026-07-01 09:00', '2026-07-01 10:00'));   -- OK
INSERT INTO room_bookings (room_id, during)
    VALUES (101, tsrange('2026-07-01 09:30', '2026-07-01 11:00'));   -- 报错:与上一条重叠

若不用范围类型,只能用两列 start_time/end_time 存,且无法用约束防重叠,得在应用层查询+加锁判断(有并发漏洞)。范围类型 + 排他约束是 PG 数据建模的经典优势。约束见 约束详解

3.4 范围查询与索引

-- GiST 索引加速范围重叠/包含查询
CREATE INDEX idx_bookings_during ON room_bookings USING GiST (during);

SELECT * FROM room_bookings WHERE during @> now();   -- 当前正在使用中的预订
SELECT * FROM room_bookings
WHERE during && tsrange('2026-07-01', '2026-07-02');  -- 某天有预订的

4. 使用建议

  • 数组适合元素数量不多、无需单独维护属性的场景(标签、权限列表);若元素本身是复杂实体、需增删改查/关联,仍应用关联表。
  • 范围类型 + 排他约束是"防重叠"类需求(预订、排班、价格生效期)的最优解。
  • 两者的包含/重叠查询记得配 GIN(数组)/ GiST(范围)索引。

5. 面试高频问答

Q1:PostgreSQL 的数组相比 MySQL 用逗号分隔字符串有什么优势? 数组是原生类型,支持下标访问、包含/交集操作符,且能用 GIN 索引高效查询"包含某元素";逗号分隔字符串只能 LIKE 模糊匹配,慢且不准。

Q2:数组下标从几开始? 从 1 开始(不是 0),这是 PG 数组的常见坑。

Q3:范围类型能解决什么经典问题? 表示连续区间(时间、数值),配合 GiST 排他约束可在数据库层原子地防止重叠(如会议室预订、排班不冲突),这是 MySQL 用约束做不到的。

Q4:数组和范围类型分别用什么索引? 数组用 GIN(加速 @>、&&、=ANY 等包含查询);范围用 GiST(加速 @>、&& 等重叠/包含查询)。

Q5:什么时候该用数组,什么时候该用关联表? 元素少、只作标记且不需单独增删改查用数组;元素是有属性的实体、需关联和维护时用关联表。

6. 下一步

学习 全文搜索,掌握文本检索能力。