ScanningReporting 速度优化

admin | 2026-06-03 19:58 | 阅读 9 次

优化 GME ScanningReport 一天数据查询性能

Summary

当前慢点主要有三层叠加:大范围 physical_layers 扫描、逐 panel 读网络 .proto 文件、逐 defect 的 N+1 MySQL 明细查询。
本次优化目标按你的选择定为:优先降低总耗时,同时保持结果完全一致

预计收益最大的改动顺序是:

  1. 去掉逐 defect 的 N+1 查询,改成批量明细预取。
  2. 初始 panel 查询不再携带 pl.Container 大字段,改成后置按需批量加载。
  3. 为首个时间范围查询补充索引检查/建议,减少一天数据扫描量。

Key Changes

1. 拆分查询为 3 个阶段

  • 阶段 A:先查询 panel 基础元数据,只取 Id/Scanned/Verified/AOI/CVR/JobName/LayerName/LotName/PhysicalID/MovesCount/DefectsCount不取 pl.Container
  • 阶段 B:遍历 panel 读取 .proto,收集所有需要的 defect 键:(StepInstanceId, OriginalCodeId, CodeId)
  • 阶段 C:按批次一次性回库加载 defect 明细,再组装最终 ScanningReportItem

2. 消除 LoadDefectDetail(...) 的 N+1 查询

  • 删除“每个 defect 查询一次 MySQL”的实现。
  • 改为在同一个连接内创建临时表,例如 tmp_scanning_defect_keys,列为:
    • StepInstanceId
    • OriginalCodeId
    • CodeId
  • 将阶段 B 收集到的唯一键去重后,分批插入临时表。
  • 用一次 join 查询取回全部 UnitID/SetID/SparkCode/DefectCode/DefectDescription,结果放入内存字典,key 为三元组。
  • 明细字典缺失时,保持当前行为:该 defect 跳过,不改变最终结果语义。

3. 将 Container 改为后置按需批量加载

  • 初始 physical_layers SQL 去掉 pl.Container as Container
  • .proto 读取完成、确定该 panel 最终会产出结果后,再按 surviving panel 的 Id 分批查询:
    • select id, Container from physical_layers where id in (...)
  • 分批大小固定为 500
  • ProcessId/Barcode 的反序列化改为基于这个后置批量结果完成,输出字段保持不变。

4. SQL 过滤前移

  • 如果 OnlyOver24Hours = true,把条件前移到首个 SQL:
    • pl.Verified is not null
    • timestampdiff(hour, pl.Scanned, pl.Verified) >= 24
  • LotId/LayerId/PanelPhysicalId 继续保留在首个 SQL 中。
  • IncludeZeroDefectPanels 先不下推到 SQL,避免依赖 DefectsCount 字段语义与 .proto 实际内容完全一致。

5. 数据库索引建议

  • physical_layers 检查并补充复合索引,优先顺序:
    • (Scanned, LotID, LayerInfoID, PhysicalID)
  • 如果 PanelPhysicalId 高频单独查询,额外考虑:
    • (PhysicalID, Scanned)
  • steps_instances.idsteps_instances.ParentIDcustomer_defects_codes.id 确认已有索引;如果是主键则无需额外处理。
  • 实施前先跑 EXPLAIN,确认首个查询命中 Scanned 相关索引,而不是全表扫描。

Implementation Notes

  • 主要改动集中在 ScanningReportService.cs
  • PhysicalLayerRow 去掉 Container 字段,新增后置容器加载用的内部 DTO 或字典。
  • 新增一个批量明细加载方法,替代 LoadDefectDetail(...)
  • 进度状态拆为更真实的阶段:
    • Loading panel list...
    • Reading proto files...
    • Loading defect details...
    • Loading container data...
    • Building result...
  • 临时表在单次查询连接内创建和使用,查询结束自动销毁,不引入持久 schema 变更。

Test Plan

  • 查询 1 天数据,结果行数与当前版本一致。
  • 随机抽样 20 条结果,对比 SetID/UnitID/SparkCode/DefectCode/DefectDescription/ProcessId/Barcode 与旧逻辑一致。
  • 验证 OnlyOver24Hours 打开/关闭时,结果与旧逻辑一致。
  • 验证 IncludeZeroDefectPanels 打开/关闭时,结果与旧逻辑一致。
  • 验证缺失 .proto、损坏 .proto、缺失 defect detail 三种异常场景仍不崩溃,行为与当前版本一致。
  • 对“一天数据”记录总耗时,重点比较:
    • 首屏开始处理时间
    • MySQL 查询次数
    • 总完成时间

Assumptions

  • 保持现有结果列和最终输出完全一致,不做 UI 或结果模型调整。
  • MySQL 允许会话级临时表。
  • ProcessId/Barcode 仍然必须保留,因此 Container 不能彻底移除,只能后置批量加载。
  • 先做代码层优化;数据库索引如果需要新增,再作为同一轮性能优化的一部分执行。

评论 (1)

发表评论
支持 Markdown 格式
a
admin 2026-06-03 19:58

已经把这轮性能优化落地在 ScanningReportService.cs

现在查询链路变成了:

  • 首次查 physical_layers 时不再带 pl.Container 大字段
  • 先批量读取 panel 对应的 .proto
  • 把所有 defect key 去重后写入临时表,一次性回库取明细,替掉原来的逐 defect N+1 查询
  • 只对最终确实会产出结果的 panel,分批回库取 Container,再反序列化 ProcessId/Barcode
  • OnlyOver24Hours 的过滤前移到了首个 SQL 里
  • 进度状态也拆成了 Loading panel list / Reading proto files / Loading defect details / Loading container data / Building result

我还顺手把临时表初始化改成了两条独立 SQL,避免 MySQL 连接不支持多语句执行的兼容性问题。解决方案已经重新编译通过,0 error / 0 warning

这次我没有直接改数据库索引,因为那属于真实库变更,不适合在代码仓里直接替你执行。如果你要,我下一步可以继续帮你整理一份可直接在 MySQL 上跑的 EXPLAIN 和索引 SQL。