3. 数据库约束:主键约束、外键约束、唯一约束、检查约束、默认值约束
约束这玩意儿,说白了就是数据库的「守门员」。我干了十几年运维,见过太多因为约束没设计好而引发的数据灾难。今天咱们就把五种核心约束掰开揉碎讲清楚。
核心观点:约束不是开发者的枷锁,而是数据质量的保险。没有约束的数据库,就像没有护栏的悬崖。
3.1 主键约束:数据的身份证
主键约束,我习惯叫它「数据的身份证」。每个表必须有且只有一个主键,它保证每一行都能被唯一识别。
你想想看,如果没有主键,你怎么精确找到某条记录?靠行号?那玩意儿一删除数据就变了。
我的习惯:主键尽量用自增整数或UUID,别用业务字段。比如身份证号看起来唯一,但万一政策变了呢?我曾经有个客户把手机号当主键,结果携号转网政策一出,整个系统差点重写。
-- 创建带主键的表
CREATE TABLE users (
user_id INT AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(100),
PRIMARY KEY (user_id)
);
-- 复合主键示例
CREATE TABLE order_items (
order_id INT,
product_id INT,
quantity INT,
PRIMARY KEY (order_id, product_id)
);
注意:复合主键的列顺序会影响索引效率。高频查询的列放前面,这是我踩过的坑。
3.2 外键约束:表与表的「血缘关系」
外键约束维护的是引用完整性。说白了,就是保证子表里的数据,在父表里确实存在。
我记得有一次接手一个电商系统,订单表里有个用户ID,但用户表里根本查不到这个人。这就是没加外键约束的后果——数据成了「孤儿」。
-- 创建带外键的表
CREATE TABLE orders (
order_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
order_date DATE,
FOREIGN KEY (user_id) REFERENCES users(user_id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
| 外键行为 | 说明 | 使用场景 |
|---|---|---|
| CASCADE | 父表删除/更新时,子表同步 | 强关联数据,如订单明细 |
| SET NULL | 父表删除时,子表置为NULL | 可选关联,如用户注销后保留订单 |
| RESTRICT | 有子记录时禁止删除父记录 | 关键数据保护,如财务记录 |
| NO ACTION | 类似RESTRICT,检查时机不同 | 某些数据库默认行为 |
避坑指南:我曾经在生产环境直接用了CASCADE,结果删一个用户,连带删了三万条订单。从那以后,我建议先用SET NULL或RESTRICT,除非你非常确定业务逻辑。
3.3 唯一约束:拒绝重复,但允许一个空
唯一约束保证列值不重复,但允许一个NULL值。嗯,这里要注意——不同数据库对NULL的处理有差异。
MySQL里,唯一索引允许多个NULL。但SQL Server早期版本只允许一个。你想想看,如果业务上要求「邮箱唯一」,但允许用户不填邮箱,那NULL的处理就很重要。
-- 单列唯一约束
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
email VARCHAR(100) UNIQUE,
phone VARCHAR(20)
);
-- 复合唯一约束
CREATE TABLE product_warehouse (
product_id INT,
warehouse_id INT,
quantity INT,
UNIQUE KEY (product_id, warehouse_id)
);
关键区别:主键 vs 唯一约束
- 主键:一个表只能有一个,不允许NULL
- 唯一约束:一个表可以有多个,允许一个NULL
- 主键自动创建聚集索引(MySQL InnoDB),唯一约束创建非聚集索引
3.4 检查约束:给数据加个「安检门」
检查约束,说白了就是给列值加个规则。比如年龄不能为负,工资不能低于最低标准。
我见过最离谱的案例:有个系统的「性别」字段存了「外星人」。为什么?因为没加检查约束,前端验证被绕过了。
-- 基本检查约束
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
age INT CHECK (age >= 18 AND age <= 65),
salary DECIMAL(10,2) CHECK (salary > 0),
gender CHAR(1) CHECK (gender IN ('M', 'F', 'O'))
);
-- 命名检查约束(方便后续维护)
CREATE TABLE products (
product_id INT PRIMARY KEY,
price DECIMAL(10,2),
discount DECIMAL(10,2),
CONSTRAINT chk_price CHECK (price > 0),
CONSTRAINT chk_discount CHECK (discount >= 0 AND discount <= price)
);
注意:MySQL 8.0.16之前,检查约束只是语法支持,实际不生效。我有个朋友升级数据库后才发现,之前「生效」的检查约束全是假的。升级后一堆脏数据冒了出来。
3.5 默认值约束:偷懒的艺术
默认值约束,就是当插入数据没指定某列时,数据库自动填一个值。这玩意儿用好了能省不少事。
我个人习惯,时间字段几乎必加默认值。比如创建时间用 CURRENT_TIMESTAMP,更新时间用 CURRENT_TIMESTAMP ON UPDATE。这样代码里少写一行是一行。
-- 默认值约束示例
CREATE TABLE articles (
article_id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(200) NOT NULL,
status VARCHAR(20) DEFAULT 'draft',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
view_count INT DEFAULT 0
);
-- 插入测试
INSERT INTO articles (title) VALUES ('数据库约束详解');
-- 此时 status='draft', created_at=当前时间, view_count=0
我的经验:默认值别设成业务相关的硬编码。比如「状态默认是待审核」,万一哪天业务改了,改默认值可能影响历史数据。我一般用「未知」或「未设置」这类中性值。
3.6 约束的日常维护与检查
约束建好了不代表万事大吉。我每周的巡检脚本里,一定会检查约束状态。
-- MySQL 查看表约束
SELECT
TABLE_NAME,
CONSTRAINT_NAME,
CONSTRAINT_TYPE
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = 'your_database';
-- 检查外键是否被禁用
SELECT
TABLE_NAME,
CONSTRAINT_NAME,
REFERENCED_TABLE_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE REFERENCED_TABLE_SCHEMA = 'your_database'
AND REFERENCED_TABLE_NAME IS NOT NULL;
| 巡检项 | 检查频率 | 异常处理 |
|---|---|---|
| 主键是否缺失 | 每周 | 立即添加,评估业务影响 |
| 外键是否失效 | 每日 | 检查数据一致性,重建约束 |
| 唯一约束是否被违反 | 实时(监控告警) | 查找重复数据,清理后重建 |
| 检查约束是否被绕过 | 每月 | 全表扫描,修复异常数据 |
总结一下:约束是数据完整性的第一道防线。主键保实体,外键保引用,唯一防重复,检查控范围,默认省麻烦。五者配合,才能构建一个健壮的数据库体系。
嗯,关于约束就讲这么多。记住一点:约束不是写完就完事了,要定期检查、持续优化。我见过太多系统,上线时约束齐全,三年后约束全被「临时」禁用了——然后数据就乱了。