Files
LabelChange-server/.trae/documents/sql_performance_optimization_plan.md
2026-06-01 16:30:29 +08:00

159 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SQL 运营监控查询性能优化方案
## 问题分析
当前查询耗时约 2 分钟,主要性能瓶颈来自以下几个方面:
### 瓶颈一CROSS JOIN 笛卡尔积(最严重)
```sql
-- DailyMetrics 中:
FROM DistinctDates dd
CROSS JOIN OrderFullInfo ofi
```
`DistinctDates` 有 N 行(假设 60 天就是 60 行),`OrderFullInfo` 有 M 行(假设 7000 条订单),则这个 CROSS JOIN 产生 **60 × 7000 = 420,000 行**的笛卡尔积,然后再对每一行做 5 个 `COUNT(DISTINCT CASE ...)` 聚合,计算量极大。
### 瓶颈二OrderFullInfo 被多次物化
`OrderFullInfo` 这个 CTE 在 `AllDates`(步骤 10中被引用了 **5 次**,然后在 `DailyMetrics` 里又被 CROSS JOIN 引用一次。MySQL 对 CTE 不做缓存(非 Materialized Hint每次引用都会重新执行整个计算链路`FormRequestRelation → FormTotalOrderCount → FormLabelPushTimes → FormFirstQualifiedDate → FormLabelRateAndQualifiedDate → OrderAssessment → OrderScanStatus → OrderFullInfo`),造成严重的重复计算。
### 瓶颈三label_scan_history 被扫描多次
`label_scan_history` 表在以下 CTE 中被反复全表扫描:
- `FormFirstScan`JOIN scan history
- `OrderScanStatus`GROUP BY NeutralWaybillNumber
- `DailyScanCount`COUNT 全表
- `DailySuccessCount`WHERE Result=0
- `DailyFailCount`:子查询 + GROUP BY
- `DailyStopCount`WHERE Description LIKE '%STOP%'
- `AllDates`UNION 中三次引用
共计 **7+ 次**扫描,而 `Description LIKE '%成功返回STOP标签%'` 是前缀通配符,无法使用索引。
### 瓶颈四GREATEST() 中重复计算 AssessmentBaseTime
`OrderAssessment` 中,`GREATEST(CASE...END, flr.FirstQualifiedTime_UTC5)` 这个表达式在同一行被计算了 **3 次**(用于 AssessmentBaseTime、HOUR() 判断、DATE() 计算),每次都重新计算。
### 瓶颈五FormRequestRelation 双 LEFT JOIN + COALESCE
```sql
LEFT JOIN ArrivalForms f_m ON r.MasterPackageNumber = f_m.HandoverNumber
LEFT JOIN ArrivalForms f_b ON r.BillOfLadingNumber = f_b.HandoverNumber
WHERE COALESCE(f_m.FormId, f_b.FormId) IS NOT NULL
```
`arrival_handover_forms` 被扫描两次,虽然 `HandoverNumber` 有唯一索引,但 `MasterPackageNumber``BillOfLadingNumber``label_replace_requests` 上**没有索引**,导致这两个 JOIN 是全表扫描。
### 瓶颈六AllDates 中 UNION 包含冗余子查询
`AllDates` 的 UNION 中有 8 个子查询,其中多个查 `label_scan_history` 的日期,结果存在大量重复,但又必须 UNION 去重,造成额外的排序和去重开销。
---
## 优化方案:使用物化临时表(存储过程)
### 选择方案:存储过程 + 临时表
**理由**
- 普通视图VIEW无法缓存中间结果MySQL 会每次重新执行,对复杂多步 CTE 无帮助
- 物化视图MySQL 不原生支持)需要额外维护
- **存储过程 + 临时表** 是 MySQL 中最有效的方式:将每个 CTE 的结果显式写入临时表,并在关键列上建索引,彻底消除重复计算和 CROSS JOIN 笛卡尔积问题
### 优化要点
#### 1. 消除 CROSS JOIN 笛卡尔积
`DailyMetrics` 的计算方式从"日期 × 订单 CROSS JOIN"改为"按订单数据聚合"
- 每个订单的 `ReceiptDate``LabelRetrievedDate_UTC5``AssessmentBaseTime``AssessmentTime``FirstSuccessDate_UTC5` 都是已知的固定值
- 改用 **预先按订单计算每个指标所属的日期范围**,再按日期 GROUP BY 汇总,避免笛卡尔积
#### 2. 将 OrderFullInfo 写入临时表并建索引
```sql
CREATE TEMPORARY TABLE tmp_order_full_info (...);
-- 建索引:
ALTER TABLE tmp_order_full_info ADD INDEX idx_receipt_date (ReceiptDate);
ALTER TABLE tmp_order_full_info ADD INDEX idx_label_date (LabelRetrievedDate_UTC5);
ALTER TABLE tmp_order_full_info ADD INDEX idx_success_date (FirstSuccessDate_UTC5);
ALTER TABLE tmp_order_full_info ADD INDEX idx_assessment_base_date (AssessmentBaseDate);
ALTER TABLE tmp_order_full_info ADD INDEX idx_assessment_date (AssessmentDate);
```
#### 3. 将 label_scan_history 的聚合结果写入临时表
`OrderScanStatus``DailyScanCount``DailySuccessCount``DailyFailCount``DailyStopCount` 提前计算并缓存到临时表,避免多次扫描 `label_scan_history`
#### 4. 为 label_replace_requests 补充关联字段索引
`label_replace_requests` 上为 `BillOfLadingNumber``MasterPackageNumber` 添加索引,加速 `FormRequestRelation` 的 JOIN。
#### 5. 提前计算 AssessmentBaseTime避免重复计算
`OrderAssessment` 阶段先计算出 `AssessmentBaseTime`,后续直接引用,不再重复展开 CASE WHEN。
---
## 实施步骤
### 步骤 1添加缺失的索引针对 BillOfLadingNumber 和 MasterPackageNumber
新建文件:`database/migrations/003_add_join_indexes.sql`
```sql
-- 为 label_replace_requests 添加 JOIN 关联字段索引
ALTER TABLE label_replace_requests
ADD INDEX IF NOT EXISTS idx_bill_of_lading_number (BillOfLadingNumber),
ADD INDEX IF NOT EXISTS idx_master_package_number (MasterPackageNumber);
```
### 步骤 2将 最新运营监控.sql 重写为存储过程
新建文件:`database/migrations/003_create_sp_operations_monitor.sql`
存储过程逻辑:
1. 建临时表 `tmp_scan_agg`label_scan_history 聚合,按 NeutralWaybillNumber
2. 建临时表 `tmp_daily_scan`(每日扫描统计,包含 扫描数/成功数/失败数/STOP数
3. 建临时表 `tmp_form_order`FormRequestRelation 的结果,含交接单信息)
4. 建临时表 `tmp_form_stats`(每个 FormId 的 TotalOrderCount、QualifyNeedCount、FirstQualifiedTime
5. 建临时表 `tmp_order_full`OrderFullInfo 结果,含 AssessmentBaseTime、AssessmentTime、IsSuccess 等所有字段)
6.`tmp_order_full` 上建必要的日期索引
7. 直接按日期聚合计算各项指标,输出最终结果(无 CROSS JOIN
调用方式:`CALL sp_GetOperationsMonitor();`
### 步骤 3新建调用文件不修改原文件
新建文件:`运营监控_优化版.sql`
内容为:`CALL sp_GetOperationsMonitor();`,作为新的日常使用入口,原 `最新运营监控.sql` 保持不变。
---
## 预期优化效果
| 优化点 | 优化前 | 优化后 |
|---|---|---|
| CROSS JOIN 笛卡尔积 | N日期 × M订单行 | 消除,直接按订单聚合 |
| OrderFullInfo 重复计算 | 6+ 次 | 1 次,写入临时表 |
| label_scan_history 扫描次数 | 7+ 次 | 2 次(一次聚合,一次日统计) |
| BillOfLadingNumber JOIN | 全表扫描 | 索引扫描 |
| MasterPackageNumber JOIN | 全表扫描 | 索引扫描 |
| AssessmentBaseTime 重复计算 | 3 次 | 1 次 |
**预期查询时间:从 120s 降至 5~20s**(取决于数据量)。
---
## 文件变更清单
| 操作 | 文件路径 | 说明 |
|---|---|---|
| 新建 | `database/migrations/003_add_join_indexes.sql` | 添加 BillOfLadingNumber、MasterPackageNumber 索引 |
| 新建 | `database/migrations/004_create_sp_operations_monitor.sql` | 创建存储过程 `sp_GetOperationsMonitor` |
| 新建 | `运营监控_优化版.sql` | 调用存储过程的入口文件(原文件保持不变) |
---
## 注意事项
- 存储过程使用 `DROP TEMPORARY TABLE IF EXISTS` 开头清理,保证每次调用都是全量最新数据
- 临时表生命周期仅限本次连接,不占用持久存储
- 索引使用 `ADD INDEX IF NOT EXISTS`MySQL 8.0 支持)
-`最新运营监控.sql` 文件不做任何修改,保留作为参考