想象一下,如果公司的员工表中出现了两条完全相同的“张三”,或者某个员工的部门 ID 指向了一个不存在的部门,甚至工资字段是空的,这会给业务带来多大的灾难?
约束就是数据库的“守门员”。它们是施加在表数据上的规则,用于:
防止无效数据:阻止不符合逻辑的数据进入数据库(如负数的年龄)。
保证数据一致性:确保关联数据之间的逻辑关系正确(如外键)。
简化应用开发:将数据校验逻辑下沉到数据库层,减少代码重复。
在添加约束之前,良好的表结构设计是基础。范式 是设计关系型数据库表的指导原则,旨在减少数据冗余并提高数据一致性。
定义:表中的每一列都是不可再分的基本数据项。
核心:属性不可分割。
反例:一个 address 字段存储了 "广东省广州市天河区",如果需要单独查询“城市”,这就违反了 1NF。应拆分为 province, city, district。
前提:满足 1NF。
定义:非主键列必须完全依赖于整个主键,消除部分依赖。
核心:每张表只描述一件事情。
场景:主要针对联合主键。
错误示例:订单详情表 (订单ID, 商品ID) 为主键。如果表中包含 商品名称,它只依赖于 商品ID,而不依赖于 订单ID,这就是部分依赖。应将商品信息拆分到独立的商品表中。
前提:满足 2NF。
定义:非主键列之间不存在传递依赖,必须直接依赖于主键。
核心:列与列之间不能“隔山打牛”。
错误示例:员工表 (员工ID, 姓名, 部门ID, 部门名称)。部门名称 依赖于 部门ID,而 部门ID 依赖于 员工ID。这就是传递依赖。应将部门信息拆分到部门表中。
遵循范式的优点:减少冗余、避免更新异常、插入异常和删除异常。
在实际高并发场景(如电商大促、报表分析)中,严格的范式会导致大量的表连接(JOIN),严重影响查询性能。此时我们会采用反范式策略:
空间换时间:故意在表中增加冗余字段(如在订单表中直接存储 商品名称 而不是只存 商品ID)。
合并表:将频繁连接的表合并为大宽表。
权衡:牺牲一定的存储空间和维护成本(更新时需同步多处),换取查询速度的大幅提升。
MySQL 提供了多种约束机制,强制执行业务规则。
作用:确保列值不能为 NULL。
场景:姓名、身份证号、入职日期等必填项。
语法:column_name TYPE NOT NULL
作用:确保列值在表中唯一,不允许重复(允许有多个 NULL,具体视数据库版本而定,MySQL InnoDB 中多个 NULL 是允许的)。
场景:用户名、邮箱、手机号。
语法:column_name TYPE UNIQUE
作用:非空 + 唯一 的组合。它是表中每一行数据的唯一标识符。
特点:一张表只能有一个主键。
语法:column_name TYPE PRIMARY KEY
扩展:支持复合主键(多列组合唯一)。
作用:当插入数据未指定该列值时,自动填入预设值。
场景:性别(默认为 '男')、状态(默认为 '正常')、创建时间。
注意:如果显式插入 NULL,则值为 NULL,不会触发默认值。
语法:column_name TYPE DEFAULT value
作用:通常与主键配合,每次插入新记录时,该列值自动加 1。
场景:自增 ID。
注意:必须是索引列(通常是主键),且一张表只能有一个自增列。
作用:建立两个表之间的链接,保证引用完整性。子表的外键值必须存在于父表的主键中。
场景:员工表中的 dept_id 必须存在于部门表中。
语法:
CONSTRAINT fk_name FOREIGN KEY (col) REFERENCES parent_table(parent_col)级联操作(可选):
ON UPDATE CASCADE:父表主键更新,子表自动更新。
ON DELETE CASCADE:父表记录删除,子表对应记录自动删除。
我们将通过两个阶段来验证约束的威力。
我们创建一个员工表,集成主键、非空、唯一、默认和自增约束。
DROP TABLE IF EXISTS emp;
CREATE TABLE emp (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '员工ID,主键自增',
ename VARCHAR(50) NOT NULL UNIQUE COMMENT '姓名,非空且唯一',
joindate DATE NOT NULL COMMENT '入职日期,非空',
salary DOUBLE(7, 2) NOT NULL COMMENT '工资,非空',
bonus DOUBLE(7, 2) DEFAULT 0 COMMENT '奖金,默认0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;✅ 测试 A:正常插入
INSERT INTO emp (ename, joindate, salary, bonus) VALUES ('张三', '1999-11-11', 8800.00, 5000.00);
-- 结果:成功。id 自动生成为 1。❌ 测试 B:违反主键约束 (非空 + 唯一)
尝试插入 id 为 NULL (自增列通常忽略此值,但若强制指定重复值):
-- 场景 1:指定重复的主键值
INSERT INTO emp (id, ename, joindate, salary) VALUES (1, '李四', '2020-01-01', 9000);
-- 报错:Duplicate entry '1' for key 'PRIMARY' (主键冲突)❌ 测试 C:违反非空约束
INSERT INTO emp (ename, joindate, salary) VALUES (NULL, '2020-01-01', 9000);
-- 报错:Column 'ename' cannot be null❌ 测试 D:违反唯一约束
INSERT INTO emp (ename, joindate, salary) VALUES ('张三', '2020-01-01', 9000);
-- 报错:Duplicate entry '张三' for key 'ename' (姓名重复)✅ 测试 E:默认约束生效
不指定 bonus 字段:
INSERT INTO emp (ename, joindate, salary) VALUES ('王五', '2020-01-01', 9000);
SELECT * FROM emp WHERE ename = '王五';
-- 结果:bonus 字段自动填充为 0.00注意:若显式插入 NULL,则值为 NULL,不会使用默认值。
外键是维护多表关系的核心。
规则:必须先创建被引用的表(父表),再创建引用表(子表)。
DROP TABLE IF EXISTS emp;
DROP TABLE IF EXISTS dept;
-- 1. 创建部门表 (父表)
CREATE TABLE dept (
id INT PRIMARY KEY AUTO_INCREMENT,
dep_name VARCHAR(20) NOT NULL,
addr VARCHAR(20)
);
-- 2. 创建员工表 (子表)
CREATE TABLE emp (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
age INT,
dep_id INT,
-- 定义外键约束
CONSTRAINT fk_emp_dept FOREIGN KEY (dep_id) REFERENCES dept(id)
-- 默认行为:RESTRICT (如果父表有引用,禁止删除/更新)
);-- 先插父表
INSERT INTO dept (dep_name, addr) VALUES ('研发部', '广州'), ('销售部', '深圳');
-- 再插子表
INSERT INTO emp (name, age, dep_id) VALUES
('张三', 20, 1), -- 合法:dep_id 1 存在
('李四', 20, 1),
('赵六', 22, 2); -- 合法:dep_id 2 存在❌ 场景 A:插入非法外键
试图将员工分配到一个不存在的部门(如 ID 99):
INSERT INTO emp (name, age, dep_id) VALUES ('孙七', 18, 99);
-- 报错:Cannot add or update a child row: a foreign key constraint fails
-- 解释:部门表中没有 ID 为 99 的记录,引用失败。❌ 场景 B:删除被引用的父记录
试图删除“研发部”(ID 1),但该部门下有员工:
DELETE FROM dept WHERE id = 1;
-- 报错:Cannot delete or update a parent row: a foreign key constraint fails
-- 解释:为了保护数据一致性,数据库阻止删除正在被使用的部门。如果业务需求是:“部门解散了,该部门下的所有员工档案也自动销毁”,我们可以修改外键约束为级联模式。
-- 重建表以演示级联
DROP TABLE IF EXISTS emp;
CREATE TABLE emp (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
dep_id INT,
CONSTRAINT fk_emp_dept_cascade
FOREIGN KEY (dep_id) REFERENCES dept(id)
ON DELETE CASCADE -- 关键:父表删除,子表自动删除
ON UPDATE CASCADE -- 关键:父表主键更新,子表自动更新
);
-- 重新插入数据后,再次执行删除:
DELETE FROM dept WHERE id = 1;
-- 结果:成功!不仅删除了部门 ID 1,emp 表中所有 dep_id=1 的员工记录也被自动删除。最佳实践
主键必选:每张表都应设计主键,推荐使用无业务意义的自增 ID 或雪花算法 ID。
外键的取舍:
在传统金融、ERP 系统中,强烈建议使用数据库外键,以确保绝对的数据一致性。
在高并发互联网系统(如淘宝、微信)中,为了性能和分库分表的灵活性,通常不在数据库层设置物理外键,而是通过应用代码(Java/Go/Python)来控制逻辑上的引用关系。
范式与反范式:设计初期遵循 3NF 减少冗余;在发现性能瓶颈时,针对性地对热点查询进行反范式优化。