除了整数、文本、时间戳这些基础类型,PostgreSQL 还内置了一组表达能力极强的高级类型:数组能在一列里存多值,范围类型能自然表达区间与重叠,复合类型让一行可以嵌套另一行,枚举约束取值集合,hstore 提供轻量键值存储。这些类型如果用对,可以显著简化数据模型、减少关联表;如果用错,则会带来索引失效、查询难写、迁移困难等一系列问题。本文逐一拆解它们的语义、索引支持与适用边界。
核心认知:高级类型不是「炫技」,而是用更贴近领域的方式表达数据。判断标准只有一个——它能否被索引、被约束、被清晰查询。
一、数组类型
1.1 定义与构造
CREATE TABLE articles (
id serial PRIMARY KEY,
title text,
tags text[] -- 文本数组
);
-- 字面量构造
INSERT INTO articles (title, tags)
VALUES ('PostgreSQL 入门', ARRAY['postgres', 'database', 'sql']);
-- 等价的花括号写法
INSERT INTO articles (title, tags)
VALUES ('索引原理', '{"postgres","index","btree"}');
-- 从查询构造
INSERT INTO articles (title, tags)
SELECT '汇总', array_agg(t) FROM unnest(ARRAY['a','b','c']) t;
1.2 下标与切片
PostgreSQL 数组下标从 1 开始(不是 0):
SELECT tags[1] FROM articles; -- 第一个元素
SELECT tags[1:2] FROM articles; -- 切片,返回子数组
SELECT array_length(tags, 1) FROM articles; -- 长度
SELECT cardinality(tags) FROM articles; -- 元素总数
1.3 数组查询操作符
| 操作符 | 含义 | 示例 |
|---|---|---|
@> | 包含 | tags @> ARRAY['sql'] |
<@ | 被包含 | ARRAY['sql'] <@ tags |
&& | 有交集 | tags && ARRAY['sql','nosql'] |
= | 相等 | tags = ARRAY['a','b'] |
|| | 拼接 | tags || ARRAY['new'] |
-- 查找包含 sql 标签的文章
SELECT title FROM articles WHERE tags @> ARRAY['sql'];
-- 查找含任一标签的文章
SELECT title FROM articles WHERE tags && ARRAY['index', 'btree'];
-- 展开数组为多行
SELECT id, unnest(tags) AS tag FROM articles;
-- 在数组中查找某值的位置
SELECT array_position(tags, 'sql') FROM articles;
1.4 数组索引:GIN 是关键
普通 B-tree 索引对数组只能做整体比较,无法加速 @>。必须用 GIN:
CREATE INDEX idx_articles_tags ON articles USING gin (tags);
-- 现在 @> 与 && 都能走索引
EXPLAIN (ANALYZE)
SELECT title FROM articles WHERE tags @> ARRAY['sql'];
1.5 数组 vs 关联表
| 维度 | 数组列 | 关联表 |
|---|---|---|
| 查询单值 | GIN 索引可加速 | 天然索引 |
| 元素约束 | 无外键 | 有外键 |
| 元素元数据 | 无法附加 | 可加列 |
| 顺序 | 保留 | 需排序列 |
| 更新单个元素 | 需重写整行 | 精确更新 |
| 适用 | 标签、小集合、只读多值 | 有元数据、需引用完整性 |
经验法则:元素是简单标量、集合小、几乎不单独更新,用数组;元素是实体、需要外键或元数据,用关联表。
1.6 数组的坑
-- 坑 1:数组是可变长度,无法声明「最多 5 个」
-- 需要约束时用 CHECK
ALTER TABLE articles ADD CONSTRAINT tags_max5 CHECK (cardinality(tags) <= 5);
-- 坑 2:NULL 元素与 NULL 数组不同
SELECT ARRAY[1, NULL, 3]; -- 含 NULL 元素
SELECT NULL::int[]; -- 整个数组为 NULL
-- 坑 3:多维数组是矩形的,不能锯齿
SELECT ARRAY[[1,2],[3,4]]; -- 合法,2x2
-- SELECT ARRAY[[1,2],[3]]; -- 非法,维度不一致
二、范围类型
2.1 内置范围类型
int4range 整数区间
int8range 大整数区间
numrange 数值区间
tsrange 无时区时间戳区间
tstzrange 带时区时间戳区间
daterange 日期区间
2.2 构造与语义
CREATE TABLE room_bookings (
id serial PRIMARY KEY,
room text,
period tstzrange,
EXCLUDE USING gist (room WITH =, period WITH &&) -- 同房间时间不可重叠
);
-- 左闭右开区间
INSERT INTO room_bookings (room, period)
VALUES ('A101', '[2026-10-01 09:00, 2026-10-01 11:00)');
-- 尝试插入重叠区间 → 被排他约束拒绝
INSERT INTO room_bookings (room, period)
VALUES ('A101', '[2026-10-01 10:00, 2026-10-01 12:00)');
-- ERROR: conflicting key value violates exclusion constraint
2.3 范围操作符
| 操作符 | 含义 |
|---|---|
&& | 重叠 |
@> | 包含元素或子范围 |
<@ | 被包含 |
| `- | -` |
<< | 严格在左侧 |
>> | 严格在右侧 |
-- 查找某时刻被占用的房间
SELECT room FROM room_bookings
WHERE period @> '2026-10-01 10:30'::timestamptz;
-- 查找与给定区间重叠的预订
SELECT room FROM room_bookings
WHERE period && '[2026-10-01 10:00, 2026-10-01 11:00)'::tstzrange;
2.4 排他约束是范围的杀手锏
EXCLUDE 约束让「同一资源在时间上不重叠」这类业务规则由数据库强制保证,而不必依赖应用层检查:
-- 复合排他:同房间 + 同类型,时间不可重叠
ALTER TABLE room_bookings ADD CONSTRAINT no_overlap
EXCLUDE USING gist (room WITH =, period WITH &&);
2.5 范围索引
-- 排他约束会自动创建 GiST 索引
-- 若只需查询加速,也可单独建
CREATE INDEX idx_bookings_period ON room_bookings USING gist (period);
三、复合类型
3.1 定义与使用
CREATE TYPE address AS (
street text,
city text,
zip text,
country text DEFAULT 'CN'
);
CREATE TABLE customers (
id serial PRIMARY KEY,
name text,
addr address
);
INSERT INTO customers (name, addr)
VALUES ('Acme', ROW('中山路 1 号', '北京', '100000', 'CN')::address);
3.2 访问字段
-- 用点号访问字段,必须加括号
SELECT (addr).city, (addr).zip FROM customers;
-- 展开为多列
SELECT id, name, (addr).* FROM customers;
-- 按字段过滤
SELECT name FROM customers WHERE (addr).city = '北京';
注意:
(addr).city的括号是必须的。写成addr.city会被解析为「表 addr 的列 city」,报错。
3.3 复合类型的适用场景
- 强类型的地理坐标、金额、时间区间
- 函数返回多值(避免建临时表)
- 与 PL/pgSQL 配合封装业务对象
3.4 复合类型的限制
-- 复合类型字段无法单独建索引(只能整体或表达式索引)
CREATE INDEX idx_customers_city ON customers (((addr).city));
-- 复合类型无表级约束(外键等)
-- 复合类型不适合频繁单字段更新
3.5 表行类型也是复合类型
-- 任意表的行类型都可以当作复合类型使用
SELECT row_to_json(c) FROM customers c;
SELECT (c).name FROM customers c;
四、枚举类型
4.1 定义与使用
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'delivered', 'cancelled');
CREATE TABLE orders (
id serial PRIMARY KEY,
status order_status NOT NULL DEFAULT 'pending'
);
INSERT INTO orders (status) VALUES ('paid');
-- INSERT INTO orders (status) VALUES ('unknown'); -- 报错,非法值
4.2 排序语义
枚举的排序按定义顺序,不是字母序:
SELECT * FROM orders ORDER BY status;
-- pending < paid < shipped < delivered < cancelled
这既是优点(业务顺序即定义顺序),也是陷阱(改变定义顺序需重建类型)。
4.3 枚举的演进
-- 添加新值(可指定位置)
ALTER TYPE order_status ADD VALUE 'refunded' BEFORE 'cancelled';
-- 注意:ALTER TYPE ADD VALUE 在旧版本不能在事务内使用
-- 且不能删除枚举值
4.4 枚举 vs 检查约束 vs 查找表
| 方案 | 取值约束 | 可加元数据 | 修改成本 | 存储 |
|---|---|---|---|---|
| enum | 强 | 否 | 高(不能删值) | 4 字节 |
| CHECK | 中 | 否 | 中 | 原类型 |
| 查找表 + 外键 | 强 | 是 | 低 | 外键 |
建议:取值集合稳定且不需要元数据时用 enum;需要显示名、排序权重、启用开关等元数据时,用查找表。
4.5 枚举的坑
-- 坑:枚举值删除不了,只能重建类型
-- 坑:枚举与文本比较需显式转换
SELECT * FROM orders WHERE status = 'paid'; -- 合法,字面量自动转
SELECT * FROM orders WHERE status::text = 'paid'; -- 也可,但会失去索引
五、hstore 键值存储
5.1 启用与使用
CREATE EXTENSION hstore;
CREATE TABLE products (
id serial PRIMARY KEY,
name text,
attrs hstore
);
INSERT INTO products (name, attrs)
VALUES ('笔记本', 'brand=>ThinkPad, ram=>16GB, cpu=>i7');
5.2 查询操作
-- 取键
SELECT attrs -> 'brand' FROM products;
-- 判断是否含键
SELECT name FROM products WHERE attrs ? 'ram';
-- 判断键值对
SELECT name FROM products WHERE attrs @> 'brand=>ThinkPad';
-- 展开为行
SELECT id, (each(attrs)).key, (each(attrs)).value FROM products;
-- 键列表
SELECT akeys(attrs) FROM products;
5.3 hstore 索引
-- GIN 索引支持 ?、@> 等操作符
CREATE INDEX idx_products_attrs ON products USING gin (attrs);
-- 单键查询也可用表达式索引
CREATE INDEX idx_products_brand ON products ((attrs -> 'brand'));
5.4 hstore vs jsonb
| 维度 | hstore | jsonb |
|---|---|---|
| 值类型 | 仅文本 | 任意 JSON |
| 嵌套 | 不支持 | 支持 |
| 索引 | GIN(无路径操作符类) | GIN(jsonb_path_ops) |
| 大小 | 更小 | 稍大 |
| 维护状态 | 稳定但发展缓慢 | 主力方向 |
结论:新项目一律优先 jsonb;只有当值全是简单字符串、对体积极度敏感、且已有 hstore 存量时才用 hstore。
5.5 hstore 的坑
-- 坑:hstore 的值只能是文本,数字需显式转换
SELECT (attrs -> 'ram')::text FROM products; -- 返回 '16GB'
-- 坑:键不存在时返回 NULL,不会报错
SELECT attrs -> 'nonexistent' FROM products; -- NULL
六、选型与性能对比
6.1 各类型的索引支持
| 类型 | 默认索引 | 推荐索引 | 支持操作符 |
|---|---|---|---|
| 数组 | B-tree(整体) | GIN | @>、&& |
| 范围 | B-tree(整体) | GiST | &&、@> |
| 复合 | 表达式索引 | B-tree 表达式 | 字段等值 |
| 枚举 | B-tree | B-tree | =、排序 |
| hstore | B-tree(整体) | GIN | ?、@> |
6.2 存储开销
-- 查看列的平均宽度
SELECT attname, avg_width
FROM pg_stats
WHERE tablename = 'articles' AND attname = 'tags';
数组、hstore 都会带来变长存储与 TOAST 溢写。当单行超过约 2KB,PostgreSQL 会压缩并可能外存到 TOAST 表,读取时需额外 IO。
6.3 查询写法对照
-- 数组包含
SELECT * FROM articles WHERE tags @> ARRAY['sql'];
-- 范围重叠
SELECT * FROM room_bookings WHERE period && '[2026-10-01, 2026-10-02)'::tstzrange;
-- 复合字段
SELECT * FROM customers WHERE (addr).city = '北京';
-- 枚举过滤
SELECT * FROM orders WHERE status = 'paid';
-- hstore 键存在
SELECT * FROM products WHERE attrs ? 'ram';
6.4 迁移与兼容
-- 数组转关联表(用 unnest 展开)
CREATE TABLE article_tags AS
SELECT id AS article_id, unnest(tags) AS tag FROM articles;
-- hstore 转 jsonb
ALTER TABLE products
ALTER COLUMN attrs TYPE jsonb USING hstore_to_jsonb(attrs);
常见问题(FAQ)
数组和 jsonb 数组的选型
如果元素是同类标量、需要 GIN 索引与 @> 查询,用原生数组,它更紧凑、类型更强。如果元素是异构对象、需要嵌套与路径查询,用 jsonb。原生数组能做的 jsonb 都能做,但反过来不成立。
范围类型能否存空区间
可以。empty 是一个特殊范围值,表示不含任何元素的区间,'empty'::int4range。它与任何范围都不重叠,常用于表示「无有效期」这类语义。
枚举能否删除某个值
不能。PostgreSQL 不支持 ALTER TYPE ... DROP VALUE。如果确实需要删除,只能新建类型、转换列、删除旧类型,这在生产上代价不小。因此枚举应只用于真正稳定的取值集合。
复合类型字段能否建索引
可以,但必须用表达式索引,如 CREATE INDEX ON t (((col).field))。复合类型本身只能整体索引,单独字段索引需要提取表达式。
hstore 和 jsonb 的查询性能对比
对于简单的「键存在」查询,hstore 的 GIN 索引通常略快且更小,因为它结构更简单。但对于嵌套、数组、数值比较等复杂查询,jsonb 完胜且功能更全。除非有明确理由,新项目选 jsonb。
相关阅读
- PostgreSQL 表结构设计 — 范式与反范式、类型选型原则
- PostgreSQL JSONB 性能与索引 — jsonb 与 hstore 的对比与索引
- PostgreSQL 高级 SQL 技巧 — 数组展开、范围运算与窗口函数
- PostgreSQL 索引类型全解 — GIN、GiST 与表达式索引
- PostgreSQL UPSERT 与冲突处理 — 数组元素的合并与冲突处理
- PostgreSQL 专题导航
延伸阅读
- PostgreSQL PL/pgSQL 函数开发 — 复合类型与数组在函数中的传递
- PostgreSQL 迁移指南 — 类型变更与在线迁移策略
完整示例(一键复制)
-- ========== 1. 数组类型 ==========
CREATE TABLE articles (
id serial PRIMARY KEY,
title text,
tags text[]
);
CREATE INDEX idx_articles_tags ON articles USING gin (tags);
INSERT INTO articles (title, tags) VALUES
('PostgreSQL 入门', ARRAY['postgres', 'database', 'sql']),
('索引原理', '{"postgres","index","btree"}');
SELECT title FROM articles WHERE tags @> ARRAY['sql'];
SELECT title FROM articles WHERE tags && ARRAY['index', 'btree'];
SELECT id, unnest(tags) AS tag FROM articles;
-- ========== 2. 范围类型 + 排他约束 ==========
CREATE TABLE room_bookings (
id serial PRIMARY KEY,
room text,
period tstzrange,
EXCLUDE USING gist (room WITH =, period WITH &&)
);
INSERT INTO room_bookings (room, period)
VALUES ('A101', '[2026-10-01 09:00, 2026-10-01 11:00)');
SELECT room FROM room_bookings
WHERE period @> '2026-10-01 10:30'::timestamptz;
-- ========== 3. 复合类型 ==========
CREATE TYPE address AS (
street text,
city text,
zip text,
country text DEFAULT 'CN'
);
CREATE TABLE customers (
id serial PRIMARY KEY,
name text,
addr address
);
INSERT INTO customers (name, addr)
VALUES ('Acme', ROW('中山路 1 号', '北京', '100000', 'CN')::address);
SELECT name, (addr).city, (addr).zip FROM customers;
CREATE INDEX idx_customers_city ON customers (((addr).city));
-- ========== 4. 枚举类型 ==========
CREATE TYPE order_status AS ENUM
('pending', 'paid', 'shipped', 'delivered', 'cancelled');
CREATE TABLE orders (
id serial PRIMARY KEY,
status order_status NOT NULL DEFAULT 'pending'
);
INSERT INTO orders (status) VALUES ('paid');
SELECT * FROM orders ORDER BY status;
ALTER TYPE order_status ADD VALUE 'refunded' BEFORE 'cancelled';
-- ========== 5. hstore ==========
CREATE EXTENSION IF NOT EXISTS hstore;
CREATE TABLE products (
id serial PRIMARY KEY,
name text,
attrs hstore
);
CREATE INDEX idx_products_attrs ON products USING gin (attrs);
INSERT INTO products (name, attrs)
VALUES ('笔记本', 'brand=>ThinkPad, ram=>16GB, cpu=>i7');
SELECT name FROM products WHERE attrs ? 'ram';
SELECT name FROM products WHERE attrs @> 'brand=>ThinkPad';
SELECT id, (each(attrs)).key, (each(attrs)).value FROM products;
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。