卷 III · 类型CH 13深度 13/24

超能力类型:数组、范围、枚举、域

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),下面还有范围类型的包含 / 重叠。

⚠ 数组下标从 1 开始,不是 0

写惯了各种语言的程序员在这里必栽: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 化——约束不再散落各表,而是升级成了一个有名字的类型。

◆ 有了这些,schema 能更贴合现实

这些类型的共同价值是:让数据库的模型更接近你脑子里的业务模型。一本书的标签本来就是「一组词」,那就用数组;一个预约本来就是「一段时间」,那就用范围;一个订单状态本来就是「几个固定值之一」,那就用枚举。模型越贴合,约束越自然、查询越直接、bug 越少。这是 PostgreSQL 那句口号「对象-关系数据库」的实感——它不止有关系表,还有丰富的类型系统。

⚠ 别滥用:该拆表时还是要拆

数组和枚举很香,但不是万能。判断标准是:这些「多个值」需要单独被查询、被约束、被关联吗?

  • 书的标签,只用来展示和粗筛 → 用 text[] 合适。
  • 但如果标签本身要有 id、有描述、要统计每个标签被用了多少次、要和别的东西关联 → 那它其实是个实体,该拆成 tags 表 + book_tags 关联表,回归规范化。

数组 / JSONB 是「把结构塞进一格」的诱惑,用错了会退化成难查、难约束的一团。能明确建模成关系的,优先关系。

⇄ 对照 MySQL
  • 数组:MySQL 没有原生数组类型。人们通常塞逗号分隔的字符串(难查、难约束、难索引),或建关联表。PostgreSQL 的数组还能建 GIN 索引,让 @> 走索引。
  • 范围类型:MySQL 没有。区间只能用两列 + 手写边界判断,「防重叠」尤其难。
  • 枚举:MySQL 有 ENUM,但它是列级的(写死在某一列上,不能共享),而且历史上有些奇怪行为(非法值可能变空串)。PostgreSQL 的枚举是可共享的独立类型
  • :MySQL 没有 DOMAIN,无法定义「带约束的可复用类型」。

这一整章,基本就是 PostgreSQL 相对 MySQL 的「能力代差」所在。

这一章的一句话

数组装多值、范围装区间、枚举是可共享的取值集、域是带约束的可复用类型——它们让 schema 更贴合业务;但能明确建模成关系的东西,还是优先拆表。

还有一种「把结构塞进一格」的能力,值得单开一章,因为它太好用也太容易被滥用——那就是 JSONB。下一章,关系库里的文档。