探讨:20 万数据量下ROW_NUMBER和GROUP BY两条 SQL 性能差异分析(查 10 条 / 查所有)

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有重复数据时,并不能保证分组下只显示最新一条数据

相关推荐
wusp19942 小时前
Django 迁移系统全指南:从模型到数据库的魔法之路
数据库·mysql·django
TDengine (老段)2 小时前
TDengine GROUP BY 与 PARTITION BY 使用及区别深度分析
大数据·开发语言·数据库·物联网·时序数据库·tdengine·涛思数据
IT教程资源D2 小时前
[N_093]基于springboot,vue的宠物商城
mysql·vue·前后端分离·宠物商城·springboot宠物商城
在风中的意志2 小时前
[数据库SQL] [leetcode-197] 197. 上升的温度
数据库·sql
啊吧怪不啊吧2 小时前
从单主机到多主机——分布式系统的不断推进
网络·数据库·redis·分布式·架构
老华带你飞2 小时前
电影购票|基于java+ vue电影购票系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
老华带你飞2 小时前
宠物管理|基于java+ vue宠物管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·宠物
IT教程资源C2 小时前
(N_093)基于springboot,vue的宠物商城
mysql·vue·前后端分离·宠物商城·springboot宠物商城
鸽芷咕3 小时前
告别适配难题:Oracle 迁移 KingbaseES SQL 语法快速兼容方案
数据库·sql·oracle·金仓数据库