SQL窗口函数入门:详解OVER语法与5大高频应用场景

发布时间:2026/10/31 18:06:25
SQL窗口函数入门:详解OVER语法与5大高频应用场景

简介:这是一份专为数据库从业者打造的《SQL窗口函数速查表》PDF文档,面向数据库管理员、数据分析师、开发工程师及SQL进阶学习者,旨在解决复杂数据分析场景下窗口函数选型难、语法易混淆、实际应用无从下手等痛点。资源以841KB单文件PDF形式交付,内容按功能分类编排,覆盖排名函数(ROW_NUMBER/RANK/DENSE_RANK)、偏移函数(LEAD/LAG)与聚合类窗口函数(SUM/AVG/COUNT等)三大核心类型,详解语法结构、PARTITION BY与ORDER BY组合逻辑、帧范围定义及典型业务场景示例(如同比环比计算、分组内TopN、连续值识别等)。已有255人下载学习,文档采用简洁排版+代码即用式示例,支持快速定位、即时理解与高效复用,是日常查询优化、报表开发与SQL能力跃迁的实用型工具手册。

1. SQL窗口函数到底是什么?——别被“窗口”俩字吓住,它其实就是你手边的Excel透视表+公式下拉键

你有没有试过在Excel里做这样的事:一列是销售员名字,一列是每月销售额,你想给每个人按月排个名次,但又不想把原始数据汇总成一行——而是希望每行都保留“张三|1月|8万|第2名”“张三|2月|9.5万|第1名”这种带明细的结果?这时候你大概率会用到“排序+填充序列”,或者写个RANK函数配合绝对/相对引用……很麻烦,还容易翻车。
SQL窗口函数,就是数据库里的“这个功能”——它不压缩你的原始行,也不强制你GROUP BY,而是在不丢失任何细节的前提下,给每一行“现场算一个新值”:可以是排名、累计和、前后对比、移动平均……就像给每行数据配了个随身计算器,而且这个计算器知道“当前行属于哪一组、排在第几个、前面还有谁”。
它不是新语言,也不是黑匣子,就是标准SQL里一个带OVER()括号的语法升级版;它不依赖特定数据库(主流MySQL 8.0+、PostgreSQL、SQL Server、Oracle、Doris、StarRocks全支持);它解决的正是你每天都在写的那种“既要明细又要统计”的查询痛点——比如“查每个部门工资最高的3个人”“算用户连续登录天数”“看本月销售额比上月增长多少”。
如果你常写SQL,但还在用自连接、子查询、临时表来硬凑这类结果;如果你是刚学SQL的新手,听说“窗口函数很厉害”却卡在“到底在哪用、怎么写、为啥加OVER就变了”;或者你是分析师,报表里总要导出Excel再加工……那这份《SQL窗口函数速查表.pdf》就是为你准备的——它不讲理论推导,不堆数学定义,而是像一张贴在你显示器边上的便利贴:查什么函数,翻哪页,抄哪段,改哪几个词,就能跑通。


2. 为什么非得学窗口函数?——告别三层嵌套子查询的“血泪经验”

2.1 传统写法有多绕?一个真实场景让你秒懂

假设你有一张销售记录表sales,字段有:salesperson(销售员)、month(月份)、amount(金额)。现在需求是:“查出每位销售员,当月销售额在本部门所有月份中的排名(并列不跳名次)”。

用老办法怎么做?
→ 先按销售员分组,算出每人每月金额;
→ 再对每个销售员的所有月份记录,跟自己做自连接,数有多少条记录金额 ≥ 当前行;
→ 最后还得去重、排序、取别名……写出来可能长这样:

SELECT s1.salesperson, s1.month, s1.amount, COUNT(s2.amount) AS dense_rank FROM sales s1 LEFT JOIN sales s2 ON s1.salesperson = s2.salesperson AND s2.amount >= s1.amount GROUP BY s1.salesperson, s1.month, s1.amount ORDER BY s1.salesperson, s1.month;

这段代码不仅难读、难改、难调试,而且一旦加个“只看2024年数据”,就得在两个地方改WHERE条件;想换成“按季度排名”,逻辑又要重写。更关键的是:它执行慢——因为自连接会让数据量指数级膨胀,1万行表可能变成上百万次比较。

2.2 窗口函数怎么一招破局?

同样的需求,窗口函数写法就干净得像呼吸:

SELECT salesperson, month, amount, DENSE_RANK() OVER ( PARTITION BY salesperson ORDER BY amount DESC ) AS rank_in_person FROM sales;

