推荐用 TINYINT 而非 ENUM 存状态,因其扩展性好、避免锁表、ORM 支持佳;须配注释说明值含义,校验旧状态再更新以防止并发覆盖,状态流转逻辑应置于应用层而非 SQL。

状态字段用 TINYINT 还是 ENUM?
直接用 TINYINT 更稳妥。虽然 ENUM 看似语义清晰,但增删状态要 ALTER TABLE,线上表大时会锁表;迁移、备份、ORM 映射也容易出问题。用 TINYINT 配合注释或字典表,扩展性好,且几乎所有 ORM(如 Django 的 choices、MyBatis 的 typeHandler)都原生支持。
- 推荐值范围:0–127(有符号),预留负数做系统保留态(如 -1 表示“已删除”)
- 必须加注释:
COMMENT '0:待提交,1:审核中,2:已通过,3:已拒绝,4:已撤回' - 避免用字符串(如
VARCHAR)存状态——索引效率低、易拼错、大小写敏感
状态变更必须走 UPDATE … WHERE current_status = ?
防止并发覆盖导致状态跳变或丢失。比如用户重复点击“提交审核”,两次请求同时读到 status = 0,都执行 UPDATE ... SET status = 1,结果没问题;但如果逻辑是“仅当 status = 0 才允许改为 1”,就必须显式校验:
UPDATE orders SET status = 1, updated_at = NOW() WHERE id = 123 AND status = 0;
执行后检查 ROW_COUNT() 是否为 1 —— 为 0 说明状态已变,业务应抛异常或返回「操作冲突」。
- 不要用
SELECT + UPDATE两步,无法保证原子性 - WHERE 条件里必须包含当前期望的旧状态,不能只靠主键
- 若需多状态跃迁(如从 0→3),可改用
IN(0,1)或拆成多个判断分支
状态流转规则别硬编码在 SQL 里
把合法转移路径抽成配置或代码逻辑,而不是写一堆 CASE WHEN status IN (0,1) THEN 2 ELSE status END。数据库不擅长表达业务规则,且难以测试和审计。
- 建议在应用层定义状态机:例如用 Python 的
transitions库,或 Java 的spring-statemachine - 关键路径可建一张
state_transition表:from_status,to_status,event,allowed,运行时查表校验 - SQL 层只做最终落库,不承担“能不能转”的决策
历史记录要不要记?怎么记?
除非是金融级审计要求,否则别一上来就搞完整状态日志表。先用单字段 last_status_change_at 和 last_status_by 满足基本追溯;真需要明细,用轻量方案:
- 追加一个
status_log表,字段精简:id、entity_id、entity_type、from_status、to_status、operator_id、created_at - 写入用异步或批量(如每秒合并一次),避免拖慢主流程
- 不外键关联主表,避免级联删除影响主业务;定期按时间分区归档
最常被忽略的是:状态变更往往伴随其他字段更新(如审核人、审核时间、驳回原因),这些字段必须和状态一起在同一个事务里写入,否则会出现「状态已变但原因为空」这类数据不一致。










