按部分列 DISTINCT 并获取另一列的 MAX 记录
结论
可以使用窗口函数 ROW_NUMBER():先按 Customer 和 Site 分组,再按 CounterValue 从大到小为组内记录编号,最后保留编号为 1 的记录。
查询返回的是 CounterValue 最大值所在的完整记录,因此还能同时取得 CollectorNode、Date、Counter 等字段。
WITH RankedRecords AS
(
SELECT
Customer,
Site,
[Date],
[Counter],
CounterValue,
CollectorNode,
ROW_NUMBER() OVER
(
PARTITION BY Customer, Site
ORDER BY CounterValue DESC, [Date] DESC
) AS RowNum
FROM dbo.YourTable
WHERE [Date] >= @BillingCycleStart
AND [Date] < @BillingCycleEnd
)
SELECT
Customer,
Site,
[Date],
[Counter],
CounterValue,
CollectorNode
FROM RankedRecords
WHERE RowNum = 1;
为什么 DISTINCT 和 MAX() 不能直接解决
MAX(CounterValue) 只能找出每组的最大值,无法确定其他字段应该取自哪条记录。
例如,同一个客户站点中有以下数据:
| Customer | Site | Date | CounterValue | CollectorNode |
|---|---|---|---|---|
| A | S1 | 2026-08-01 | 100 | Node01 |
| A | S1 | 2026-08-15 | 150 | Node02 |
下面的聚合查询可以得到最大值 150:
SELECT
Customer,
Site,
MAX(CounterValue) AS MaxCounterValue
FROM dbo.YourTable
GROUP BY Customer, Site;
但如果把 Date、CollectorNode 等字段直接加入 GROUP BY:
GROUP BY Customer, Site, [Date], CollectorNode
分组粒度就变了。数据不再以每个 Customer、Site 组合为一组,而是以这些字段的每种组合为一组,因此每个站点仍可能返回多条记录。
DISTINCT 只能删除所有已选字段都完全相同的重复行,不能表达“为每个客户站点选出最大值所在的记录”。
使用 ROW_NUMBER() 选择完整记录
窗口函数不会把多行聚合成一行,而是为每条记录计算组内排名:
ROW_NUMBER() OVER
(
PARTITION BY Customer, Site
ORDER BY CounterValue DESC
)
这里:
PARTITION BY Customer, Site按客户和站点的组合分别排名。ORDER BY CounterValue DESC将最大值排在最前面。RowNum = 1只保留每组排名第一的完整记录。
计费周期条件应在排名前应用。否则,查询可能先从全部历史记录中找出最大值,再检查该记录是否处于指定周期内,得到的结果会与需求不符。
如果结束参数表示下一个计费周期的起点,建议使用左闭右开的时间范围:
WHERE [Date] >= @BillingCycleStart
AND [Date] < @BillingCycleEnd
这种写法适用于包含时间部分的 datetime 或 datetime2 字段,也不需要手动拼接 23:59:59。
最大值相同时如何处理
如果同一个 Customer、Site 组合中有多条记录的 CounterValue 都是最大值,只按该字段排序时,ROW_NUMBER() 会选出其中一条,但无法保证具体选中哪条。
如果业务规则要求在最大值相同时选择日期最新的记录,可以增加次级排序:
ROW_NUMBER() OVER
(
PARTITION BY Customer, Site
ORDER BY CounterValue DESC, [Date] DESC
)
如果日期也可能相同,最好再加入唯一键,例如 RecordId:
ROW_NUMBER() OVER
(
PARTITION BY Customer, Site
ORDER BY CounterValue DESC, [Date] DESC, RecordId DESC
)
排序字段只有能够唯一确定记录顺序,查询结果才会稳定。
如果需要返回所有并列的最大值记录,可以改用 RANK():
WITH RankedRecords AS
(
SELECT
Customer,
Site,
[Date],
[Counter],
CounterValue,
CollectorNode,
RANK() OVER
(
PARTITION BY Customer, Site
ORDER BY CounterValue DESC
) AS ValueRank
FROM dbo.YourTable
WHERE [Date] >= @BillingCycleStart
AND [Date] < @BillingCycleEnd
)
SELECT
Customer,
Site,
[Date],
[Counter],
CounterValue,
CollectorNode
FROM RankedRecords
WHERE ValueRank = 1;
此时每个客户站点可能返回多行,这是存在并列最大值时的正常结果。
使用聚合查询再关联原表
另一种方法是先计算每组的最大值,再关联回原表:
WITH MaximumValues AS
(
SELECT
Customer,
Site,
MAX(CounterValue) AS MaxCounterValue
FROM dbo.YourTable
WHERE [Date] >= @BillingCycleStart
AND [Date] < @BillingCycleEnd
GROUP BY Customer, Site
)
SELECT
t.Customer,
t.Site,
t.[Date],
t.[Counter],
t.CounterValue,
t.CollectorNode
FROM dbo.YourTable AS t
INNER JOIN MaximumValues AS m
ON m.Customer = t.Customer
AND m.Site = t.Site
AND m.MaxCounterValue = t.CounterValue
WHERE t.[Date] >= @BillingCycleStart
AND t.[Date] < @BillingCycleEnd;
这种写法会返回所有并列的最大值记录。如果要求每组严格返回一行,还需要定义并列时的取舍规则,所以使用 ROW_NUMBER() 通常更直接。
注意事项
- 建议用方括号引用
[Date]和[Counter],避免它们与 SQL 关键字或类型名称产生歧义。更好的做法是改用含义明确的列名。 MAX()会忽略NULL。使用ORDER BY CounterValue DESC时,还需要确认CounterValue全为NULL的分组是否应该返回记录。- 如果
CounterValue使用字符类型保存,数据库可能按字符串规则排序。例如,字符串'9'可能排在'100'前面。需要进行数值比较的字段应使用合适的数值类型。 - 数据量较大时,可以结合实际查询情况,考虑建立以计费周期、
Customer、Site为主要键并包含CounterValue的索引。具体的索引顺序应根据执行计划和现有索引确定。 - 此处不需要同时使用
DISTINCT和GROUP BY。窗口函数已经按Customer、Site划分数据,并通过排名选出了每组所需的记录。
备注:内容仅供参考。
