易君召
发布于 2026-08-07 / 作者:易君召 / 0 阅读
0

数据库设计约束优化使用建议

约束用于保证数据完整性(实体完整性、参照完整性、域完整性、业务完整性),不是越多越好,约束会带来写入开销,不合理使用会造成锁等待、慢 DML、业务报错难以排查。下面分主键、唯一、外键、非空、check、默认值,给出选型、避坑、最佳实践。

1. 主键约束 PRIMARY KEY

实体完整性,一张表只能一个主键,隐含非空 + 唯一索引。

✅ 推荐做法

  1. 优先使用无业务含义代理主键(自增 ID、雪花 ID、UUIDv4),不要用业务字段做主键(手机号、身份证、订单编号)。业务字段会更新,更新主键会连带更新二级索引,代价极高。

  2. InnoDB 主键尽量用整型有序主键,减少页分裂,提升写入性能。

  3. 主键必须非空,不要再额外加NOT NULL,主键自带非空。

  4. 避免复合主键,复合主键会让所有二级索引带上全部主键字段,索引膨胀。

❌ 不推荐

  • 业务字段作为主键;大字符串做主键;超长复合主键。

2. 唯一约束 UNIQUE

保证字段 / 组合不重复,底层会创建唯一索引。

✅ 推荐

  1. 业务唯一性(手机号、邮箱)使用UNIQUE约束,防止业务层漏校验产生脏数据。

  2. 多字段唯一,使用组合唯一约束,而不是程序层判断。

  3. 注意:MySQL 中UNIQUE允许多个 NULL,NULL 不等于 NULL;如果业务要求不能有空,需要搭配NOT NULL

⚠️ 坑点

  1. 唯一索引在大量并发插入重复数据时,会产生锁等待、死锁,高并发场景不能完全依赖唯一约束做防重,业务层先做幂等。

  2. 不要滥用大量唯一约束,每个唯一约束对应一个索引,增大写开销。

高并发场景:业务幂等优先,约束做兜底,不要把约束当主逻辑

3. 外键约束 FOREIGN KEY(参照完整性)

保证关联数据一致性,生产环境很多业务会禁用数据库外键,改为应用层维护

✅ 适合使用外键场景

  • 内部小系统、数据量不大、并发不高,希望数据库强制保证参照完整性,防止脏数据。

  • 测试库、中台元数据表。

❌ 不建议开启外键场景(互联网高并发业务)

  1. 外键会触发级联锁,DML 操作父 / 子表会互相加锁,极易死锁、锁等待;

  2. 批量删除、批量更新性能差;

  3. 分库分表之后数据库外键完全失效;

  4. 迁移、数据同步、备份恢复容易受外键约束报错。

最佳实践:高并发、分表项目关闭数据库外键,在应用层实现参照校验;外键仅做低并发库强约束

如果必须使用外键:谨慎使用ON DELETE CASCADE级联删除,级联删除大数量会锁表,优先用ON DELETE SET NULL

4. 非空约束 NOT NULL(域完整性)

✅ 推荐

  1. 业务上永远不会为空的字段,强制设置NOT NULL,配合DEFAULT默认值。

  2. 避免大量业务逻辑去判断IS NULL,简化 SQL,同时索引效率更好。

  3. 字符串类型不要用NULL代表空业务含义,用空字符串''

⚠️ 避坑

  1. 已有大表增加NOT NULL约束,MySQL 5.7/8.0 部分版本会锁表;大表 DDL 用 Online DDL 工具。

  2. 不要无脑全部加 NOT NULL;允许业务为空的字段保留 NULL。

  3. NULL 不能用=判断,只能IS NULL / IS NOT NULL,容易写出错误 SQL。

5. CHECK 约束(业务域校验)

MySQL8.0 才完整支持 CHECK,5.7 语法兼容但不生效。

✅ 使用建议

  1. 用于简单常量规则:状态取值范围、数值范围,作为兜底校验。

CHECK(status IN(0,1,2)), CHECK(amount >=0)
  1. 复杂业务逻辑不要交给 CHECK,复杂判断放应用层。

❌ 不适合

  • 需要关联其他表、子查询的校验,CHECK 做不到,只能应用层实现。

6. DEFAULT 默认值约束

✅ 最佳实践

  1. 配合NOT NULL使用,避免插入时漏写字段报错。

  2. 时间字段:创建时间DEFAULT CURRENT_TIMESTAMP,更新时间DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

  3. 数值默认 0,字符串默认'',尽量避免默认 NULL。

⚠️注意

  • 默认值仅在插入不指定该字段才生效;update 设置 null 会覆盖默认值。

7. 约束整体通用优化原则

7.1 分层思想:约束是兜底,不是业务主逻辑

  1. 第一层:应用层校验(优先) 参数校验、幂等、业务规则;

  2. 第二层:数据库约束兜底,防止 bug、人工导入数据产生脏数据;

不能反过来:不要把全部业务规则丢给数据库约束,会严重影响并发性能。

7.2 性能权衡

所有约束本质上:写入 (INSERT/UPDATE/DELETE) 需要额外校验,牺牲写性能换取读的数据正确性

  • OLTP 高并发业务:精简约束,优先主键、必要的 NOT NULL、少量 UNIQUE;谨慎外键、少复杂 CHECK。

  • OLAP 数仓:可以大量约束,读多写少,数据质量优先。

7.3 约束与索引关系

  • PRIMARY KEY、UNIQUE 自动创建索引;

  • FOREIGN KEY 要求外键字段必须有索引,否则会锁表;

新增约束时要关注索引数量,索引过多拖慢写入。

7.4 迁移、分库分表场景约束注意

  1. 分库分表后:外键、跨库唯一约束数据库层面全部失效,全部交由应用层;

  2. 数据导入、同步工具(datax、canal)要兼容约束,否则同步报错。

7.5 异常可观测

约束报错(唯一冲突、外键报错、check 失败)要捕获数据库异常码,不要只返回笼统报错给前端;便于定位是业务 bug 还是重复请求。

8. 选型速查表

约束

作用

适合场景

不适合场景

PRIMARY KEY

实体唯一非空

每张表必选,代理主键

业务字段做主键

UNIQUE

字段唯一

手机号、编码等业务唯一

超高并发大量重复写入场景

FOREIGN KEY

关联参照完整

小系统、元数据、低并发

高并发、分库分表

NOT NULL

禁止空值

业务确定不为空字段

允许为空业务字段

CHECK

字段值范围校验

简单常量范围校验

跨表、复杂业务逻辑

DEFAULT

字段默认值

配合 NOT NULL

动态业务计算值

9. 落地实践示例(MySQL8.0)

CREATE TABLE `user` (
  `id` BIGINT NOT NULL COMMENT '代理主键,雪花id',
  `phone` VARCHAR(11) NOT NULL COMMENT '手机号,业务唯一',
  `user_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '用户名',
  `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态0正常1禁用',
  `balance` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '余额',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_phone` (`phone`),
  CHECK (`status` IN (0,1)),
  CHECK (`balance` >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

没有外键;简单 check 做兜底;非空 + 默认值;业务唯一使用唯一索引;代理主键。