1. 为什么需要表的约束
我刚接触MySQL那会儿,经常遇到数据乱七八糟的情况。比如用户表里突然冒出两个一模一样的身份证号,订单表里出现了不存在的用户ID,还有些必填字段莫名其妙就空了。这些问题让我意识到:没有规矩,不成方圆。数据库表的约束,就是给数据立规矩。
约束的本质是数据完整性保障机制。想象一下银行系统如果没有约束会怎样?你的账户余额可能突然变成负数,或者转账记录凭空消失。我在金融项目里就吃过这种亏——因为漏加外键约束,导致对账时发现资金流水和账户余额对不上,排查了整整三天。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. MySQL的五大约束类型
2.1 非空约束(NOT NULL)
上周排查的一个生产问题特别典型:用户注册时没填手机号,结果短信通知系统疯狂报错。这就是没加NOT NULL约束的后果。
sql复制CREATE TABLE users (
id INT PRIMARY KEY,
phone VARCHAR(20) NOT NULL, -- 必须填写
wechat VARCHAR(30) -- 可选
);
注意:给已有字段添加NOT NULL约束时,必须确保该列没有NULL值,否则会报错。我通常先用
UPDATE table SET column='' WHERE column IS NULL清理数据。
2.2 唯一约束(UNIQUE)
电商平台的SKU编码必须唯一,这个道理大家都懂。但我在实际项目中见过更隐蔽的问题:用户邮箱允许重复导致密码重置链接发错人。
sql复制ALTER TABLE products ADD CONSTRAINT uk_sku
UNIQUE (sku_code); -- 添加唯一约束
-- 复合唯一约束示例
CREATE TABLE user_devices (
user_id INT,
device_id VARCHAR(64),
CONSTRAINT uk_user_device UNIQUE (user_id, device_id)
);
有个坑要注意:唯一约束允许存在多个NULL值,因为NULL不等于任何值包括它自己。如果确实需要防止重复NULL,可以配合DEFAULT值使用。
2.3 主键约束(PRIMARY KEY)
主键是表的身份证,设计时要注意:
- 自增INT/BIGINT最常用,但分布式系统建议用UUID或雪花ID
- 不要用业务字段(如身份证号)当主键
- 复合主键要谨慎,可能影响JOIN性能
sql复制-- 自增主键
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(32) UNIQUE
);
-- 复合主键(适用于关联表)
CREATE TABLE order_items (
order_id INT,
item_id INT,
quantity INT,
PRIMARY KEY (order_id, item_id)
);
2.4 外键约束(FOREIGN KEY)
外键是关系数据库的基石,但用不好会变成性能杀手。我的经验法则是:
- 开发环境加外键方便排查问题
- 生产环境大表慎用,改用应用层校验
- 一定要加ON DELETE/UPDATE策略
sql复制CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
CONSTRAINT fk_user
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE SET NULL
ON UPDATE CASCADE
);
踩坑记录:有次误删用户导致关联订单全部消失,就是因为外键设置了ON DELETE CASCADE。现在我会优先考虑SET NULL或RESTRICT。
2.5 检查约束(CHECK)
MySQL 8.0终于原生支持CHECK约束了!之前只能用触发器模拟,现在可以这样:
sql复制CREATE TABLE employees (
id INT PRIMARY KEY,
salary DECIMAL(10,2) CHECK (salary > 0),
gender ENUM('M','F'),
age TINYINT CHECK (age >= 18)
);
-- 复杂检查条件
ALTER TABLE products ADD CONSTRAINT chk_price
CHECK (price > cost * 1.2);
3. 约束的高级玩法
3.1 约束命名规范
建议给约束显式命名,方便后续管理:
- pk_表名:主键
- uk_表名_字段:唯一约束
- fk_从表_主表:外键
- chk_表名_字段:检查约束
sql复制ALTER TABLE users ADD CONSTRAINT uk_users_email
UNIQUE (email);
3.2 延迟约束检查
事务场景下,可以临时禁用约束检查:
sql复制SET FOREIGN_KEY_CHECKS = 0;
-- 执行数据修复操作
SET FOREIGN_KEY_CHECKS = 1;
警告:这个操作非常危险,一定要确保修复后的数据满足约束条件。
3.3 约束与索引的关系
很多人不知道的是:
- 主键自动创建聚簇索引
- 唯一约束自动创建唯一索引
- 普通索引不能替代约束,因为它允许重复值
sql复制-- 查看约束对应的索引
SHOW INDEX FROM table_name;
4. 性能优化建议
4.1 约束的开销
每种约束都会带来额外开销:
- NOT NULL:几乎零成本
- UNIQUE:需要维护唯一索引
- FOREIGN KEY:检查引用表
- CHECK:逐行验证条件
在亿级数据表上加约束要谨慎,我通常这样做:
- 先不加约束导入数据
- 用SQL验证数据完整性
- 最后再加约束
4.2 批量导入优化
大批量导入时临时禁用约束和索引:
sql复制-- 导入前
ALTER TABLE big_table DISABLE KEYS;
-- 导入后
ALTER TABLE big_table ENABLE KEYS;
ANALYZE TABLE big_table;
4.3 外键的替代方案
高并发场景下,可以考虑:
- 使用应用层校验
- 定期执行校验脚本
- 使用触发器(谨慎)
python复制# Python伪代码示例
def create_order(user_id):
if not User.objects.filter(id=user_id).exists():
raise ValueError("用户不存在")
# 创建订单逻辑
5. 常见问题排查
5.1 约束冲突错误
遇到错误时这样排查:
- 看清错误类型(Duplicate entry/ Cannot add foreign key等)
- 用SHOW CREATE TABLE查看约束定义
- 查询冲突的具体数据
sql复制-- 查找重复值
SELECT email, COUNT(*) FROM users
GROUP BY email HAVING COUNT(*) > 1;
-- 查找违反外键的值
SELECT * FROM orders
WHERE user_id NOT IN (SELECT id FROM users);
5.2 约束导致的死锁
外键约束可能引发死锁,典型场景:
- 事务A删除主表记录
- 事务B同时在从表插入关联记录
- 双方互相等待
解决方案:
- 调整事务隔离级别
- 按固定顺序访问表
- 使用SELECT FOR UPDATE提前锁定
5.3 修改约束的注意事项
修改约束要小心数据兼容性:
- 删除NOT NULL前检查是否有NULL值
- 放松CHECK条件前验证现有数据
- 修改外键可能需先删除子表数据
sql复制-- 安全删除约束的步骤
START TRANSACTION;
-- 1. 备份数据
CREATE TABLE users_backup SELECT * FROM users;
-- 2. 验证新约束
ALTER TABLE users MODIFY email VARCHAR(255);
-- 3. 确认无误后提交
COMMIT;
6. 最佳实践总结
经过多年踩坑,我总结的约束使用原则:
- 所有表必须有主键
- 核心业务字段加NOT NULL
- 外键在开发环境强制使用,生产环境评估
- 定期用脚本检查数据完整性
- 约束命名要规范,方便管理
最后分享一个真实案例:我们有个用户表没加手机号唯一约束,结果出现两个用户绑定同一个手机号。当用户修改密码时,系统随机选了一个账号操作,导致另一个账号被意外锁定。这个故障让我们损失了3个重要客户。从那以后,我对关键字段的约束再也不敢马虎了。
