好的表设计是高性能查询的基石,而约束是保证数据完整性的第一道防线。
一、为什么表设计如此重要?
想象一下,如果一张表允许「用户名字段为空」且「没有唯一限制」------那你可能会存入两个同名空用户,后续业务逻辑直接崩溃。约束就是数据库的「交通规则」,没有它,数据会变成混乱的垃圾场。
二、6 大约束类型详解
1. NOT NULL --- 非空约束
确保字段不能为 NULL,适用于必有值的列(如用户名、邮箱)。
sql
CREATE TABLE users (
id INT PRIMARY KEY,
username VARCHAR(50) NOT NULL, -- 用户名字段不允许为空
email VARCHAR(100) NOT NULL
);
常见坑: 空字符串 '' 不等于 NULL,NOT NULL 只禁止 NULL,不禁止空字符串。
2. UNIQUE --- 唯一约束
保证列(或列组合)的值全局唯一。
sql
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(100) UNIQUE, -- 每个邮箱只能注册一次
phone VARCHAR(20) UNIQUE
);
多列联合唯一:
sql
CREATE TABLE follows (
follower_id INT,
followee_id INT,
UNIQUE (follower_id, followee_id) -- 不能重复关注同一个人
);
3. PRIMARY KEY --- 主键约束
= NOT NULL + UNIQUE,每张表有且仅有一个主键,作为行的唯一标识。
sql
-- 单列主键
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT NOT NULL,
amount DECIMAL(10,2)
);
-- 复合主键(联合主键)
CREATE TABLE order_items (
order_id INT,
product_id INT,
quantity INT,
PRIMARY KEY (order_id, product_id) -- 一个订单中的每个产品唯一
);
性能提示: 自增整数主键在 InnoDB 中性能最佳(聚簇索引按主键顺序排列)。
4. FOREIGN KEY --- 外键约束
保证引用完整性------子表中的值必须在父表存在。
sql
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT,
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE CASCADE -- 删除客户时自动删除其订单
ON UPDATE CASCADE -- 客户 ID 更新时同步更新
);
ON DELETE / ON UPDATE 选项:
| 选项 | 行为 |
|---|---|
CASCADE |
父表变更时,子表自动同步 |
SET NULL |
父表删除时,子表外键置为 NULL |
RESTRICT |
有子记录时禁止删除父记录(默认) |
NO ACTION |
同 RESTRICT(MySQL 下等同) |
⚠️ 生产建议: 高并发场景下外键会带来性能开销,许多团队选择在应用层保证引用完整性,数据库只用索引。
5. CHECK --- 检查约束
确保字段值满足指定条件。
sql
CREATE TABLE products (
id INT PRIMARY KEY,
price DECIMAL(10,2) CHECK (price > 0), -- 价格必须为正
stock INT CHECK (stock >= 0), -- 库存不能为负
status VARCHAR(20) CHECK (status IN ('上架', '下架', '预售'))
);
注意: MySQL 8.0.16+ 才开始真正支持 CHECK(之前版本语法通过但不生效)。
6. DEFAULT --- 默认值约束
为字段指定默认值,插入时若未提供则自动填充。
sql
CREATE TABLE articles (
id INT PRIMARY KEY,
title VARCHAR(200) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 默认当前时间
view_count INT DEFAULT 0, -- 默认阅读数为0
is_published TINYINT DEFAULT 0 -- 默认未发布
);
三、表设计最佳实践
1. 范式化 vs 反范式化
三范式(3NF)核心原则:
| 范式 | 规则 | 反例 |
|---|---|---|
| 1NF | 每列不可再分 | 一个字段存多个手机号 '138xxx,139xxx' |
| 2NF | 非主键列完全依赖主键 | 订单表存了 customer_name(应只存 customer_id) |
| 3NF | 非主键列不传递依赖 | 存了 customer_city 而非通过 customer_id 关联 |
何时反范式化? 查询频繁、写入少的场景,适当冗余可减少 JOIN,提升查询速度。
2. 选择合适的数据类型
基本原则:够用就好,越小越快。
sql
-- ❌ 过度设计
CREATE TABLE bad_design (
id BIGINT PRIMARY KEY, -- 实际只有几百条数据
is_active CHAR(10) DEFAULT 'true', -- 用 CHAR(10) 存布尔值
price VARCHAR(20) -- 金额用字符串存
);
-- ✅ 合理设计
CREATE TABLE good_design (
id INT PRIMARY KEY AUTO_INCREMENT, -- 数据量不大用 INT 足够
is_active TINYINT DEFAULT 1, -- 布尔值用 TINYINT(1)
price DECIMAL(10,2) -- 金额用 DECIMAL 避免精度丢失
);
3. 索引设计原则
- 主键自动成为聚簇索引,InnoDB 表必须有主键
- 经常出现在
WHERE/JOIN/ORDER BY的列考虑加索引 - 联合索引遵循最左前缀原则 :索引
(a, b, c)能加速WHERE a=?、WHERE a=? AND b=?,但无法加速WHERE b=? - 不要对频繁更新的列加过多索引(维护成本高)
四、实战案例分析
场景:设计一个电商订单系统
sql
-- 用户表
CREATE TABLE customers (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
phone VARCHAR(20),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_email (email) -- 加速登录查询
);
-- 商品表
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(200) NOT NULL,
price DECIMAL(10,2) NOT NULL CHECK (price > 0),
stock INT NOT NULL DEFAULT 0 CHECK (stock >= 0),
status ENUM('上架', '下架') DEFAULT '上架',
INDEX idx_status (status)
);
-- 订单表
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
customer_id INT NOT NULL,
total_amount DECIMAL(10,2) DEFAULT 0.00,
status ENUM('待支付', '已支付', '已发货', '已完成', '已取消') DEFAULT '待支付',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE RESTRICT,
INDEX idx_customer (customer_id),
INDEX idx_status_created (status, created_at) -- 联合索引加速常见查询
);
-- 订单明细表
CREATE TABLE order_items (
id INT PRIMARY KEY AUTO_INCREMENT,
order_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL CHECK (quantity > 0),
price DECIMAL(10,2) NOT NULL, -- 快照价格,不受商品改价影响
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT,
UNIQUE (order_id, product_id) -- 同一订单中每种商品只出现一次
);
五、常见设计陷阱
| 陷阱 | 问题 | 正确做法 |
|---|---|---|
| 万能字段 | 一个字段存多个值(JSON/逗号分隔) | 拆成关联表 |
| 过度索引 | 给表建了 20 个索引,插入极慢 | 只给查询字段建索引 |
| 不用外键 | 数据不一致,出现孤儿记录 | 应用层保证或加外键 |
| 类型滥用 | VARCHAR(255) 存所有文本 |
按实际长度选择 CHAR/VARCHAR/TEXT |
| 缺少时间戳 | 不知道数据何时插入/更新 | 加 created_at / updated_at |
总结
- 约束:NOT NULL、UNIQUE、PRIMARY KEY、FOREIGN KEY、CHECK、DEFAULT
- 设计三范式:原子性、完全依赖、不传递依赖
- 反范式化:用空间换时间,适合读多写少场景
- 数据类型:选小不选大,选对不选贵
- 索引:最左前缀、适度索引、主键必设
好的表设计,能让后续的查询优化省 80% 的力气。花时间在表设计上,是性价比最高的投入。