布尔、枚举与 UUID 类型
本教程共 50 篇 · 第 14 篇 · 更新于 2026-07-31 · 约 7 分钟阅读
14. 布尔、枚举与 UUID 类型
本节目标:学完能使用 boolean、自定义枚举,以及用 uuid 给行生成全局唯一标识。
除了数字和字符串,PostgreSQL 还有几个很实用的特殊类型。本章讲三个:布尔、枚举、UUID。它们各自解决一类具体问题,用对了能让表更干净、数据更不容易写脏。
布尔类型(boolean)
boolean 存「真 / 假」,占 1 字节,还有第三个状态 NULL(未知)。它适合表示开关、是否、有无这类只有两种状态、且可能为空(未知)的字段。
写真值时除了 TRUE / FALSE,还认 'yes'、'on'、'1' 这类写法;假值认 'no'、'off'、'0'。输出时统一显示 t / f。
CREATE TABLE users (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(120),
age SMALLINT,
is_vip BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now()
);
INSERT INTO users (username, is_vip) VALUES ('alice', true);
INSERT INTO users (username, is_vip) VALUES ('bob', 'yes');
SELECT * FROM users WHERE is_vip;
Note查询里直接写
WHERE is_vip就等于WHERE is_vip = TRUE,这是布尔列专属的简洁写法。同理WHERE NOT is_vip表示「为假」。但要注意NULL既不算真也不算假,WHERE is_vip查不出NULL行。要判断「非真」(含未知)得写WHERE is_vip IS NOT TRUE。
布尔列也能建索引、参与条件,比如「找出所有 VIP 用户」:
CREATE INDEX idx_vip ON users(is_vip);
SELECT count(*) FROM users WHERE is_vip;
枚举类型(ENUM)
枚举(enum)适合「取值固定在一个小清单里」的场景,比如订单状态、用户等级。用 CREATE TYPE ... AS ENUM 先定义:
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'done');
定义后就能当列类型用,写入不在清单里的值会报错,从数据库层就挡住了脏数据:
CREATE TABLE orders (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id INT,
amount NUMERIC(10,2),
status order_status DEFAULT 'pending',
created_at TIMESTAMPTZ DEFAULT now()
);
INSERT INTO orders (user_id, amount) VALUES (1, 99.90);
SELECT * FROM orders;
枚举值按定义时的顺序排列,可以用来排序、比较大小。比如 'paid' 排在 'pending' 之后:
SELECT 'paid'::order_status > 'pending'::order_status; -- true
Tip枚举标签区分大小写,
'paid'和'PAID'不是一回事。已创建的枚举不能删某个值,只能加新值(ALTER TYPE ... ADD VALUE)或整体重建。所以定义枚举前想清楚取值清单,别中途老改。
加新值的写法:
ALTER TYPE order_status ADD VALUE IF NOT EXISTS 'cancelled';
Warning
ALTER TYPE ... ADD VALUE从 PostgreSQL 12 起已允许在事务块里执行;但事务提交前,新加的标签还不能被引用(会报unsafe use of new value)。日常在 psql 外层会话直接执行、提交后再用最稳妥。另外它不能删除已有标签,设计时要留余地,别把以后可能要废弃的状态写死又删不掉。
如果枚举值太多了、或者经常变动,其实用「关联小表 + 外键」比枚举更灵活。枚举适合那种几乎不变、且要强约束的小集合。
UUID 类型
UUID 是一串 128 位的全局唯一标识,形如 a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11。分布式系统里比自增 id 更适合做主键,因为多台机器各自生成也不会撞号,还能在插入前就拿到 id。
PostgreSQL 的 uuid 类型能存任意版本的 UUID。生成它用内置函数:
SELECT gen_random_uuid();
-- 5b30857f-0bfa-48b5-ac0b-5c64e28078d1
Note从 PostgreSQL 13 起
gen_random_uuid()就是内置函数,v18 直接能用,不用CREATE EXTENSION。v18 还额外提供uuidv7()生成带时间顺序的 UUID,方便按时间排序,对索引更友好。
把它放进表,给每行自动发一个追踪码:
ALTER TABLE orders
ADD COLUMN tracking_code UUID DEFAULT gen_random_uuid();
INSERT INTO orders (user_id, amount) VALUES (2, 19.90)
RETURNING id, tracking_code;
UUID 也能当主键用。如果你不想要自增、又想全局唯一,完全可以:
CREATE TABLE files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT
);
TipUUID 作主键的缺点是比较长、随机写入会让 B-tree 索引碎片多一些;优点是全局不冲突、能在客户端先生成。自增 id 和 UUID 没有绝对好坏,看业务是否需要分布式、是否需要提前知道 id。需要按时间排序又不想要随机碎片时,
uuidv7()是个好折中。
这三个类型解决了什么
布尔、枚举、UUID 看着不相关,其实共同点是「用对类型让数据库替你把关」。布尔让「是/否」变得一目了然;枚举把「只能从这几个里选」的约束下沉到库里,写错值直接报错;UUID 则解决了「跨系统也不撞号」的标识难题。
反过来,如果你用 varchar 存状态、用 integer 存「是否」,也不是不行,但约束就弱了:状态能写出 'pendding' 这种拼写错误,布尔列写 '否' 还会因为不是标准真假值而报错。类型选得准,脏数据就少,这比事后写一堆校验逻辑划算得多。
布尔的三态与索引
布尔列除了 true/false 还有 NULL。下面四个查询结果各不相同,写条件时要心里有数:
SELECT * FROM users WHERE is_vip; -- 仅 true
SELECT * FROM users WHERE NOT is_vip; -- 仅 false(不含 NULL)
SELECT * FROM users WHERE is_vip IS NULL; -- 仅未知
SELECT * FROM users WHERE is_vip IS NOT TRUE; -- false 加上 NULL
布尔列也能建索引加速筛选,特别是「绝大多数 false、少数 true」时,配合部分索引更高效:
CREATE INDEX idx_vip ON users(is_vip) WHERE is_vip;
枚举的内部与排序
枚举在内部按「定义顺序」存一个整数,所以比较和排序都按你定义时的先后。比如 'pending' < 'paid' < 'shipped',查「已付款之后的状态」可以直接比较:
SELECT * FROM orders WHERE status >= 'paid'::order_status;
但也正因为按定义顺序,想插到中间一个新状态做不到,只能在末尾追加。这是用枚举前要考虑的代价。
UUID 版本与选型
gen_random_uuid() 生成的是 v4(随机)。v18 新增的 uuidv7() 生成 v7(带时间前缀),按生成先后大致有序,插入 B-tree 主键时碎片更少、缓存命中更好:
SELECT uuidv7(); -- 形如 018f... 开头(以毫秒时间戳打头),含时间信息
| 对比 | bigint 自增 | UUID v4 | UUID v7 |
|---|---|---|---|
| 是否有序 | 是 | 否 | 大致有序 |
| 是否全局唯一 | 否(单库) | 是 | 是 |
| 长度 | 8 字节 | 16 字节 | 16 字节 |
| 能否提前生成 | 否 | 是 | 是 |
简单说:单库、要短小、要天然有序 → bigint 自增(用 IDENTITY);分布式、要提前拿 id、要防串通 → UUID,优先 uuidv7()。
Tip想统计「为真的行数」,布尔列不能直接
SUM。先把它转成整数再求和:SELECT SUM(is_vip::int) FROM users;这样一行就得到 VIP 人数。反过来要算「为假或未知」的比例,用SUM((NOT is_vip)::int)。这比先WHERE is_vip再COUNT再UNION干净得多。我刚开始也老写WHERE is_vip单独查,后来发现用::int转一下,一个聚合就能同时拿到真和假的数量,做报表时省掉两条查询,很顺手。
小结
- 真假标志 →
boolean,注意它还有NULL这第三态,查询别漏了。 - 取值固定的小清单 →
CREATE TYPE ... AS ENUM,从库层面挡住非法值,但标签不能删只能加。 - 全局唯一标识 →
uuid+gen_random_uuid()(v18 还有uuidv7()),适合分布式或需提前生成 id 的场景。