超能力类型:数组、范围、枚举、域
MySQL 用户碰到「一格想放多个值」,通常只能塞个逗号串或另开一张表。PostgreSQL 有一整套别家几乎没有的类型:数组、范围、枚举、域、复合类型。它们不是花架子——用对了,能让 schema 更贴合业务、约束更强、查询更利落。
数组:一格装多个同类值
任何类型后面加 [] 就成了它的数组类型:text[]、integer[]。它有自己的字面量和一套操作符:
CREATE TABLE books2 (id int, tags text[]);
INSERT INTO books2 VALUES (1, '{scifi,award,ursula}'); -- 或 ARRAY['scifi','award']
SELECT tags[1] FROM books2; -- 取第 1 个元素(注意:下标从 1 开始!)
SELECT array_length(tags,1) FROM books2; -- 元素个数
SELECT * FROM books2 WHERE tags @> ARRAY['scifi']; -- 包含 scifi
SELECT * FROM books2 WHERE 'award' = ANY(tags); -- award 是数组元素之一
SELECT unnest(tags) FROM books2; -- 把数组「炸开」成多行
下面这台真引擎在一张带 tags text[] 的表上,演示三种数组查询(@> 包含、&& 有交集、= ANY),下面还有范围类型的包含 / 重叠。
写惯了各种语言的程序员在这里必栽:PostgreSQL 数组 arr[1] 是第一个元素,arr[0] 返回 NULL(越界不报错,给 NULL)。这是 SQL 数组的历史约定,记住就好。
范围类型:一个区间当一个值
「有效期」「价目区间」「预约时段」这类连续区间,用两列 start/end 表达很笨,判断包含 / 重叠还得手写一堆边界比较。PostgreSQL 有范围类型,把一个区间当成一个值:
SELECT int4range(10, 20) @> 15; -- t:15 落在 [10,20) SELECT int4range(10, 20) && int4range(18, 30); -- t:两区间重叠 SELECT '[2026-01-01,2026-04-01)'::daterange @> DATE '2026-02-14'; -- t
内置的有 int4range / int8range / numrange / tsrange / tstzrange / daterange。默认是「左闭右开」[a, b)——和你写代码切片的习惯一致。范围类型配合上一章的 EXCLUDE 约束,就能优雅地表达「时段不许重叠」这种规则。
枚举:一个独立、可共享的取值集
状态字段(pending / paid / shipped)用枚举,比用文本 + CHECK 更规整。PostgreSQL 的枚举是独立的类型,能被多张表、多列复用:
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'cancelled');
CREATE TABLE orders2 (id int, status order_status NOT NULL DEFAULT 'pending');
INSERT INTO orders2 VALUES (1, 'paid');
INSERT INTO orders2 VALUES (2, 'unknown'); -- ERROR:不是合法的 order_status
枚举值有内在顺序(按定义顺序),能比大小、能排序。要加新状态用 ALTER TYPE ... ADD VALUE。注意枚举适合「稳定、极少变」的取值集;如果取值经常增删、还带额外属性,老老实实建一张字典表更灵活。
域:给类型加一层可复用的约束
域(DOMAIN)是「一个带了约束的类型别名」。如果你的库里到处都有「邮箱」「正数金额」「非空短文本」,与其每张表都重写一遍 CHECK,不如定义一次域,到处复用:
CREATE DOMAIN email AS text CHECK (VALUE ~ '^[^@]+@[^@]+\.[^@]+$'); -- 一个简化的邮箱格式校验 CREATE DOMAIN positive_money AS numeric(12,2) CHECK (VALUE >= 0); CREATE TABLE users (id int, contact email, balance positive_money); -- contact 列自动带上邮箱格式校验,balance 自动非负
域让「什么样的值算合法邮箱」这条规矩集中定义、一处修改、处处生效。这是把「当真」DRY 化——约束不再散落各表,而是升级成了一个有名字的类型。
这些类型的共同价值是:让数据库的模型更接近你脑子里的业务模型。一本书的标签本来就是「一组词」,那就用数组;一个预约本来就是「一段时间」,那就用范围;一个订单状态本来就是「几个固定值之一」,那就用枚举。模型越贴合,约束越自然、查询越直接、bug 越少。这是 PostgreSQL 那句口号「对象-关系数据库」的实感——它不止有关系表,还有丰富的类型系统。
数组和枚举很香,但不是万能。判断标准是:这些「多个值」需要单独被查询、被约束、被关联吗?
- 书的标签,只用来展示和粗筛 → 用
text[]合适。 - 但如果标签本身要有 id、有描述、要统计每个标签被用了多少次、要和别的东西关联 → 那它其实是个实体,该拆成
tags表 +book_tags关联表,回归规范化。
数组 / JSONB 是「把结构塞进一格」的诱惑,用错了会退化成难查、难约束的一团。能明确建模成关系的,优先关系。
- 数组:MySQL 没有原生数组类型。人们通常塞逗号分隔的字符串(难查、难约束、难索引),或建关联表。PostgreSQL 的数组还能建 GIN 索引,让
@>走索引。 - 范围类型:MySQL 没有。区间只能用两列 + 手写边界判断,「防重叠」尤其难。
- 枚举:MySQL 有
ENUM,但它是列级的(写死在某一列上,不能共享),而且历史上有些奇怪行为(非法值可能变空串)。PostgreSQL 的枚举是可共享的独立类型。 - 域:MySQL 没有
DOMAIN,无法定义「带约束的可复用类型」。
这一整章,基本就是 PostgreSQL 相对 MySQL 的「能力代差」所在。
这一章的一句话
数组装多值、范围装区间、枚举是可共享的取值集、域是带约束的可复用类型——它们让 schema 更贴合业务;但能明确建模成关系的东西,还是优先拆表。
还有一种「把结构塞进一格」的能力,值得单开一章,因为它太好用也太容易被滥用——那就是 JSONB。下一章,关系库里的文档。