sql
1.使用窗口函数,分组取最新一条数据
SELECT *
FROM (
-- 仅一次表扫描,生成带行号的结果集
SELECT
*,
-- 按appy_no分组,按插入时间降序排序,生成行号
ROW_NUMBER() OVER (PARTITION BY psn_no ORDER BY brdy DESC) AS rn
FROM t_info
) tmp
-- 直接筛选每个分组的第一条(最新)记录
WHERE tmp.rn = 1 limit 10
;
2.使用group by,分组取最新一条数据
select * from t_info where id in (
SELECT id FROM t_info WHERE (psn_no, brdy) IN (SELECT psn_no, MAX(brdy) FROM t_info GROUP BY psn_no)
)
limit 10
要分析两条 SQL 的性能差异,核心要从「执行逻辑、LIMIT 优化影响、扫描次数、排序开销」四个维度拆解,结合 20 万数据量的场景特性,就能解释 "查 10 条group by性能更优、查所有ROW_NUMBER更优" 的现象:
一、先明确两条 SQL 的核心执行逻辑(无索引默认场景)
两条 SQL 的目标一致:按psn_no分组,取每个分组内brdy最新(MAX (brdy))的记录,但执行路径完全不同:
| SQL 类型 | 核心执行步骤 |
|---|---|
| ① 窗口函数(ROW_NUMBER) | 1. 全表扫描 20 万条数据 → 2. 按psn_no分组 → 3. 每个分组内按brdy DESC排序 → 4. 分配行号rn → 5. 筛选rn=1 → 6. LIMIT 10 |
| ② GROUP BY + IN 子查询 | 1. 子查询 1:全表扫描→按psn_no分组→计算 MAX (brdy)(得到psn_no+max_brdy集合) → 2. 子查询 2:匹配原表得到对应id → 3. 主查询:按id查原表 → 4. LIMIT 10 |
二、为什么「查询 10 条时,②比①快十多倍」?
核心差异是「是否支持 "提前终止"」和「排序开销」,这两点在 "取少量数据" 时被无限放大:
1. SQL①(窗口函数)的致命短板:全量计算无法提前终止
窗口函数的设计逻辑是「先处理所有数据,再筛选结果」------ 即使最后要LIMIT 10,数据库也必须先完成20 万条数据的全部分组、排序、行号分配,再从 "所有符合条件(rn=1)的记录" 中取前 10 条。
相当于:你要从 100 个箱子里各拿 1 个最新的苹果,窗口函数是先把每个箱子里的苹果全排序,标上序号,再挑出每个箱子的第 1 个,最后只拿前 10 个 ------ 前面 "全排序标序号" 的冗余工作全做了,开销极大。
尤其 20 万数据无索引时,步骤 3 的「分组内排序」会触发「文件排序」(内存放不下 20 万条排序数据,需磁盘 IO),这是性能瓶颈的核心。
2. SQL②(GROUP BY + IN)的关键优势:轻量聚合 + LIMIT 提前终止
- 第一步聚合开销小:子查询 1 的
MAX(brdy)是「轻量聚合」------ 不需要对分组内的所有记录排序,只需遍历记录时记录最大值,比窗口函数的 "全排序 + 行号分配" 节省 80% 以上的计算资源; - LIMIT 可提前终止:主查询按
id匹配原表时,数据库会 "找到 10 条符合条件的记录就停止扫描",不需要遍历所有 20 万条数据。比如找到 10 个不同psn_no对应的最新记录后,直接返回,无需处理剩余数据。
相当于:你要拿 10 个最新苹果,先问每个箱子的管理员 "最新的苹果是哪个"(MAX (brdy)),再直接去拿这 10 个,不用管每个箱子里其他苹果的顺序 ------ 步骤少、无排序、早终止,速度自然快。
三、为什么「查询所有时,①比②好」?
当去掉LIMIT 10,需要返回所有分组的最新记录(假设共 N 条,N≤20 万)时,「扫描次数」成为核心影响因素:
1. SQL①(窗口函数):单遍扫描,一次完成
窗口函数是「全表扫描一次」就完成所有操作:扫描时同时做分组、排序、行号分配,最后筛选rn=1,属于 "一站式处理"。即使有排序开销,但只需扫描一次表,无额外关联成本。
2. SQL②(GROUP BY + IN):双遍扫描,关联开销大
需要「两次全表扫描」:
- 第一次:子查询 1 扫描全表做分组聚合(psn_no+MAX (brdy));
- 第二次:主查询扫描全表,匹配(psn_no+max_brdy)的记录;
- 额外开销:IN 子查询会生成临时表,主查询与临时表的关联(匹配 id)需要额外的哈希 / 嵌套循环计算。
20 万数据量下,"两次扫描 + 关联" 的总开销,会超过 "一次扫描 + 排序" 的开销 ------ 尤其是当符合条件的记录 N 较大时(比如 N=10 万),双遍扫描的 IO 成本会急剧上升,导致②比①慢。
四、补充:索引对性能的影响(关键优化点)
以上分析是「无合适索引」的默认场景,若创建针对性索引,差异会变化,但核心逻辑不变:
最优索引:(psn_no, brdy DESC) 复合索引
- 对 SQL①:
- 理论上:索引直接按
psn_no分组、brdy DESC排序,窗口函数无需额外排序,直接利用索引分配行号,性能会大幅提升(查 10 条时仍需全量分配行号,但排序开销消失); - 实际上:SQL①没有命中索引,无论是查询所有还是查询十条,性能都不如group by
- 理论上:索引直接按
- 对 SQL②:索引可让子查询 1 的分组聚合(MAX (brdy))无需全表扫描,直接按索引分组取最大值,主查询也能通过索引快速匹配(psn_no+brdy),查 10 条时优势更明显,但查所有时仍需双遍索引扫描。
五、总结:核心结论
| 场景 | 性能差异原因 |
|---|---|
| 查 10 条(少量数据) | ①需全量分组排序 + 行号分配(无法提前终止),②轻量聚合 + LIMIT 提前终止,差距源于 "全量计算 vs 按需终止" |
| 查所有(大量数据) | ①单遍扫描一站式处理,②双遍扫描 + 关联,差距源于 "扫描次数 + 关联开销" |
| 20 万数据放大差异 | 无索引时,①的文件排序 IO 开销极大,②的聚合无排序开销,进一步拉开差距 |
优化建议
- 若频繁 "查少量分组的最新记录"(如 LIMIT 10/100):用 SQL②的逻辑,搭配
(psn_no, brdy DESC)索引,性能最优; - 若频繁 "查所有分组的最新记录":用 SQL①的窗口函数,搭配
(psn_no, brdy DESC)索引,避免双遍扫描; - 核心原则:窗口函数适合 "全量结果处理",GROUP BY + 聚合适合 "按需取少量结果"。
备注:group by 在psn_no, brdy有重复数据时,并不能保证分组下只显示最新一条数据