唯一、非空、检查与默认值约束
本教程共 50 篇 · 第 37 篇 · 更新于 2026-07-31 · 约 12 分钟阅读
37. 唯一、非空、检查与默认值约束
本节目标:掌握 UNIQUE、NOT NULL、CHECK、DEFAULT 四种列约束的写法,知道 NULL 在它们各自中的行为差异,会如何给约束命名、查看和删除,能在建表时给数据加上合理的校验。
约束(constraint)是加在表或列上的「规矩」,用来保证存进去的数据靠谱。第 36 章讲了主键和外键,这章补全四家常客:UNIQUE、NOT NULL、CHECK、DEFAULT。它们都能写在建表时,也能事后用 ALTER TABLE 调整。
Note约束尽量下在数据库层,而不是只靠应用代码校验。因为数据可能从多处写入(脚本、别的程序、手动 psql),数据库层的约束是最后一道、也是最可靠的防线。
UNIQUE:值不能重复
UNIQUE 保证这一列(或这几列的组合)在表里没有两行相同。比如用户名、邮箱通常要唯一。
CREATE TABLE users (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) UNIQUE
);
也可以写成表级约束,多个列组合唯一时用这种:
CREATE TABLE users (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100),
UNIQUE (username, email) -- 两列组合不重复即可
);
插入重复邮箱会报错:ERROR: duplicate key value violates unique constraint ...。和主键不同的是:
- 一张表可以有多个
UNIQUE,但只能有一个主键; UNIQUE列允许有多个NULL(在 SQL 里,NULL 互不相等,所以不算重复)。
加 UNIQUE 时 PostgreSQL 会自动建一个唯一索引,所以按这列查重也很快。
-- 多列唯一:允许 (张三, 138...) 和 (张三, 139...) 共存,但不允许完全相同
ALTER TABLE users ADD CONSTRAINT uq_user_contact UNIQUE (username, email);
NOT NULL:不许为空
NULL 表示「未知/缺失」。NOT NULL 强制这一列必须有值。
CREATE TABLE users (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username VARCHAR(50) NOT NULL,
age INT,
created_at DATE
);
这里 age、created_at 没写 NOT NULL,所以允许为空;username 必须填。试图插入 NULL 到 username 会直接报错。注意:默认情况下列是允许 NULL 的,要禁空就得显式写 NOT NULL。
给已有列追加非空约束:
ALTER TABLE users ALTER COLUMN age SET NOT NULL;
Warning如果该列已经存在
NULL值,直接SET NOT NULL会失败。得先把NULL改成具体值,再加约束。反过来,去掉非空用ALTER TABLE users ALTER COLUMN age DROP NOT NULL;。
CHECK:满足条件才让进
CHECK 用一个布尔表达式给数据「立规矩」,只有表达式为真(或结果为 NULL)才允许插入/更新。
CREATE TABLE users (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username VARCHAR(50) NOT NULL,
age INT CHECK (age >= 0 AND age < 150)
);
插入 age = -5 会报错,违反检查约束。表达式可以很灵活,还能调用函数:
-- 用户名至少 3 个字符(去空格后)
ALTER TABLE users
ADD CONSTRAINT username_len_check
CHECK (LENGTH(TRIM(username)) >= 3);
多列联合校验也行,比如「结束日期不能早于开始日期」:
CREATE TABLE projects (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
start_date DATE,
end_date DATE
);
ALTER TABLE projects
ADD CONSTRAINT dates_check
CHECK (end_date >= start_date);
Warning注意
CHECK里NULL的坑:表达式结果为NULL时不算违规,允许通过(因为约束只拒绝「结果为假」的行,而NULL既不是真也不是假)。如果希望「空也不行」,要把该列同时设为NOT NULL,否则带NULL的行会绕过 CHECK。
DEFAULT:没填就用默认值
DEFAULT 给列一个默认值。插入时没指定这一列,PostgreSQL 就用它。
CREATE TABLE users (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username VARCHAR(50) NOT NULL,
age INT DEFAULT 18,
created_at DATE DEFAULT CURRENT_DATE
);
插入 INSERT INTO users(username) VALUES ('Tom'); 时,age 自动变 18,created_at 变成当天。默认值可以是字面量,也可以是表达式(如 CURRENT_DATE、now()、gen_random_uuid())。
既想用默认值又想在 INSERT 里显式占位,可以写 DEFAULT 关键字:
INSERT INTO users(username, age) VALUES ('Tom', DEFAULT);
事后改默认值、删默认值:
ALTER TABLE users ALTER COLUMN age SET DEFAULT 20;
ALTER TABLE users ALTER COLUMN age DROP DEFAULT;
Note
DEFAULT只在「没指定这列」时生效。如果显式插入NULL,那么存进去的就是NULL,默认值不会兜底。DEFAULT管的是「省略」,不是「填空」。想让NULL也变成默认值,得配合NOT NULL或在写入前用COALESCE处理。
给约束命名与查看
写约束时最好用 CONSTRAINT 名字 ... 显式起名,方便以后维护。系统自动命名的格式一般是 表_列_约束类型,比如 users_age_check,可读性差且难记。
查看一张表上有哪些约束,用 psql 的 \d:
postgres=# \d users
输出里会列出 CHECK、UNIQUE、NOT NULL 等约束及它们的名字。删除约束统一用「按名删除」:
ALTER TABLE users DROP CONSTRAINT username_len_check;
Tip想一次性看全库约束,可以查系统视图:
SELECT constraint_name, table_name, constraint_type FROM information_schema.table_constraints;排查「为什么插不进数据」时很好用。
多种约束可以叠加
一列上可以同时挂多个约束,顺序无所谓:
CREATE TABLE users (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE CHECK (LENGTH(username) > 0),
age INT NOT NULL CHECK (age >= 0) DEFAULT 18
);
NOT NULL、UNIQUE、CHECK、DEFAULT 各管一摊,配合起来就能把数据规矩定得很细。再叠加上一章的主键、外键,一张表的数据质量就能从库层面兜住。
四种约束行为对比:
| 约束 | 是否允许 NULL | 主要作用 |
|---|---|---|
NOT NULL | 不允许 | 必须填值 |
UNIQUE | 允许多个 NULL | 值不重复 |
CHECK | 表达式 NULL 时放行 | 满足条件才让进 |
DEFAULT | 可 | 省略时填默认值 |
常见错误
- 以为
UNIQUE会拦NULL重复——其实 NULL 不算重复,允许多个。 - 给已有
NULL的列加NOT NULL或CHECK失败,忘了先清数据。 - 以为
DEFAULT能兜住显式写入的NULL——它只管「省略列」的情况。 - 以为
CHECK会自动拒绝NULL——表达式结果为 NULL 时约束放行。
约束对写入性能的影响
每加一道约束,写入(INSERT/UPDATE)时数据库就多一道检查,所以约束不是越多越好:
NOT NULL、CHECK:开销很小,基本可忽略。UNIQUE:写入时要维护唯一索引,数据量大时插入会变慢一点。DEFAULT:几乎无成本,只在省略列时算一次。- 外键(第 36 章):写入子表时要去父表核对,建议在外键列上建索引。
Tip对高频写入的表,核心约束(NOT NULL、CHECK、关键 UNIQUE)该上加上;一些「锦上添花」的 CHECK 可以放到应用层,或只在入库前批次校验,平衡可靠性与性能。
用 SQL 查看约束明细
除了 psql 的 \d,也能直接查系统视图拿到约束定义,方便写脚本或排查:
SELECT conname AS 约束名,
pg_get_constraintdef(oid) AS 定义
FROM pg_constraint
WHERE conrelid = 'users'::regclass;
pg_get_constraintdef 会把约束还原成建表时的定义文本,比 \d 更适合程序化处理。想看全库的,去掉 WHERE 条件即可。
约束命名的小建议
起名用「表_列_类型」这种可读格式,比如 users_email_unique、orders_amount_check。别用系统自动生成的 users_email_key 这类含糊名字——出问题时你根本想不起它管什么。好名字能让你删约束、查问题省力一半,也是多人协作时少踩坑的基本功。
小结
四种列约束各管一件事:UNIQUE 防重复(允许多个 NULL)、NOT NULL 禁空、CHECK 按布尔条件校验(NULL 会放行)、DEFAULT 在省略列时填空值。建表时写进列定义,事后用 ALTER TABLE 增删;给约束起个好名字,再用 \d 表名 或系统视图就能随时查看。把约束下在数据库层,比在代码里校验更可靠。