追溯系统做反向查询是常见场景:按成品序列号反查经历了哪些工序、哪些物料批次、谁在哪个工位操作。业务量上去后,反查页面经常要等十几秒甚至超时。很多人第一反应是把关联字段都加上索引,结果索引越建越多、写入越来越慢,反查也没实质变快。真正能把这类查询压到可接受范围的做法,通常是在 MySQL 联合索引与冗余快照表之间做取舍,而不是把两种手段混在一起。
追溯反查慢,慢在两个地方。第一是查询路径长。典型的反查要跨主表(成品/批次)、工序记录表、物料投入表、人员/设备表,往往还要带时间范围和状态过滤,JOIN 数量多、每步返回行数都不小。第二是选择性差。按序列号反查本身选择性高,但一加上"某段时间内""某种缺陷代码""某个供应商批次"这类条件,优化器很容易放弃原本的主索引,转而走范围扫描或排序,出现 Using filesort 和临时表。
联合索引能做什么,取决于查询条件是否走覆盖。追溯反查的典型写法是按序列号 + 时间范围 + 状态过滤,再按工序时间排序,取前 N 条。这类查询适合在前置表上建 (seq_no, status, created_at) 的联合索引,查询走等值 + 范围,排序利用索引顺序避免 filesort。很多项目还有一类"缺陷代码反查":给定缺陷类型和供应商,列出涉及的成品序列号。这类查询的选择性来自缺陷代码和供应商组合,单独按缺陷代码的索引返回量很大,按供应商又太大,组合起来才有用,所以适合 (defect_code, supplier_id, record_date) 的联合索引,并且把 seq_no 也放进索引实现覆盖扫描,直接避免回表。
联合索引的局限同样明确。第一,列顺序一旦定死,换一种反查口径就可能失效。比如把"缺陷代码+供应商"改成"供应商+时间+状态",原本 (defect_code, supplier_id, record_date) 就走不上了,要另外补索引。第二,每多一个联合索引,INSERT/UPDATE 都要多写一份 B+ 树,追溯系统写入频率高,索引数量上去后写入延迟和锁竞争会明显体现出来。第三,MySQL 5.7/8.0 在 range + sort 场景下有时仍会选 filesort,优化器选错的时候,EXPLAIN 看到的 rows 估算值和实际返回差距很大,索引建了也不起作用。
冗余快照表的核心思路是用空间换查询复杂度。追溯场景里,一个成品从上线到完工要写很多条工序记录、物料投入记录和不良记录。在每条记录写入事务中,同步(或异步批量)往一张追溯快照表写入一行:固定包含成品序列号、工序/物料/不良类型、关键业务字段(供应商、缺陷代码、班次、时间等)以及一条指向原始表的 source_record_id。快照表本身不再 JOIN 原始业务表,查询只在这张宽表上做等值或范围过滤,结构简单、命中率高。
冗余快照表也带来代价,必须正面考虑。第一是冗余一致性。原始业务数据修正或补录时,快照必须同步修正,否则反查结果会出现"快照说有、原始表没有"或反之的差异。比较稳健的做法是快照只增不改:业务记录修正通过新增修正记录解决,快照追加修正版本;这样查询侧只要按 version/is_latest 过滤即可,无需维护快照的 UPDATE 一致性。第二是存储与写入放大。快照把多个表的字段铺平,单行体积变大;如果每条工序/物料/不良都产生一行快照,写入量会成倍增加。常见折中是按反查维度聚合:只拍"序列号+关键检查点"的快照(比如首末工序、关键质检点、供应商交接点),中间过程仍走原始表,把快照控制在一个可接受的数量级。第三是数据同步链路。实时拍快照意味着业务写入链路变重,很多项目改用异步:原始表变更入 Binlog/CDC 流水,消费者写到快照表。这能降低写延迟,但引入延迟窗口和消费失败重试,上线时要用对比脚本确保快照与源表在延迟窗口外的行数一致。
取舍的判断清单可以直接落到决策上。第一,反查口径稳定、维度和字段变化频率低,且跨表 JOIN 是性能瓶颈,快照优先。第二,反查口径经常变、临时加维度的需求多,快照表列一增再增维护成本高,联合索引优先。第三,写入吞吐高、修正/补录频繁,快照必须配合"只增不改 + 版本控制"才划算,否则索引方案更安全。第四,实时性要求高且能接受写放大,实时快照可行;能接受秒到分钟级延迟,CDC 异步快照更稳。第五,单成品工序链长度稳定(几十到一百多条),联合索引加覆盖扫描往往够用;单成品链长可达上千且反查还要带多条件过滤,快照的优势更明显。两种手段并非互斥,但建议先选一个主路径,避免同时维护联合索引和快照导致"两边都沾、哪边都不彻底"。
落地有三个常见坑。第一,快照字段"尽量齐全"导致表越来越宽:应只保留反查页面实际展示和过滤的字段,其他走原始表回查。第二,索引列顺序按字段名或字段长度排,而不是按查询等值/范围/排序的实际顺序排:(seq_no, status, created_at) 这种顺序是因为 seq_no 等值、status 等值、created_at 范围+排序,列顺序错了索引就退化。第三,EXPLAIN 只看 rows 不看 key 和 Extra:用实际反查 SQL 跑 EXPLAIN,确认走了 target key 且 Extra 没有 Using filesort / Using temporary,再用压测脚本验证 P95 延迟,而不是靠 rows 估算。FAQ 补充三个高频问题:是否需要把 seq_no 单独建主键/唯一索引?不需要额外唯一索引,业务主键即可,seq_no 在联合索引的首列已经保证等值查询高效;快照表的 source_record_id 要不要建索引?要,这是回查原始明细、排查一致性差异的唯一入口,建议建普通索引;8.0 的 ICP 和 MRR 对这类查询有帮助吗?有帮助,覆盖索引+ICP 能减少回表,但改善幅度通常在常数级别,核心仍是把查询压到一条索引的等值+范围上。本文部分内容在撰写过程中使用了 AI 辅助,技术判断与参数示例由作者校验后发布。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70051529/viewspace-3187405/ ,如需转载,请注明出处,否则将追究法律责任。