应遵循范式设计而非面向对象建模:共性字段归users表,权限用role字段或关联表,特殊字段单独扩展;禁用json存关系数据;统一utf8mb4字符集与排序规则;主键优先选业务自然键,复合索引按最左前缀原则设计。

把 mysql 当成对象容器来建表
MySQL 是关系型数据库,不支持类继承、多态或封装。常见误区是照搬 Java 或 Python 的类结构,比如为 User 和 Admin 各建一张表,再加一堆重复字段(name、email、created_at),以为“符合面向对象”。结果导致数据冗余、更新异常、联查变重。
实际应优先用范式设计:提取共性到 users 表,用 role 字段或 user_roles 关联表区分权限;特殊字段(如 admin_secret_key)按需单独建扩展表,而非复制整套结构。
- 避免在多个表中重复定义
updated_at、status、is_deleted等通用字段——它们属于业务语义,不是实体差异 - 不要为每个“子类”新建表并用
type字段模拟多态;联合查询时 MySQL 无法有效利用索引,且union ALL易出错 - 若真需强类型隔离(如
customer和supplier完全无交集),也应通过应用层控制写入路径,而非靠表名区分
用 JSON 字段存本该拆分的关系数据
看到 ORM 支持 JSON 类型,就随手把地址、订单项、标签列表全塞进一个 meta 字段。表面省事,实则破坏可查询性与一致性。
典型错误示例:orders 表里用 items_json 存商品 ID、数量、单价,导致无法按“某商品销量”统计,无法加外键约束,备份/同步时 JSON 格式错误还静默失败。
CREATE table orders ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, items_json JSON NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );
-
JSON字段不能建普通索引;想查“含商品 123 的订单”,只能全表扫描 +JSON_CONTAINS(items_json, '"123"', '$.id'),性能差 - 外键、非空、唯一等约束全部失效;插入非法数据不会报错,直到应用读取时解析失败
- 迁移困难:从 MySQL 5.7 升到 8.0 后,
JSON函数行为有细微变化,历史数据可能解析异常
忽略字符集与排序规则导致的隐式转换
建表时随手写 CHARACTER SET utf8,却没注意 MySQL 的 utf8 实际是 utf8mb3,不支持 emoji 和部分生僻汉字;更糟的是混用 utf8mb4_general_ci 和 utf8mb4_0900_as_cs,让 = 查询忽大忽小、ORDER BY 排序错乱。
常见现象:同一张 users 表,select * FROM users WHERE name = '张三' 有时命中有时不命,日志显示字段值明明是 '张三 '(带空格),但 = 却匹配成功——因为用了 _ci(case-insensitive)且忽略尾部空格。
- 统一使用
CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_cs(MySQL 8.0+)或utf8mb4_unicode_ci(兼容性更好),避免大小写/空格/重音敏感问题 - 连接层(如 JDBC URL)也要显式指定
characterEncoding=utf8mb4,否则客户端传入的 emoji 会变成??? - 已有表修改字符集需同时改列级 collation,仅改表级无效:
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_cs;
主键滥用自增 + 复合索引设计反模式
为图省事,所有表都设 id BIGINT AUTO_INCREMENT PRIMARY KEY,再额外加 UNIQUE(user_id, order_no)。问题在于:业务查询几乎不用 id,却强制走聚簇索引;而真正高频的 WHERE user_id = ? AND status = ? 查询,因索引顺序不对(user_id 在前但 status 未覆盖)导致回表严重。
另一个典型是给 logs 表加 (created_at, level, module) 复合索引,却忘了 created_at 范围查询(BETWEEN)后,后续字段无法走索引——level 和 module 实际未生效。
- 主键优先选业务有意义的自然键(如
order_no),或用BIGINT自增但确保高频查询能利用其顺序性(如分页场景) - 复合索引遵循最左前缀原则:若常查
WHERE a = ? AND b > ? AND c = ?,索引应为(a, b, c),而非(a, c, b) - 对写多读少的表(如日志),考虑用
ARCHIVE引擎或分区表(PARTITION BY RANGE (TO_DAYS(created_at))),别硬扛索引膨胀
最易被忽略的其实是“变更成本”:一张上线三个月的表,哪怕只是把 VARCHAR(255) 改成 VARCHAR(512),在千万级数据上也可能锁表数十秒。设计阶段多花十分钟想清楚字段粒度和关联方式,远比后期加中间层或数据迁移省心。