# SQL 改进实现细节文档
## 核心逻辑理解
### 用户需求核心梳理
#### 当前指标体系
- **当天应该换单数** = 历史未完成换单数 + 当日新增换单数
- **实际换单数** = 当天换单完成的包裹数
- **当天换单完成率** = 实际换单数 / 当天应该换单数
#### 新增/修改的考核时间规则
当前系统对每个包裹有一个"考核时间",用来判断包裹是否在规定时间内完成了换单。新需求改变了考核时间的计算方式:
**基于标签率的分组考核**:
```
IF 客户标签率 >= 80% THEN
IF 到仓时间.hour < 16 THEN
考核时间 = 次日16:00
ELSE
考核时间 = 次日23:59
END IF
ELSE
考核时间 = 该包裹实际完成换单的时间
END IF
```
这意味着:
- 对于标签率高的客户(>=80%),给予固定的考核时间窗口
- 对于标签率低的客户(<80%),只要换单完成了就算达标
#### 新的24小时换单率计算
**公式**:(完成时间 <= 考核时间的包裹数) / 当天应该换单数
**含义**:
- 分子:通过24小时内完成考核的包裹数
- 分母:当天应该完成的所有包裹数(包括历史未完成+当日新增)
#### 新增指标
1. **16点前到仓包裹数**:当日 HOUR(到仓时间) < 16 的包裹
2. **16点后到仓包裹数**:当日 HOUR(到仓时间) >= 16 的包裹
---
## SQL实现方案详解
### 关键计算步骤
#### 步骤A:计算客户级别标签率(新增CTE)
用于在考核时间计算中判断是否应用固定时间窗口:
```sql
CustomerLabelRates AS (
SELECT
l.CustomerId,
COUNT(DISTINCT l.Id) AS total_requests,
COUNT(DISTINCT CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN l.Id END) AS labeled_requests,
ROUND(
COUNT(DISTINCT CASE WHEN l.Label IS NOT NULL AND l.Label != '' THEN l.Id END) /
COUNT(DISTINCT l.Id) * 100,
2
) AS label_rate_percent
FROM label_replace_requests l
GROUP BY l.CustomerId
)
```
#### 步骤A.5:计算系统整体标签率(在最终SELECT中)
用于输出到前端展示系统全局指标。
**重要**:标签率的分母应该是与交接单关联的所有订单数(因为当前的统计都是基于与arrival_handover_forms关联的订单)
```sql
-- 在最终SELECT的子查询中计算
CONCAT(ROUND(
(SELECT COUNT(DISTINCT l.Id) FROM label_replace_requests l
INNER JOIN arrival_handover_forms a
ON l.BillOfLadingNumber = a.HandoverNumber OR l.MasterPackageNumber = a.HandoverNumber
WHERE l.Label IS NOT NULL AND l.Label != '')
/
(SELECT COUNT(DISTINCT l.Id) FROM label_replace_requests l
INNER JOIN arrival_handover_forms a
ON l.BillOfLadingNumber = a.HandoverNumber OR l.MasterPackageNumber = a.HandoverNumber)
* 100,
2
), '%') AS 系统标签率
```
**说明**:这样计算的标签率与后续的换单统计保持逻辑一致,都是基于与交接单有关联的订单。
#### 步骤B:重新计算考核时间(修改ArrivalRequests CTE)
需要合并客户标签率信息,并根据新规则计算考核时间:
```sql
-- 关键伪代码逻辑
考核时间 = CASE
WHEN clr.label_rate_percent >= 80 THEN
CASE
WHEN HOUR(a.ReceiptTime) < 16 THEN
DATE_ADD(DATE(a.ReceiptTime), INTERVAL 1 DAY) + TIME '16:00:00'
ELSE
DATE_ADD(DATE(a.ReceiptTime), INTERVAL 1 DAY) + TIME '23:59:59'
END
ELSE
-- 对于标签率低的客户,需要获取实际完成时间
-- 这个需要在后续步骤中通过JOIN获得
oss.首次成功时间
END
```
#### 步骤C:统计16点分段到仓的包裹数(修改DailyBase CTE)
```sql
DailyBase AS (
SELECT
dd.日期,
-- 当日新增换单数
COUNT(DISTINCT CASE
WHEN ar.到货日期 = dd.日期 AND ar.LabelRetrievedAt IS NOT NULL
THEN ar.RequestId
END) AS 当日新增换单数,
-- 新增:16点前到仓
COUNT(DISTINCT CASE
WHEN ar.到货日期 = dd.日期 AND HOUR(ar.到货时间) < 16
THEN ar.RequestId
END) AS 16点前到仓包裹数,
-- 新增:16点后到仓
COUNT(DISTINCT CASE
WHEN ar.到货日期 = dd.日期 AND HOUR(ar.到货时间) >= 16
THEN ar.RequestId
END) AS 16点后到仓包裹数,
-- 当天标签推送数
COUNT(DISTINCT CASE
WHEN DATE(CONVERT_TZ(ar.LabelRetrievedAt, '+00:00', '-05:00')) = dd.日期
THEN ar.RequestId
END) AS 当日标签推送数
FROM DistinctDates dd
CROSS JOIN ArrivalRequests ar
GROUP BY dd.日期
)
```
#### 步骤D:重新计算24小时完成数(新/修改CTE)
```sql
Daily24HCompletedOrders AS (
SELECT
ar.到货日期 AS 日期,
COUNT(DISTINCT ar.NeutralWaybillNumber) AS 24H内完成数
FROM ArrivalRequests ar
INNER JOIN OverallScanStatus oss ON ar.NeutralWaybillNumber = oss.NeutralWaybillNumber
WHERE
oss.曾成功 = 1
AND oss.首次成功时间 IS NOT NULL
AND ar.考核时间 IS NOT NULL
-- 关键条件:完成时间 <= 考核时间
AND oss.首次成功时间 <= ar.考核时间
GROUP BY ar.到货日期
)
```
#### 步骤E:计算"当天应该换单数"(在最终输出中)
```sql
当天应该换单数 = 累计要换的总单数 + 当日新增换单数
-- 或在CASE中根据是否是历史日期判断
```
#### 步骤F:修改最终SELECT中的24小时率计算
```sql
-- 原逻辑:
CASE
WHEN 当日完成数 = 0 THEN '0.00%'
ELSE CONCAT(ROUND(24H内完成数 / 当日完成数 * 100, 2), '%')
END AS 24H换单率
-- 新逻辑:
CASE
WHEN 当天应该换单数 = 0 THEN '0.00%'
ELSE CONCAT(ROUND(24H内完成数 / 当天应该换单数 * 100, 2), '%')
END AS 24H换单率
```
---
## 实现复杂点分析
### 1. 考核时间的二阶段计算问题
**问题**:对于标签率低的客户,考核时间需要是"该包裹实际完成换单的时间",但这个时间在关联ArrivalRequests时还不可知。
**解决方案**:
- 在ArrivalRequests中先计算一个"参考考核时间"(对标签率>=80%的客户)
- 对于标签率<80%的客户,在后续JOIN OverallScanStatus时使用首次成功时间作为考核时间
- 在最终统计时通过CASE WHEN判断
```sql
ArrivalRequests AS (
SELECT
...,
l.CustomerId,
-- 先计算标签率
clr.label_rate_percent,
-- 基础到仓时间
a.到货时间,
-- 根据标签率计算参考考核时间
CASE
WHEN clr.label_rate_percent >= 80 THEN
CASE
WHEN HOUR(a.ReceiptTime) < 16 THEN
CONCAT(DATE_ADD(DATE(a.ReceiptTime), INTERVAL 1 DAY), ' 16:00:00')
ELSE
CONCAT(DATE_ADD(DATE(a.ReceiptTime), INTERVAL 1 DAY), ' 23:59:59')
END
ELSE
NULL -- 低标签率客户,考核时间取决于完成时间
END AS 基础考核时间
FROM ...
LEFT JOIN CustomerLabelRates clr ON l.CustomerId = clr.CustomerId
)
```
### 2. 时间比较精度问题
**问题**:`首次成功时间 <= 考核时间`的比较需要考虑:
- UTC和UTC-5的转换
- 时间戳的精度(秒级)
**解决方案**:
```sql
-- 确保都转换为UTC-5时区
WHEN CONVERT_TZ(oss.首次成功时间, '+00:00', '-05:00') <= ar.考核时间 THEN 1
```
### 3. 统计维度的叠加问题
**问题**:多个CTE都需要按日期统计,需要确保JOIN逻辑不会导致数据重复计数。
**解决方案**:
- 在每个COUNT中使用DISTINCT确保去重
- 使用CASE WHEN限制统计范围
- 在最终聚合时使用GROUP BY日期
---
## DTO修改方案
### DailyLabelStatsChineseDto 新增字段
```csharp
///
/// 系统整体标签率(%)
///
[SugarColumn(ColumnName = "系统标签率")]
public string LabelRate { get; set; }
///
/// 16点前到仓的包裹数
///
[SugarColumn(ColumnName = "16点前到仓包裹数")]
public int BeforeNoonArrivedCount { get; set; }
///
/// 16点后到仓的包裹数
///
[SugarColumn(ColumnName = "16点后到仓包裹数")]
public int AfternoonArrivedCount { get; set; }
///
/// 当天应该换单数(历史未完成+当日新增)
///
[SugarColumn(ColumnName = "当天应该换单数")]
public int ShouldReplaceCount { get; set; }
```
### C#映射代码
```csharp
var dto = new DailyLabelStatsChineseDto
{
// ... 现有字段 ...
LabelRate = reader["系统标签率"] as string ?? "0.00%",
BeforeNoonArrivedCount = reader["16点前到仓包裹数"] != DBNull.Value
? Convert.ToInt32(reader["16点前到仓包裹数"])
: 0,
AfternoonArrivedCount = reader["16点后到仓包裹数"] != DBNull.Value
? Convert.ToInt32(reader["16点后到仓包裹数"])
: 0,
ShouldReplaceCount = reader["当天应该换单数"] != DBNull.Value
? Convert.ToInt32(reader["当天应该换单数"])
: 0,
};
```
---
## 测试验证清单
- [ ] SQL语法校验(无错误)
- [ ] 数据准确性:验证16点分段统计
- [ ] 时区转换:确认所有时间操作都基于UTC-5
- [ ] 标签率计算:确认>=80%和<80%的分组逻辑
- [ ] 考核时间逻辑:抽样验证几个包裹的考核时间是否正确
- [ ] 24小时率:对比原逻辑,确保新分母计算正确
- [ ] 当天应该换单数:验证 = 累计未完成 + 当日新增
- [ ] Excel导出:确认新字段能正确导出