数据约束
约束(Constraint)是数据库保证数据完整性的机制,让数据库在写入时自动校验,避免非法数据进入表。它是关系数据库"强约束"价值的核心体现。本文系统讲解六类常用约束:主键、唯一、非空、默认、检查、外键。
约束概述
为什么需要约束
在数据库层面用约束拦截非法数据,而不是完全依赖应用层:
- 应用层校验可被绕过(直接连库、脚本灌数据等);
- 约束是声明式的数据契约,多人协作时结构即文档;
- 数据库能基于约束做查询优化(如唯一索引、主键索引)。
约束分类
| 约束 | 关键字 | 级别 | 作用 |
|---|---|---|---|
| 主键 | PRIMARY KEY | 表级 | 唯一标识一行,非空且唯一 |
| 唯一 | UNIQUE | 列/表级 | 列值不重复(允许一个 NULL) |
| 非空 | NOT NULL | 列级 | 禁止 NULL |
| 默认 | DEFAULT | 列级 | 未提供值时用默认值 |
| 检查 | CHECK | 表级 | 满足表达式条件 |
| 外键 | FOREIGN KEY | 表级 | 引用另一表,保证参照完整性 |
主键 PRIMARY KEY
作用与规则
- 唯一标识一行,非空 + 唯一,一个表只能有一个主键;
- 通常作为聚簇索引(InnoDB),是最高效的定位方式;
- 主键可由一列(单列主键)或多列(联合主键/复合主键)组成。
语法
sql
-- 建表时:列级定义
CREATE TABLE heros (
id INT PRIMARY KEY, -- 列级主键
name VARCHAR(20)
);
-- 建表时:表级定义(支持联合主键)
CREATE TABLE player_game (
player_id INT NOT NULL,
game_id INT NOT NULL,
score INT,
PRIMARY KEY (player_id, game_id) -- 联合主键
);
-- 事后添加/删除
ALTER TABLE heros ADD PRIMARY KEY (id);
ALTER TABLE heros DROP PRIMARY KEY;自增主键
sql
CREATE TABLE heros (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(20),
PRIMARY KEY (id)
);最佳实践:主键尽量用无业务含义的自增整数或分布式 ID,避免用可变长字符串(影响索引大小与性能)。详见《范式设计》中"键"的概念。
唯一 UNIQUE
作用与规则
- 保证列或列组合的值不重复,但允许多个 NULL(NULL 不参与唯一性比较);
- 一个表可有多个唯一约束,会自动建立唯一索引。
语法
sql
-- 列级
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(50) UNIQUE
);
-- 表级 + 联合唯一
CREATE TABLE orders (
order_no VARCHAR(32),
user_id INT,
UNIQUE KEY uk_order_no (order_no),
UNIQUE KEY uk_user_date (user_id, order_date) -- 组合唯一
);
-- 事后添加/删除
ALTER TABLE users ADD UNIQUE KEY uk_email (email);
ALTER TABLE users DROP INDEX uk_email;UNIQUE vs PRIMARY KEY
| 特性 | PRIMARY KEY | UNIQUE |
|---|---|---|
| 是否允许 NULL | ❌ | ✅(一个) |
| 每表数量 | 1 | 多个 |
| 是否自动建索引 | ✅ | ✅ |
| 语义 | 实体标识 | 业务唯一性(如邮箱、订单号) |
典型应用:业务上"不该重复"的字段(手机号、邮箱、订单号)都应用 UNIQUE,靠数据库兜底防止重复数据。
非空 NOT NULL 与默认 DEFAULT
作用
NOT NULL:该列禁止 NULL,必须给值;DEFAULT 值:未显式给值时,数据库填充默认值。
语法
sql
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50) NOT NULL, -- 必填
status TINYINT NOT NULL DEFAULT 1, -- 默认 1
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 默认当前时间
remark VARCHAR(200) DEFAULT '无' -- 可空但有默认
);设计建议
sql
-- ✅ 推荐:业务无值用"空串/0/默认值"而非 NULL
CREATE TABLE users (
nickname VARCHAR(50) NOT NULL DEFAULT '', -- 而非允许 NULL
score INT NOT NULL DEFAULT 0
);
-- ❌ 不推荐:大量可空字段会让查询/索引复杂化
CREATE TABLE users (
nickname VARCHAR(50) NULL,
score INT NULL
);为什么不建议滥用 NULL:NULL 的三值逻辑(TRUE/FALSE/UNKNOWN)容易导致查询陷阱;NULL 不参与索引的某些优化;
NOT NULL + DEFAULT语义更清晰。详见《数据过滤》中 NULL 的处理。
检查 CHECK
作用
通过表达式约束列的值范围,数据库在写入时校验。注意:MySQL 8.0 才真正强制执行 CHECK(5.7 仅解析不执行);SQL Server/Oracle/PostgreSQL 都支持。
语法
sql
-- MySQL 8.0 起强制生效
CREATE TABLE products (
id INT PRIMARY KEY,
price DECIMAL(10,2),
qty INT,
CONSTRAINT chk_price CHECK (price >= 0), -- 价格非负
CONSTRAINT chk_qty CHECK (qty >= 0 AND qty <= 10000)
);
-- 列级 CHECK
CREATE TABLE people (
age INT CHECK (age BETWEEN 0 AND 150),
gender CHAR(1) CHECK (gender IN ('M', 'F'))
);
-- 事后添加(MySQL 8.0)
ALTER TABLE products ADD CONSTRAINT chk_price CHECK (price >= 0);
ALTER TABLE products DROP CHECK chk_price;CHECK 技巧
sql
-- 校验枚举/格式
CREATE TABLE orders (
status VARCHAR(10) CHECK (status IN ('pending', 'paid', 'cancelled')),
email VARCHAR(100) CHECK (email LIKE '%@%')
);MySQL 8.0 注意:8.0.16+ 才强制 CHECK,且要求"每列值满足 CHECK",
REPLACE/INSERT ... ON DUPLICATE KEY UPDATE等也会校验。
外键 FOREIGN KEY
作用
保证参照完整性:子表的外键列值必须存在于父表主键/唯一列中,防止"悬空引用"。
语法
sql
CREATE TABLE team (
team_id INT PRIMARY KEY,
team_name VARCHAR(50)
);
-- 建表时定义外键
CREATE TABLE player (
player_id INT PRIMARY KEY,
player_name VARCHAR(50),
team_id INT,
CONSTRAINT fk_team FOREIGN KEY (team_id) REFERENCES team(team_id)
);
-- 事后添加(MySQL 需指定约束名)
ALTER TABLE player
ADD CONSTRAINT fk_team FOREIGN KEY (team_id) REFERENCES team(team_id);外键的删除/更新动作(ON DELETE / ON UPDATE)
| 动作 | 父表删除/更新时子表行为 |
|---|---|
RESTRICT(默认) | 拒绝删除/更新(若子表有引用) |
NO ACTION | 同 RESTRICT |
CASCADE | 级联删除/更新子表对应行 |
SET NULL | 子表外键置 NULL |
SET DEFAULT | 子表外键置默认值 |
sql
-- 父表删球队时,级联删除该队球员
CREATE TABLE player (
player_id INT PRIMARY KEY,
team_id INT,
FOREIGN KEY (team_id) REFERENCES team(team_id)
ON DELETE CASCADE ON UPDATE CASCADE
);⚠️ 外键的使用争议
sql
-- 物理外键:数据库强制
FOREIGN KEY (team_id) REFERENCES team(team_id)
-- 逻辑外键:应用层维护,字段上只建普通索引
KEY idx_team_id (team_id)| 方式 | 优点 | 缺点 |
|---|---|---|
| 物理外键 | 数据库保证一致性、自动级联 | 影响写入性能、分布式/分库分表不适用、耦合高 |
| 逻辑外键 | 灵活、性能好 | 需应用层保证一致性,可能产生脏数据 |
本节从约束视角概览外键;外键的完整专题分析(多数据库差异、级联场景、逻辑外键 vs 物理外键取舍、最佳实践)见《21-数据库外键》。
约束综合示例
把六类约束用在一个订单系统上:
sql
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
email VARCHAR(100) NOT NULL,
phone VARCHAR(20),
status TINYINT NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_email (email),
CONSTRAINT chk_status CHECK (status IN (0, 1, 2))
) ENGINE=InnoDB;
CREATE TABLE orders (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
amount DECIMAL(12,2) NOT NULL,
status VARCHAR(10) NOT NULL DEFAULT 'pending',
paid_at DATETIME NULL,
PRIMARY KEY (id),
KEY idx_user (user_id),
CONSTRAINT chk_amount CHECK (amount >= 0),
CONSTRAINT chk_order_status CHECK (status IN ('pending','paid','cancelled')),
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB;约束与数据完整性
四类完整性
| 完整性 | 机制 | 约束 |
|---|---|---|
| 实体完整性 | 每行有唯一标识 | PRIMARY KEY、UNIQUE |
| 域完整性 | 列值符合规定 | NOT NULL、DEFAULT、CHECK、数据类型 |
| 参照完整性 | 外键引用有效 | FOREIGN KEY |
| 用户定义完整性 | 业务规则 | CHECK + 应用逻辑 |
约束设计建议
- 必填字段一律 NOT NULL + DEFAULT,避免 NULL 泛滥;
- 业务唯一字段用 UNIQUE 兜底,防止应用层漏判;
- CHECK 适合静态、简单的取值范围;复杂业务规则交给应用层;
- 物理外键按需:强一致单机可用,高并发/分库慎用(用逻辑外键);
- 约束在建模阶段设计,事后补约束成本高(需扫描/重建数据)。
总结
约束速查
| 约束 | 关键字 | 作用 | MySQL 8.0 |
|---|---|---|---|
| 主键 | PRIMARY KEY | 非空唯一标识 | ✅ |
| 唯一 | UNIQUE | 值不重复(允许一个 NULL) | ✅ |
| 非空 | NOT NULL | 禁止 NULL | ✅ |
| 默认 | DEFAULT | 缺省值 | ✅ |
| 检查 | CHECK | 值满足表达式 | ✅(8.0.16+ 强制) |
| 外键 | FOREIGN KEY | 参照完整性 | ✅ |
关键点
- 约束是数据库层的数据契约,比应用层校验更可靠;
- 主键唯一标识行、唯一保证业务不重复、非空+默认收敛 NULL、CHECK 限定取值范围、外键保证引用有效;
NOT NULL + DEFAULT是消除 NULL 陷阱的推荐组合;- 物理外键能保证一致性但有性能/耦合代价,互联网架构常用逻辑外键替代;
- 约束尽量在建表时设计好,配合《13-数据定义语言DDL》《21-数据库外键》阅读更完整。