提示:PARTITION BY salesperson= 把数据按销售员切分成独立小组(相当于Excel里“按销售员分组”);ORDER BY amount DESC= 在每个小组内,按金额从高到低排;DENSE_RANK()= 给每行打一个“不跳名次”的排名。整段话直译就是:“对每个销售员自己的所有月份记录,按金额降序排,给我标上1、2、2、3这样的名次”。

它不生成新表,不重复扫描,不破坏原始行数,执行计划清晰,维护成本极低。而这份速查表,就是帮你把这类“翻译工作”压缩成查字典:看到“不跳名次”,翻到“排名函数”页,抄DENSE_RANK(),填进OVER()里对应位置,改好字段名——完事。

2.3 它真能覆盖你80%的复杂查询吗?三个高频场景实锤

我们拆开速查表里最常被翻的三类函数,对应你工作中真正卡壳的时刻:

  • “找Top N”类问题(如“每个城市销量前3的店铺”):不用LIMIT 3(那只能取全局前三),也不用写N个UNION,直接用ROW_NUMBER() OVER (PARTITION BY city ORDER BY sales DESC),再套一层WHERE rn <= 3——逻辑清晰,性能可控,改N=5还是N=10只动一个数字。

  • “前后对比”类问题(如“用户本月活跃度 vs 上月”“股价今日涨跌幅”):LAG(amount, 1) OVER (PARTITION BY user_id ORDER BY date)—— 意思是“对每个用户,按日期排好序,取前1行的amount值”。它自动处理了“上月不存在怎么办”(返回NULL),比你自己写LEFT JOIN日期偏移安全十倍。

  • “滚动计算”类问题(如“最近7天销售额”“过去30天平均访问时长”):用SUM(amount) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)——ROWS BETWEEN ...就是告诉数据库:“别算全部,就看当前行往前6行+当前行这7行”。这是传统聚合函数根本做不到的“动态窗口”。

这些不是炫技,而是你日报、周报、AB测试分析、风控规则里天天要写的逻辑。学会窗口函数,不是为了“显得高级”,而是为了少写一半SQL,少调三次接口,少等两分钟查询,少背三遍错误提示。


3. 第一步怎么迈?——从安装环境到跑通第一个例子,零基础也能照着敲

3.1 先确认:你手头的数据库支不支持?(别白忙活)

窗口函数不是“未来技术”,它已是现代SQL的事实标准。但版本门槛必须看清——速查表里所有函数,要求你的数据库满足以下最低版本:

数据库类型最低支持版本验证命令(复制粘贴就能试)
MySQL8.0.2+SELECT VERSION();→ 结果应显示8.0.x或更高
PostgreSQL8.4+(完全支持)SELECT version();→ 查看是否含PostgreSQL 8.4或更新
SQL Server2005+(基础)
2012+(完整)
SELECT @@VERSION;→ 含2012或更高即支持全部
SQLite3.25.0+.version(在sqlite3命令行中)或SELECT sqlite_version();

注意:如果你用的是旧版MySQL(如5.7),OVER()会直接报错You have an error in your SQL syntax。这不是你写错了,是数据库不认这个语法。此时有两个选择:① 升级数据库(推荐,长期省心);② 用速查表里标注的“兼容写法”替代(如用变量模拟ROW_NUMBER),但速查表PDF第2页就明确写了“兼容方案仅作过渡,性能与可读性均大幅下降”——所以第一步,请先花2分钟确认版本。

3.2 准备一个最简测试表(5行数据,3分钟搞定)

别急着打开生产库!我们用最轻量的方式建一个本地测试表。以MySQL为例(其他数据库语法微调,速查表附录页有对照):

-- 1. 创建测试库(如果还没建) CREATE DATABASE IF NOT EXISTS window_demo; USE window_demo; -- 2. 建表:就5行销售数据,足够验证所有核心函数 CREATE TABLE sales ( id INT PRIMARY KEY AUTO_INCREMENT, salesperson VARCHAR(20), month VARCHAR(10), amount DECIMAL(10,2) ); -- 3. 插入5行测试数据(复制粘贴即可) INSERT INTO sales (salesperson, month, amount) VALUES ('张三', '1月', 80000.00), ('张三', '2月', 95000.00), ('李四', '1月', 72000.00), ('李四', '2月', 68000.00), ('王五', '1月', 85000.00);

执行完后,运行SELECT * FROM sales;,你应该看到5行整齐的数据。这就是你的“练手沙盒”——所有后续例子,都基于这张表,改字段名、换函数名,就能立刻看到结果变化。

3.3 跑通第一个窗口函数:ROW_NUMBER()(给每行编个号)

