基于PERSON_ID列去重而非整行的SQL查询优化需求
看起来你遇到的问题是:关联两张表后,同一个PERSON_ID因为在students表中有多条记录(比如示例里的12401有两个入学周期),导致关联结果出现重复的PERSON_ID,而DISTINCT只能去除完全重复的整行,没法实现每个PERSON_ID只留一条的需求。针对大规模数据集,这里有几个高效的解决方案:
方法1:使用窗口函数(推荐,适合大数据)
窗口函数是现代数据库中处理这类分组取第一条记录场景的高效方式,它能精准保证每个PERSON_ID只返回你指定的那条记录(比如最早入学周期对应的行):
SELECT PERSON_ID, GRADE, ENROLL_PERIOD FROM ( SELECT g.PERSON_ID, g.GRADE, s.ENROLL_PERIOD, -- 按PERSON_ID分组,每组内按入学周期排序,给每条记录编号 ROW_NUMBER() OVER (PARTITION BY g.PERSON_ID ORDER BY s.ENROLL_PERIOD) AS row_num FROM students s INNER JOIN grades g ON s.PERSON_ID = g.PERSON_ID WHERE s.ENROLL_PERIOD < 132 ) ranked_records -- 只保留每组的第一条记录 WHERE row_num = 1 ORDER BY ENROLL_PERIOD;
说明:
PARTITION BY g.PERSON_ID:把结果按PERSON_ID分成独立的组ORDER BY s.ENROLL_PERIOD:每组内的记录按入学周期升序排列(这样最早的入学记录会被标记为row_num=1)- 外层查询过滤
row_num=1的记录,就实现了每个PERSON_ID仅显示一次的效果
这个方法的优势是性能优秀,现代数据库(MySQL 8.0+、PostgreSQL、SQL Server等)对窗口函数有专门的优化,处理大规模数据集时比关联子查询更高效。
方法2:使用GROUP BY结合聚合函数(适合简单场景)
如果你只需要每个PERSON_ID的某个聚合结果(比如最小的入学周期、对应的成绩),可以用GROUP BY,但要注意确保成绩和入学周期是对应同一条记录的:
SELECT g.PERSON_ID, -- 用FIRST_VALUE确保取对应最小入学周期的成绩(部分数据库支持) FIRST_VALUE(g.GRADE) OVER (PARTITION BY g.PERSON_ID ORDER BY s.ENROLL_PERIOD) AS GRADE, MIN(s.ENROLL_PERIOD) AS ENROLL_PERIOD FROM students s INNER JOIN grades g ON s.PERSON_ID = g.PERSON_ID WHERE s.ENROLL_PERIOD < 132 GROUP BY g.PERSON_ID ORDER BY ENROLL_PERIOD;
或者如果你的数据库支持KEEP子句(比如Oracle),也可以这样写:
SELECT g.PERSON_ID, MIN(g.GRADE) KEEP (DENSE_RANK FIRST ORDER BY s.ENROLL_PERIOD) AS GRADE, MIN(s.ENROLL_PERIOD) AS ENROLL_PERIOD FROM students s INNER JOIN grades g ON s.PERSON_ID = g.PERSON_ID WHERE s.ENROLL_PERIOD < 132 GROUP BY g.PERSON_ID ORDER BY ENROLL_PERIOD;
不过相比窗口函数的方法,这个方式的灵活性稍差,窗口函数能更清晰地控制取哪一条记录。
为什么DISTINCT没法解决你的问题?
DISTINCT的作用是去除完全重复的整行数据,只有当你选中的所有列(grades.PERSON_ID、grades.GRADE、students.PERSON_ID、students.ENROLL_PERIOD)都完全相同时,才会被去重。而你的场景中,同一个PERSON_ID对应的ENROLL_PERIOD或GRADE不同,所以整行不重复,DISTINCT就无法过滤掉这些记录。
内容的提问来源于stack exchange,提问作者Adem

