SQL示例:获得积分最多的人,求和操作与去重的关系
本文分析了SQL查询中DISTINCT关键字错误使用导致积分计算错误的问题。
原SQL在子查询t1中使用DISTINCT导致重复积分记录被合并,使得最终求和结果偏小。
正确解法应移除DISTINCT,直接使用GROUP BY进行分组求和。
文章强调去重操作应根据业务需求决定:若重复记录代表独立业务事件(如多次交易)则不应去重;若为数据重复则需去重。
最佳实践是明确业务需求后,选择使用SUM()或SUM(DISTINCT)进行聚合计算。
题目:SQL290 获得积分最多的人(三)
描述
牛客每天有很多用户刷题,发帖,点赞,点踩等等,这些都会记录相应的积分。
有一个用户表(user),简况如下:
id name 1 tm 2 wwy 3 zk 4 5 lm 还有一个积分表(grade_info),简况如下:
user_id grade_num type 1 3 add 2 3 add 1 1 reduce 3 3 add 4 3 add 5 3 add 3 1 reduce 第1行表示,user_id为1的用户积分增加了3分。
第2行表示,user_id为2的用户积分增加了3分。
第3行表示,user_id为1的用户积分减少了1分。
.......
最后1行表示,user_id为3的用户积分减少了1分。
请你写一个SQL查找积分最高的用户的id,名字,以及他的总积分是多少(可能有多个),查询结果按照id升序排序,以上例子查询结果如下:
id name grade_num 2 wwy 3 4 3 5 lm 3 解释:
user_id为1和3的先加了3分,但是后面又减了1分,他们2个是2分,
其他3个都是3分,所以输出其他三个的数据。
错误解法
with t1 as( select distinct id, name, case when type='add' then grade_num else -grade_num end as new_grade from user u join grade_info g on u.id=g.user_id ), t2 as( select distinct id, name, sum(new_grade) over(partition by id) as score from t1 ) select id, name, score as grade_num from t2 where score=(select max(score) from t2) order by id;主要问题分析
1.
DISTINCT导致重复行被错误合并在
t1中:sql
select distinct id, name, case when type='add' then grade_num else -grade_num end as new_grade如果同一个
(id, name, new_grade)组合有多条记录,DISTINCT会合并成1行,导致t2中该分数只被计算一次,而不是多次。示例:
用户A:
add+5,add+5,add+5正确总和:+15
你的SQL:
DISTINCT后只有1行 +5,总和变成 +5 ❌2.
t2中再次使用DISTINCT多余
t2的DISTINCT也不必要,因为SUM(...) OVER(PARTITION BY id)已经为每个id生成相同的score值。
执行顺序
sql
t1: FROM → JOIN → CASE → DISTINCT → 生成 t1 t2: FROM t1 → 窗口函数 SUM() → DISTINCT → 生成 t2真正的问题
t1的DISTINCT在聚合之前去重 → 会导致丢失重复的new_grade记录→ 求和结果偏小
t2的DISTINCT在聚合之后去重 → 此时每个id的score已经确定,去重只是去掉重复行 →不影响求和结果
正确的执行顺序
SQL的执行逻辑是:
先执行 FROM/JOIN→ 生成原始行
再执行 CASE→ 计算
new_grade然后执行 DISTINCT→ 对
(id, name, new_grade)去重最后执行t2中的窗口函数 SUM() OVER()→ 对去重后的结果求和
所以 t1 的
DISTINCT确实在 t2 的求和之前,会影响结果。
如果业务上确保不会出现重复记录(比如有唯一约束),那么
DISTINCT不会造成问题。
正确解法
with t1 as( select id, name, case when type='add' then grade_num else -grade_num end as new_grade from user u join grade_info g on u.id=g.user_id ), t2 as( select id, name, sum(new_grade) as score from t1 group by id,name ) select id, name, score as grade_num from t2 where score=(select max(score) from t2) order by id;求和操作与去重的关系
-- 情况1:DISTINCT 在聚合函数外面 SELECT DISTINCT id, SUM(score) OVER() FROM table -- 执行顺序:先 SUM(),后 DISTINCT ✅ -- 情况2:DISTINCT 在子查询/CTE 里面 WITH t1 AS (SELECT DISTINCT ...) SELECT SUM(...) OVER() FROM t1 -- 执行顺序:先 DISTINCT(在子查询),后 SUM() ✅ -- 情况3:DISTINCT 作为聚合函数的参数 SELECT SUM(DISTINCT score) FROM table -- 执行顺序:先去重,后求和 ✅不是“不能去重”,而是“去重会改变业务含义”,需要根据业务需求决定。
两种情况对比
1. 业务上需要去重(正确使用)
sql
-- 场景:每个用户每天只记录一次加分,但可能重复上报 SELECT DISTINCT user_id, score FROM records; -- 或者 SELECT user_id, SUM(DISTINCT score) FROM records GROUP BY user_id;
2. 业务上不需要去重(错误使用)
sql
-- 场景:记录每次真实交易,重复意味着多次交易 SELECT DISTINCT user_id, amount FROM transactions; -- ❌ 会丢失交易次数
各种聚合函数的去重规则
| 函数 | 是否支持 DISTINCT | 说明 |
|---|---|---|
SUM() | ✅SUM(DISTINCT col) | 去重后求和 |
COUNT() | ✅COUNT(DISTINCT col) | 去重后计数 |
AVG() | ✅AVG(DISTINCT col) | 去重后平均 |
MAX()/MIN() | ❌ 不需要 | 去重不影响结果 |
窗口函数SUM() OVER() | ❌ 不支持 DISTINCT | 必须在子查询中预先去重 |
你的SQL问题本质
sql
-- t1 中的 DISTINCT 在t2 的聚合之前执行 with t1 as( select distinct id, name, ... -- ❌ 去重发生在求和之前 )
这不是“聚合函数不能去重”,而是“在错误的时间点去重”。
正确做法:
sql
-- 方案1:明确需要去重,在聚合函数内使用 SUM(DISTINCT new_grade) -- 方案2:不需要去重,删除 DISTINCT -- 直接保留所有行
判断标准:问自己一个问题
“重复记录代表重复的业务事件吗?”
是(如:多次下单、多次加分)→不能去重❌
否(如:重复上报、数据错误)→可以去重✅
最佳实践
sql
-- ❌ 危险写法(容易出错) SELECT DISTINCT id, SUM(amount) OVER(PARTITION BY id) -- ✅ 清晰写法 SELECT id, SUM(amount) FROM table GROUP BY id -- ✅ 明确去重(业务明确需要) SELECT id, SUM(DISTINCT amount) FROM table GROUP BY id
核心原则:去重是业务决策,不是技术习惯。