这是窗口函数的“Hello World”,也是速查表第1页第一个示例。我们给这5行数据,按销售员分组、按金额从高到低编号:

SELECT salesperson, month, amount, ROW_NUMBER() OVER ( PARTITION BY salesperson ORDER BY amount DESC ) AS row_num FROM sales;

执行后你会看到什么?

salesperson | month | amount | row_num ------------|--------|----------|--------- 张三 | 2月 | 95000.00 | 1 张三 | 1月 | 80000.00 | 2 李四 | 1月 | 72000.00 | 1 李四 | 2月 | 68000.00 | 2 王五 | 1月 | 85000.00 | 1

这行代码每个部分在干什么?

  • ROW_NUMBER():函数名,意思是“给我一个从1开始的连续序号”;
  • OVER (...):窗口定义区,告诉数据库“这个序号怎么算”;
  • PARTITION BY salesperson:把5行切成3组(张三2行、李四2行、王五1行),序号在每组内独立计数;
  • ORDER BY amount DESC:在每组里,金额大的排前面,所以张三的9.5万分到第1名,8万分到第2名;
  • AS row_num:给这个新列起个易读的名字,方便你在后续WHERE或ORDER中引用。

提示:如果你删掉PARTITION BY,只留ORDER BY amount DESC,结果会变成全局排序编号(王五8.5万→1,张三9.5万→2……),这就是“不分组”的用法,速查表第3页有专门对比表格。

3.4 动手改一改:30秒体验“举一反三”的快感

别只抄!马上动手改两处,亲眼看看效果变化:

改法1:把排名逻辑从“金额”换成“月份”
把ORDER BY amount DESC换成ORDER BY month ASC,再执行——你会发现张三的1月变第1名,2月变第2名,李四同理。说明:ORDER BY控制的是“组内排序依据”,换字段就是换排序维度。

改法2:把函数从ROW_NUMBER()换成RANK()
把第一行改成RANK() OVER (...),再执行——目前数据没并列,结果看起来一样;但如果你手动把张三两行金额都改成95000,再跑一次,就会看到:ROW_NUMBER给出1、2,而RANK给出1、1(然后下一个名次跳到3)。这就是速查表里强调的“RANK跳名次,DENSE_RANK不跳”的实际差异。

这两次修改,总共不到1分钟,但你已经亲手验证了窗口函数最核心的两个变量:函数选择决定计算逻辑,OVER子句决定作用范围。速查表的价值,就是把这种“改什么、影响什么”的映射关系,一页一页给你列清楚。


4. 新手最容易踩的5个坑:避坑指南(照着做,少走三天弯路)

4.1 现象:OVER()括号里写了ORDER BY,但结果没排序,还是乱序

原因:OVER()里的ORDER BY只影响窗口函数内部计算顺序(比如ROW_NUMBER按什么排),不控制最终SELECT结果的显示顺序!很多人以为写了ORDER BY amount,输出就自动按金额排,其实不然。
解决:必须在整条SQL末尾,单独加一个ORDER BY。例如:

SELECT salesperson, amount, ROW_NUMBER() OVER (ORDER BY amount) AS rn FROM sales ORDER BY amount; -- 这个才是控制最终显示顺序的!

4.2 现象:PARTITION BY后结果为空,或只返回1行

原因:字段名拼错了,或者该字段有NULL值。PARTITION BY遇到NULL,会把所有NULL值归为同一组;如果某字段全为NULL,就只剩1个分区。
解决:先用SELECT DISTINCT 字段名 FROM 表名;检查值是否符合预期;若字段可能为空,改用COALESCE(字段名, '未知')兜底。速查表第7页“分区技巧”栏有完整写法。

4.3 现象:LAG()/LEAD()返回全是NULL,明明数据有前后行

原因:OVER()里漏写了ORDER BY!LAG和LEAD必须知道“哪行是前一行”,没有ORDER BY,数据库无法确定行序,就默认返回NULL。
解决:LAG(amount, 1) OVER (ORDER BY month)是正确写法;LAG(amount, 1) OVER ()是无效写法,速查表所有行序号函数示例都强制包含ORDER BY,请严格对照。

4.4 现象:SUM() OVER()结果和普通SUM()一样,都是单个值

原因:忘了加PARTITION BY或ORDER BY,导致窗口默认是“整个结果集”,于是SUM算的是全表总和,每行都显示同一个数字。
解决:明确你要的窗口范围。想按部门累加?加PARTITION BY dept;想算累计值?加ORDER BY date ROWS UNBOUNDED PRECEDING(速查表第12页“累计求和”模板可直接抄)。

