设计选择带来的必然代价
这套系统的关联方式是"业务号对业务号":订单明细通过 order_biz_no 关联订单,结算单通过 order_biz_no 关联订单,资金流水通过 biz_no 关联业务单。好处很多——跨库、分表、数据复制时不会因自增 ID 变化而断链。
但有一个必然代价:每次关联查询都落在业务号列上。如果这一列没有索引,关联就是全表扫描,而且随着数据量增长会指数级变慢。
自增主键有 MySQL 自动建的索引,业务号没有。这就是那次索引体检的出发点。
体检怎么做
不靠肉眼看 DDL,而是把"引用关系"和"索引现状"放到一起比对:
- 从
information_schema.COLUMNS里取出所有以_biz_no结尾的列,得到"可能被用作外键的列"清单; - 从
information_schema.STATISTICS里取出每张表已有的索引及列顺序; - 求差集:出现在业务表里、但没有任何索引以它为最左列的
*_biz_no列; - 对高频查询做
EXPLAIN抽样验证,确认确实走的是全表扫描而不是别的索引。
第 3 步是关键:索引存在但不在最左列,等于没用。idx(status, order_biz_no) 无法支撑只按 order_biz_no 的查询。
结果:补了 142 个索引
按域分布补齐,重点在三个地方:
| 域 | 典型列 | 说明 |
|---|---|---|
| 供应链 | order_biz_no、supplier_biz_no、product_biz_no、sku_biz_no | 订单详情、结算、库存流水的高频关联 |
| 资金 | merchant_biz_no、bill_biz_no、source_no | 对账与结算查询 |
| 系统与分析 | user_biz_no、ref_biz_no | 操作日志、埋点与业务单反查 |
原则是只补引用列,不动已有复合索引。复合索引的列顺序往往服务于特定查询,随意调整会让另一条查询变慢。
唯一约束:哪些列必须加
索引解决"快不快",唯一约束解决"对不对"。业务号列里必须加唯一约束的有两类:
- 单据主表的 biz_no:订单、结算单、账单、回款,业务号重复意味着同一笔业务被记录了两次,这必须由数据库拦住;
- 日志类表的 biz_no:即便日志是追加写,业务号重复也说明写入逻辑有 bug,早暴露比晚暴露好。
这次补齐的两张表属于第二类。补之前先做了一次查重,确认存量数据没有重复值——加唯一约束前不查重,等于赌数据是干净的。
索引不是越多越好
补索引的同时也守了两条边界:
- 不给低基数列建单列索引。
status(只有几个值)、is_deleted(0/1)单独建索引意义不大,应该做成复合索引的次列; - 不为"可能会查"建索引。每个索引都要占写入成本,只有真实查询场景支撑才建。
这两条在数据量小的时候看不出差别,等单表到百万级,写入放大会非常明显。
沉淀成检查项
这次整改之后,索引检查变成了固定流程:新增表必查引用列是否建索引,新增唯一约束前必查重。数据库的问题不会自己消失,只会等到数据量足够大时集中爆发。