上传源代码版本

This commit is contained in:
Im-Jenisson
2026-06-01 16:30:29 +08:00
commit b2a9b7d3c2
462 changed files with 104365 additions and 0 deletions

View File

@@ -0,0 +1,237 @@
# 指标查询性能优化计划
## 📋 背景分析
当前仪表盘的日汇总查询涉及**多个复杂计算**
- ✅ 当天新增换单数
- ✅ 当天应该换单数
- ✅ 当日完成数
- ✅ 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<Daily24HCompletionRateDto> 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. 部署上线