# 指标查询性能优化计划 ## 📋 背景分析 当前仪表盘的日汇总查询涉及**多个复杂计算**: - ✅ 当天新增换单数 - ✅ 当天应该换单数 - ✅ 当日完成数 - ✅ 24小时完成率 - ✅ 当日完成率 - ✅ 16点前/后统计 - ✅ STOP数、标签推送、扫描数等 **当前问题**: - 🐌 单次查询需要 15-30 秒+ - 🔄 涉及多个 JOIN 操作和复杂的时区转换 - 💾 数据量可能较大,无缓存 --- ## 🎯 优化方案(按优先级) ### 方案一:数据库视图(强烈推荐)⭐⭐⭐⭐⭐ **优点**: - ✅ 预聚合数据,查询极快(通常 < 1 秒) - ✅ 减少应用端计算,降低 CPU 占用 - ✅ 支持直接在 SQL 中添加索引 - ✅ 易于维护和调试 **实现步骤**: 1. 创建 `v_DailyMetricsSummary` 视图 2. 在视图中实现所有指标计算逻辑 3. 后端直接查询视图,返回结果 **SQL 视图示例结构**: ```sql CREATE VIEW v_DailyMetricsSummary AS SELECT CAST(CONVERT_TZ(r.CreatedAt, '+00:00', '-05:00') AS DATE) AS MetricsDate, COUNT(DISTINCT CASE WHEN ... THEN r.Id END) AS DailyNewReplaceCount, COUNT(DISTINCT CASE WHEN ... THEN r.Id END) AS DailyShouldReplaceCount, COUNT(DISTINCT CASE WHEN ... THEN r.Id END) AS DailySuccessCount, ... FROM LabelReplace r LEFT JOIN LabelScan s ON r.NeutralWaybillNumber = s.NeutralWaybillNumber WHERE CONVERT_TZ(r.CreatedAt, '+00:00', '-05:00') >= CURRENT_DATE GROUP BY MetricsDate ``` **预期效果**:查询时间从 15-30 秒 → **< 1 秒** ✨ --- ### 方案二:Redis 缓存层(中等优先级)⭐⭐⭐⭐ **优点**: - ✅ 应用端缓存,极速响应 - ✅ 减轻数据库压力 - ✅ 支持多环境部署 **实现步骤**: 1. 在 `MetricsCalculationService` 中添加缓存装饰 2. 设置缓存过期时间(如 5 分钟自动刷新) 3. 提供手动刷新接口 **代码示例**: ```csharp public async Task GetDailySummaryAsync(DateTime date) { var cacheKey = $"metrics:daily:{date:yyyy-MM-dd}"; // 尝试从缓存获取 if (_cacheService.TryGet(cacheKey, out var cachedData)) { return cachedData; } // 从数据库查询 var summary = await CalculateFromDatabase(date); // 缓存 5 分钟 _cacheService.Set(cacheKey, summary, TimeSpan.FromMinutes(5)); return summary; } ``` **预期效果**:第一次查询 15-30 秒 → 后续查询 **< 100 ms** ⚡ --- ### 方案三:SQL 存储过程(降低优先级)⭐⭐⭐ **优点**: - ✅ 数据库端执行,性能接近视图 - ✅ 更灵活的逻辑控制 - ✅ 易于版本管理 **缺点**: - ❌ 维护复杂度高 - ❌ 跨平台迁移困难 **适用场景**: - 如果后续需要非常复杂的业务逻辑 - 需要多步骤的数据验证 --- ### 方案四:数据库索引优化(基础优化)⭐⭐ **必需的索引**: ```sql -- 优化时区转换查询 CREATE INDEX idx_created_at ON LabelReplace(CreatedAt); -- 优化 JOIN 操作 CREATE INDEX idx_neutral_waybill ON LabelScan(NeutralWaybillNumber); CREATE INDEX idx_neutral_waybill_replace ON LabelReplace(NeutralWaybillNumber); -- 优化状态查询 CREATE INDEX idx_replace_status ON LabelReplace(Status); CREATE INDEX idx_scan_result ON LabelScan(Result); ``` **预期效果**:基础查询优化 15-20% 性能提升 --- ## 📊 方案对比表 | 方案 | 查询速度 | 实现难度 | 维护成本 | 推荐指数 | |------|--------|--------|--------|---------| | **视图** | ⚡⚡⚡ (< 1s) | 中 | 低 | ⭐⭐⭐⭐⭐ | | **Redis缓存** | ⚡⚡⚡ (< 100ms) | 中 | 中 | ⭐⭐⭐⭐ | | **存储过程** | ⚡⚡ (1-2s) | 高 | 高 | ⭐⭐⭐ | | **索引优化** | ⚡ (10-12s) | 低 | 低 | ⭐⭐ | --- ## 🚀 推荐实施方案(综合最优) ### 第一阶段:快速修复(立即实施) 1. ✅ **创建 `v_DailyMetricsSummary` 视图** - 预计开发时间:2-3 小时 - 预期性能提升:**85-90%** - 难度:中等 2. ✅ **优化关键索引** - 预计开发时间:1 小时 - 难度:低 ### 第二阶段:长期优化(后续迭代) 1. 🔄 **添加 Redis 缓存层** - 前提:视图已完成 - 预计开发时间:2-3 小时 2. 📊 **监控和调优** - 定期检查查询性能 - 根据实际使用情况调整缓存策略 --- ## 💡 技术实现建议 ### 视图设计核心逻辑 **时区处理**: ```sql -- 使用 CONVERT_TZ 确保 UTC-5 时区的日期分组 DATE(CONVERT_TZ(r.CreatedAt, '+00:00', '-05:00')) AS MetricsDate ``` **标签率计算**: ```sql -- 两阶段计算:未扫描状态 vs 已扫描状态 CASE WHEN s.Id IS NULL THEN CASE WHEN r.Label IS NOT NULL THEN 1 ELSE 0 END ELSE CASE WHEN s.Result = 0 THEN 1 ELSE 0 END END AS IsCompleted ``` **分时段统计**: ```sql -- 16点前/后分组 CASE WHEN HOUR(CONVERT_TZ(r.ReceiptTime, '+00:00', '-05:00')) < 16 THEN '16点前' ELSE '16点后' END AS TimeSegment ``` --- ## ✅ 实施检查清单 - [ ] 分析当前查询性能瓶颈(获取执行计划) - [ ] 创建 `v_DailyMetricsSummary` 视图 - [ ] 添加必需的数据库索引 - [ ] 修改 `GetDailySummaryAsync` 方法调用视图 - [ ] 测试新查询性能(对标 < 1 秒) - [ ] 更新单元测试 - [ ] 性能基准测试和监控 - [ ] 文档更新 --- ## 📈 预期收益 | 指标 | 当前 | 优化后 | 提升幅度 | |------|------|--------|---------| | **查询延迟** | 15-30s | < 1s | **95%↓** | | **数据库 CPU** | 高 | 低 | **70%↓** | | **用户体验** | 差 | 优 | **显著** | | **可扩展性** | 有限 | 好 | **3-5x** | --- ## 🎓 关键要点 1. **视图是首选** - 一次性投入,长期收益 2. **缓存是补充** - 结合视图,性能最优 3. **索引是基础** - 无论采用哪种方案都需要 4. **监控很关键** - 定期检查性能指标 --- ## 📝 后续步骤 1. 用户确认优化方案 2. 创建数据库视图 3. 修改后端代码以调用视图 4. 性能测试和验证 5. 部署上线