4.5 现象:执行时报错This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA(常见于MySQL存储过程)

原因:在存储过程或函数里直接调用窗口函数,但未声明特性。这不是窗口函数本身的问题,而是MySQL对存储程序的安全限制。
解决:在创建存储过程时,加上READS SQL DATA声明。例如:

DELIMITER $$ CREATE PROCEDURE get_top3() READS SQL DATA -- 加这一行! BEGIN SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn FROM emp ) t WHERE rn <= 3; END$$ DELIMITER ;

速查表附录页“存储过程适配”有详细说明和各版本兼容写法。


5. 下一步学什么?——从速查表出发的3条渐进路径(附资源定位)

5.1 路径一:先吃透速查表里的“函数分类表”(第1页+第5页)

别急着往后翻!速查表第1页顶部就是一个横向分类导航图:
✅排名类(ROW_NUMBER / RANK / DENSE_RANK)→ 解决“谁第几”;
✅偏移类(LAG / LEAD)→ 解决“跟上/下一行比”;
✅分布类(PERCENT_RANK / CUME_DIST / NTILE)→ 解决“我在整体中什么位置”(如Top 10%);
✅聚合类(SUM / AVG / COUNT + OVER)→ 解决“组内累计/移动计算”;
✅专用类(FIRST_VALUE / LAST_VALUE / NTH_VALUE)→ 解决“取窗口内特定位置的值”。

我第一次学的时候,就是拿一张便利贴,盖住第1页分类图,只露函数名,自己默写“它用来干啥、典型场景、必带哪个OVER子句”。比如看到NTILE(4),马上反应:“分四等份,常用于用户分层,必须带ORDER BY,否则分得毫无意义”。每天5分钟,三天就能把5大类函数的“心智模型”刻进肌肉记忆。速查表第5页的“函数速查矩阵”就是为此设计的:行是函数名,列是“是否需PARTITION”“是否需ORDER”“是否支持frame”,一眼扫完,就知道该填什么。

5.2 路径二:精读“实际应用场景”章节(第15–19页),把例子改造成你的业务数据

速查表不是概念手册,而是案例集。第15页起的“应用场景”,每个都带完整SQL+注释+结果截图。但别只看——要动手“移植”:

场景原文描述你的改造动作(3步)速查表对应页码
“计算每个产品的月度销售额占比”① 把product字段换成你表里的品类字段名
② 把sales_amount换成你表里的金额字段名
③ 把MONTH(date)换成你表里的时间提取方式(如DATE_FORMAT(date,'%Y-%m'))
P16
“识别用户连续登录天数”① 替换用户ID字段名
② 替换日期字段名
③ 检查日期格式是否为DATE类型(若为字符串,需先STR_TO_DATE()转换)
P17
“动态TOP N排行榜(N可变)”① 把WHERE rn <= 3中的3换成你业务需要的数字
② 若需按不同维度TOP N(如“各城市TOP5”),确认PARTITION BY字段已替换
P18

从那以后我每次拿到新需求,都先翻速查表P15–P19,找最接近的案例,然后只改3个地方:字段名、表名、参数值。90%的复杂查询,10分钟内就能写出初版。不是因为多聪明,而是速查表把“模式”提炼出来了——你只需要做填空题,不用从零造轮子。

5.3 路径三:攻克“frame子句”这个最后关卡(第22–24页)

很多新手学到这里就停了,觉得“能用就行”。但真正的效率跃迁,发生在理解frame_clause之后。它决定了聚合类窗口函数“看多大一片数据”。速查表第22页用一张对比表讲清了所有组合:

frame写法含义说明典型用途
ROWS UNBOUNDED PRECEDING从分区第一行算到当前行累计求和、累计计数
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING取当前行+前1行+后1行(共3行)3日移动平均、平滑异常值
RANGE BETWEEN INTERVAL 7 DAY PRECEDING AND CURRENT ROW按日期值取7天内所有行(非固定行数)“近7天活跃用户数”(自动忽略无数据日期)

关键区别:ROWS按物理行数切,RANGE按排序值切。比如按日期排序,ROWS BETWEEN 1 PRECEDING AND CURRENT ROW永远取2行;而RANGE BETWEEN INTERVAL 1 DAY PRECEDING AND CURRENT ROW会取“昨天+今天”所有记录,哪怕昨天有10条数据也全算进来。速查表第23页有个“日期类frame避坑清单”,明确写了哪些数据库支持INTERVAL,哪些必须用NUMERIC替代——照着抄,不踩坑。

希望帮到你。

本文还有配套的精品资源,点击获取