数据库约束一次讲清:把"能插入"升级成"插入就对"
数据库表如果没有约束,就像没有交通规则的十字路口:看起来自由,实际上全靠运气。约束做的事情很朴素:给表里的数据加规则,从源头保证准确性、可靠性与一致性。
本文把常见约束按"什么时候用、怎么建、违反会怎样"讲透,并且每个板块都配一条能直接跑的 SQL 例句。
1)约束到底是什么:给数据立规矩
来看一句定义:数据库约束是施加在表数据上的规则/条件,用来确保数据准确、可靠、相容。约束可以基于数据类型、取值范围、唯一性、非空等规则。
常见类型包括:NOT NULL、DEFAULT、UNIQUE、PRIMARY KEY、FOREIGN KEY、CHECK。
2)NOT NULL:非空约束(字段必须有值)
场景:姓名、标题、订单号这类字段,空值没有意义,就该禁止 NULL。
来看这段代码:给 name 加非空约束后,再插入 NULL 会直接报错。
sql
DROP TABLE IF EXISTS student;
CREATE TABLE student (
id BIGINT,
name VARCHAR(20) NOT NULL
);
INSERT INTO student VALUES (1, NULL); -- 触发:Column 'name' cannot be null
INSERT INTO student VALUES (1, '张三'); -- 正常
顺手检查结构也很直观:DESC student; 里 Null 列会显示 NO(不允许 NULL)或 YES(允许 NULL)。
3)DEFAULT:默认值约束(不赋值时给个"兜底值")
场景:年龄默认 18、状态默认"未支付"、创建时间默认当前时间等。
来看这段代码:age 设置默认 18,不提供 age 时自动填 18;但如果手动明确插入 NULL,依然会得到 NULL(默认值不会强行覆盖显式 NULL)。
sql
DROP TABLE IF EXISTS student;
CREATE TABLE student (
id BIGINT,
name VARCHAR(20) NOT NULL,
age INT DEFAULT 18
);
INSERT INTO student(id, name) VALUES (1, '张三'); -- age=18
INSERT INTO student(id, name, age) VALUES (2, '李四', NULL); -- age=NULL(显式给 NULL)
DESC student; 里 Default 列能看到默认值(例如 18)。
4)UNIQUE:唯一约束(值不能重复)
场景:学号、身份证号、邮箱、手机号------同一张表里不允许重复。
来看这段代码:sno 加唯一约束后,重复插入会报 Duplicate entry。
sql
DROP TABLE IF EXISTS student;
CREATE TABLE student (
id BIGINT,
name VARCHAR(20) NOT NULL,
age INT DEFAULT 18,
sno VARCHAR(10) UNIQUE
);
INSERT INTO student(id, name, sno) VALUES (1, '张三', '100001');
INSERT INTO student(id, name, sno) VALUES (2, '李四', '100001'); -- 触发唯一冲突
结构里也能看出来:DESC student; 的 Key 列会显示 UNI 表示唯一约束。
5)PRIMARY KEY:主键约束(每行的"身份证")
主键的硬规则很简单但很强:
- 唯一标识每条记录
- 不能重复
- 不能为 NULL
- 每张表只能有一个主键
- 主键可以是单列 或多列(复合主键)
- 通常建议每张表都有主键,且主键列常用
BIGINT。
5.1 自增主键:让数据库维护主键值
来看这段代码:id 设置 PRIMARY KEY auto_increment,插入时不给 id 或给 NULL,数据库会自动生成。DESC 里 Extra 显示 auto_increment。
sql
DROP TABLE IF EXISTS student;
CREATE TABLE student (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL,
age INT DEFAULT 18,
sno VARCHAR(10) UNIQUE
);
INSERT INTO student(id, name, sno) VALUES (NULL, '张三', '100001'); -- 自动生成 id
INSERT INTO student(name, sno) VALUES ('李四', '100002'); -- 自动生成 id
5.2 主键不一定连续:插入失败会"浪费号段"
来看这段代码:如果某次插入因为唯一冲突失败,已分配的自增 id 可能作废,导致后续 id 出现跳号。
sql
INSERT INTO student(name, sno) VALUES ('王五', '100002'); -- sno 冲突,插入失败
INSERT INTO student(name, sno) VALUES ('王五', '100003'); -- 下一次成功插入,id 可能跳过
5.3 手动指定更大的主键:下一次自增从最大值继续
sql
INSERT INTO student(id, name, sno) VALUES (100, '赵六', '100004');
INSERT INTO student(name, sno) VALUES ('钱七', '100005'); -- 自增会从 100 往后
5.4 主键冲突时"插入否则更新":ON DUPLICATE KEY UPDATE
来看这段代码:当插入触发主键/唯一键冲突时,转而执行更新,等效于对冲突行做 UPDATE ... WHERE key=...。
sql
INSERT INTO student(id, name, sno) VALUES (100, '赵六', '100100')
ON DUPLICATE KEY UPDATE name='赵六', sno='100100';
5.5 REPLACE:存在冲突就"替换",不存在就插入
REPLACE 的效果更像"先删后插":冲突时会影响 2 行(删除旧行 + 插入新行),不冲突时影响 1 行。
sql
REPLACE INTO student(id, name, sno) VALUES (101, '钱七', '100101'); -- 冲突则替换
REPLACE INTO student(id, name, sno) VALUES (102, '吴八', '100102'); -- 不冲突则插入
5.6 只能有一个主键 & 复合主键
主键只能定义一次:两列都写 PRIMARY KEY 会直接报 "Multiple primary key defined"。
复合主键则由多列共同决定是否冲突:
sql
DROP TABLE IF EXISTS student;
CREATE TABLE student (
id BIGINT,
name VARCHAR(20),
PRIMARY KEY (id, name)
);
INSERT INTO student(id, name) VALUES (1, '张三');
INSERT INTO student(id, name) VALUES (1, '张三'); -- 复合主键冲突
INSERT INTO student(id, name) VALUES (2, '张三'); -- 可插入(id 不同)
6)FOREIGN KEY:外键约束(维护表与表之间的关系)
外键用于定义**主表(父表)与从表(子表)**之间的关联:外键定义在从表列上,引用主表的主键或唯一列。要求从表外键列的值要么在主表存在,要么为 NULL。
6.1 建两张表:class(主表)+ student(从表)
来看这段代码:student.class_id 引用 class.id。
sql
DROP TABLE IF EXISTS class;
CREATE TABLE class (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL
);
INSERT INTO class(name) VALUES ('java01'), ('java02'), ('java03');
DROP TABLE IF EXISTS student;
CREATE TABLE student (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL,
age INT DEFAULT 18,
class_id BIGINT,
FOREIGN KEY (class_id) REFERENCES class(id)
);
DESC student; 里 class_id 的 Key 可能显示 MUL(文档示例中用于表示外键列)。
6.2 外键插入规则:主表不存在就失败,但 NULL 可以
sql
INSERT INTO student(name, class_id) VALUES ('张三', 1); -- OK(主表存在)
INSERT INTO student(name, class_id) VALUES ('王五', 100); -- 失败:主表无此 id
INSERT INTO student(name, class_id) VALUES ('赵六', NULL); -- OK:表示暂未分配班级
6.3 删除/删表规则:被引用的主表记录不能随便删,删表要先删从表
sql
DELETE FROM class WHERE name='java03'; -- OK(没人引用)
DELETE FROM class WHERE name='java01'; -- 失败(student 引用了该班级)
并且:如果从表存在外键引用,直接 DROP TABLE class; 会失败;必须先删从表再删主表。
sql
DROP TABLE class; -- 失败:被 student 外键引用
DROP TABLE student; -- 先删从表
DROP TABLE class; -- 再删主表,成功
7)CHECK:校验约束(限制取值范围/枚举/列间关系)
CHECK 用来限制列或数据能接受的值,保证完整性与准确性。并且有个版本点很关键:MySQL 在 8.0.16 开始全面支持 CHECK,之前版本会忽略 CHECK 定义。
7.1 常见:年龄范围 + 性别枚举
sql
DROP TABLE IF EXISTS student;
CREATE TABLE student (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL,
age INT DEFAULT 18,
gender CHAR(1),
CHECK (age >= 16),
CHECK (gender = '男' OR gender = '女')
);
INSERT INTO student(name, age, gender) VALUES ('张三', 17, '男'); -- OK
INSERT INTO student(name, age, gender) VALUES ('张三', 15, '男'); -- 失败:违反 age>=16
INSERT INTO student(name, age, gender) VALUES ('张三', 17, '1'); -- 失败:违反性别枚举
7.2 列与列比较:跨列约束要单独写一行
sql
CREATE TABLE t_check (
c1 INT CHECK (c1 <> 0),
c2 INT CHECK (c2 > 0),
c3 INT,
CHECK (c3 >= c2)
);
INSERT INTO t_check VALUES (-1, 3, 10); -- OK
INSERT INTO t_check VALUES (0, 5, 6); -- 失败:c1 不能为 0
INSERT INTO t_check VALUES (2, -10, 10);-- 失败:c2 必须 >0
INSERT INTO t_check VALUES (2, 10, 9); -- 失败:c3 必须 >= c2
8)把这些约束组合起来:一张"能扛事"的 student 表长什么样
来看一份"常用组合":必填字段用 NOT NULL,默认值用 DEFAULT,学号用 UNIQUE,主键用自增 PRIMARY KEY AUTO_INCREMENT,班级关系用 FOREIGN KEY,业务校验用 CHECK。
sql
CREATE TABLE student (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL,
age INT DEFAULT 18,
sno VARCHAR(10) UNIQUE,
class_id BIGINT,
gender CHAR(1),
FOREIGN KEY (class_id) REFERENCES class(id),
CHECK (age >= 16),
CHECK (gender = '男' OR gender = '女')
);
约束的底层逻辑其实很"工程":让错误更早发生、发生在数据库层、并且以一致方式发生。数据一旦守规矩,上层业务代码就少掉一堆"补锅 if-else",整个系统都会更稳。