1. 数据库约束的本质与价值
刚入行的开发同事老张最近踩了个坑——他们业务系统出现了大量脏数据:用户表里冒出一堆空手机号,订单表里出现了负数的金额,还有一堆重复的身份证号。排查到最后发现,这些本该在数据库层面就被拦截的非法数据,竟然全都成功写入了。问题的根源很简单:建表时没加约束。
数据库约束就像交通规则中的红绿灯,它能在数据写入前就进行合法性校验。我经手过的金融系统中,约束条件曾拦截过90%以上的异常数据。今天我们就深入聊聊MySQL中那些守护数据安全的"门神"们。
2. 六大约束类型详解
2.1 非空约束:数据完整性的第一道防线
创建用户表时最该先考虑的就是NOT NULL约束。去年我们系统升级时,就因漏掉这个约束导致用户注册流程崩溃:
sql复制CREATE TABLE users (
id INT PRIMARY KEY,
username VARCHAR(50) NOT NULL, -- 必须填写
phone CHAR(11) NOT NULL, -- 不允许NULL值
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
关键经验:所有业务关键字段都应设为NOT NULL。我们曾遇到DEFAULT NULL的字段被误认为有值,导致统计报表严重偏差。特别要注意的是,NOT NULL字段在INSERT时必须显式赋值或用DEFAULT约束。
2.2 唯一约束:杜绝重复数据的银弹
电商平台的SKU编码、用户的身份证号这些需要绝对唯一的字段,必须上UNIQUE约束。去年双十一大促时,有个商品因编码重复导致库存统计出错:
sql复制CREATE TABLE products (
id INT PRIMARY KEY,
sku_code VARCHAR(20) UNIQUE, -- 唯一索引
product_name VARCHAR(100)
);
-- 插入重复值会报错
INSERT INTO products VALUES(1, 'A001', '手机');
INSERT INTO products VALUES(2, 'A001', '耳机'); -- 报错:Duplicate entry
避坑指南:UNIQUE约束实际是通过创建唯一索引实现的。对于大表要评估性能影响,我们有个千万级用户表加UNIQUE后写入性能下降了15%。复合唯一约束的语法是UNIQUE(field1, field2)。
2.3 主键约束:数据的身份证系统
主键是表的灵魂所在。我们踩过最痛的坑就是使用业务字段做主键,结果业务规则变更导致大规模数据迁移:
sql复制-- 推荐做法:自增ID主键
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY, -- 代理主键
order_no VARCHAR(20) UNIQUE, -- 业务编号
user_id INT NOT NULL
);
-- 反面教材:用手机号做主键
CREATE TABLE bad_design (
phone CHAR(11) PRIMARY KEY, -- 手机号可能变更
name VARCHAR(20)
);
架构建议:永远使用无意义的自增ID作主键(代理主键),业务编号用UNIQUE约束即可。我们金融系统采用BIGINT避免溢出,分布式系统可以用雪花ID。
2.4 默认约束:给字段装上安全气囊
DEFAULT约束是保证数据一致性的隐形守护者。我们物流系统曾因缺少DEFAULT导致未支付订单显示为NULL:
sql复制CREATE TABLE orders (
id INT PRIMARY KEY,
status TINYINT DEFAULT 0, -- 0=待支付
pay_amount DECIMAL(10,2) DEFAULT 0.00,
address VARCHAR(200) DEFAULT '未填写'
);
实战技巧:DEFAULT值要符合业务语义。我们曾用DEFAULT NULL导致代码中到处都是null检查。建议数值型默认0,字符串默认空串,布尔默认false。
2.5 检查约束:数据校验的瑞士军刀
MySQL 8.0终于支持CHECK约束了!我们立即用它替换了应用层的校验逻辑:
sql复制CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(20),
age INT CHECK (age >= 18), -- 年龄校验
salary DECIMAL(10,2) CHECK (salary > 0),
gender ENUM('M','F') CHECK (gender IN ('M','F'))
);
-- 违反约束的插入
INSERT INTO employees VALUES(1, '张三', 16, 5000, 'M'); -- 报错:Check constraint violated
版本注意:5.7及以下版本CHECK约束只语法支持但不生效。我们通过触发器实现了类似功能,但维护成本很高。
2.6 外键约束:表关系的法律文书
外键是维护数据完整性的核武器,但我们支付系统曾经因为它导致级联锁表:
sql复制CREATE TABLE departments (
dept_id INT PRIMARY KEY,
dept_name VARCHAR(50)
);
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
dept_id INT,
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
ON DELETE RESTRICT -- 禁止删除有员工的部门
ON UPDATE CASCADE -- 部门ID更新时自动同步
);
血泪教训:高并发系统慎用外键!我们遇到过级联更新导致的死锁。建议在应用层实现关联逻辑,或者使用ON DELETE SET NULL等非阻塞策略。
3. 约束的组合使用实战
3.1 多约束联合防御
银行账户表需要多重防护:
sql复制CREATE TABLE bank_accounts (
account_id VARCHAR(20) PRIMARY KEY,
user_id INT NOT NULL,
balance DECIMAL(15,2) NOT NULL DEFAULT 0.00 CHECK (balance >= 0),
status ENUM('active','frozen') NOT NULL DEFAULT 'active',
UNIQUE (user_id, account_id), -- 复合唯一约束
FOREIGN KEY (user_id) REFERENCES users(id)
);
3.2 约束与索引的配合
我们发现UNIQUE和PRIMARY KEY会自动创建索引,合理利用可以提升查询性能:
sql复制-- 查看约束创建的索引
SHOW INDEX FROM products;
-- 输出示例:
+----------+------------+-----------+--------------+-------------+-----------+...
| Table | Key_name | Seq_in_index | Column_name | Collation | Cardinality |...
+----------+------------+-----------+--------------+-------------+-----------+
| products | PRIMARY | 1 | id | A | 0 |...
| products | sku_code | 1 | sku_code | A | 0 |...
4. 约束管理进阶技巧
4.1 后期添加约束的注意事项
给已有表加约束可能失败,我们总结了一套安全流程:
sql复制-- 1. 先检查现有数据是否合规
SELECT COUNT(*) FROM users WHERE phone IS NULL;
-- 2. 添加约束(MySQL会扫描全表)
ALTER TABLE users MODIFY phone VARCHAR(20) NOT NULL;
-- 3. 大表建议用pt-online-schema-change工具
4.2 约束命名规范
给约束显式命名便于后续管理:
sql复制CREATE TABLE tickets (
id INT,
user_id INT,
CONSTRAINT pk_tickets PRIMARY KEY (id),
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT chk_price CHECK (price > 0)
);
-- 修改约束时需要名字
ALTER TABLE tickets DROP CONSTRAINT chk_price;
5. 生产环境避坑指南
-
性能权衡:我们核心交易表去掉了所有外键,通过应用事务保证一致性,QPS提升了30%
-
错误处理:捕获约束违反错误码:
- 1062:Duplicate entry(唯一约束冲突)
- 1048:Column cannot be null(非空约束)
- 4025:Check constraint violated(检查约束)
-
数据迁移:用SET FOREIGN_KEY_CHECKS=0临时关闭外键检查
-
版本差异:
- MySQL 5.7:检查约束不生效
- MariaDB 10.2+:支持所有约束类型
-
ORM适配:Hibernate配置中需要显式声明约束注解,否则DDL生成可能不完整