外键字段命名必须统一为{referenced_table}_id格式并立即加FOREIGN KEY约束,一对多只在“多”侧设外键,JSON仅用于不参与查询的动态元数据,迁移时须先删外键再操作。
外键字段命名不统一,导致 JOIN 时反复查文档
数据库里加关联,最常踩的坑不是不会加,而是加完自己都认不出哪个字段连哪个表。比如 user_id 在订单表里指 users.id,但在日志表里却指向 admins.id,没注释、没约束,后期查问题全靠猜。
实操建议:
- 外键字段名必须严格遵循
{referenced_table}_id格式,例如关联到products表就叫product_id,不能简写成pid或混用item_id - 建表时立刻加
FOREIGN KEY约束,别等上线后补——MySQL 8.0+ 和 PostgreSQL 都支持延迟检查,但没约束的字段等于裸奔 - 用
COMMENT注明关联意图,例如product_id INT COMMENT 'foreign key to products.id',很多 ORM(如 Django 的db_column)会读这个注释
一对多关系误用双向外键,引发循环依赖
常见错误:在 orders 表加了 user_id,又反向在 users 表加个 last_order_id,以为能快速查最新订单。结果插入新订单时要先更新用户表,事务一卡,死锁预警。
实操建议:
- 一对多关系只在「多」侧设外键,即只有
orders.user_id,不要在users表存任何指向子表的 ID - 需要「查用户最新订单」这种场景,用视图或查询时
ORDER BY created_at DESC LIMIT 1,而不是靠冗余字段保一致性 - 如果真有强性能要求(比如首页千次/秒查最新订单),用单独的缓存表(如
user_latest_order),但必须配套触发器或应用层双写逻辑,不能靠手动维护
JSON 字段滥用替代关联表,后期无法索引和 JOIN
看到「一个用户有多个地址」就直接上 addresses JSON,初期开发快,半年后业务要按城市统计用户数、要和 cities 表联查行政区划——全卡住。JSON 字段里存的数据,MySQL 5.7+ 虽支持 -> 提取,但没法建普通索引,更没法 JOIN。
实操建议:
- 只要数据有独立业务含义(比如地址、标签、配置项)、需要被查询/筛选/聚合,就拆成独立表,哪怕只有两三个字段
- JSON 只用于真正动态、不可枚举、不参与关联查询的元数据,例如
settings JSON COMMENT 'user-specific UI preferences' - PostgreSQL 的
jsonb支持 GIN 索引,但前提是明确路径,比如CREATE INDEX ON users USING GIN ((data->'profile'->>'theme'));MySQL 的 JSON 索引能力弱得多,慎用
迁移脚本没处理外键约束,导致线上执行失败
本地跑通的 ALTER TABLE orders ADD COLUMN product_id INT,上线时直接报错:ERROR 1832 (HY000): Cannot change column 'product_id': used in a foreign key constraint。因为目标列已被其他表引用,MySQL 不允许直接改类型或删列。
实操建议:
- 所有涉及外键的 DDL(增删改列、删表、改类型),必须先
DROP FOREIGN KEY,再操作,最后重建——别指望 ORM 迁移工具自动处理这个 - 用
SHOW CREATE TABLE orders查当前外键名,它通常长得像fk_orders_product_id_products_id,DROP FOREIGN KEY fk_orders_product_id_products_id才生效 - 生产环境执行前,在从库或影子库上完整跑一遍迁移链,外键相关操作最容易在大表上锁超时,别等主库堵住才发觉
关联关系不是加完外键就结束的事。字段命名、约束时机、数据形态选择、迁移顺序,每一步都在悄悄决定半年后的排查成本。最麻烦的往往不是语法错误,而是当时觉得“先这么着吧”的那个小妥协。










