SQL窗口函数入门:像Excel一样写高效数据库查询

发布时间:2026/10/31 18:06:25
SQL窗口函数入门:像Excel一样写高效数据库查询

简介:这是一份专为数据库从业者设计的《SQL窗口函数速查表》PDF文档,面向数据库管理员、数据分析师、开发工程师及SQL进阶学习者,旨在解决复杂数据分析场景下窗口函数选型难、语法易混淆、实际应用无参照等痛点。文档系统梳理了排名函数(ROW_NUMBER/RANK/DENSE_RANK)、偏移函数(LEAD/LAG)与聚合类窗口函数(SUM/AVG/COUNT等)三类核心用法,详解OVER子句中PARTITION BY、ORDER BY与FRAME CLAUSE的组合逻辑,并配以典型业务场景示例(如分组排序、同比环比、滚动统计),便于即查即用。资源为单文件PDF格式,共1个841KB文件,内容排版紧凑、分类清晰、示例可直接复现,适合作为开发调试时的桌面参考或学习过程中的记忆锚点。目前已有255人下载学习,是兼顾准确性、实用性与便携性的SQL高阶技能提效工具。

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

你有没有试过在Excel里做这样的事:

  • 给销售数据按地区分组,然后给每个地区的每条记录自动编个序号(从1开始连续排);
  • 算出每个员工当月工资,再顺手标出“比上个月涨了还是跌了”,直接用上一行的数据;
  • 不仅要算出每个部门的平均薪资,还要把这平均值“贴”到每个人那行里,方便对比——而不是只返回一个数字、把原始名单全吞掉。

这些操作,你在Excel里点几下就能搞定:选中区域 → 插入透视表 → 写个=OFFSET或=INDEX+MATCH,甚至直接拖填充柄。但一旦数据量上百万、要跑在数据库里、还得嵌进报表系统自动刷新……Excel就歇菜了。这时候,SQL窗口函数就是你的「数据库版Excel公式下拉键」——它不改变原始行数,不合并数据,也不丢细节,而是一边保留每一行的完整信息,一边给每行“悄悄算好一个新值”。

它不是什么黑匣子玄学,也不是只有DBA才配碰的高阶技巧。你每天用的“TOP 10销量商品”“近30天日活环比”“用户首次下单后第7天是否复购”……背后八成靠它撑着。这份《SQL窗口函数速查表.pdf》,就是一张印满“常用公式写法+参数说明+抄了就能跑”的便携卡片——没有长篇大论讲执行计划,不扯底层B+树索引,就干一件事:让你今天下午下班前,写出第一个能跑通的ROW_NUMBER() OVER (PARTITION BY region ORDER BY sales DESC)。适合完全没听过“窗口”、连OVER()括号都打不全的新手;也适合写了三年SQL却总在RANK()和DENSE_RANK()之间反复横跳、每次上线都提心吊胆的半熟手。


2. 为什么非得学窗口函数?——告别三层嵌套子查询、临时表和“先查再拼”的土办法

2.1 传统SQL处理“组内计算”的三大翻车现场

我们先看一个真实场景:某电商平台想统计“每个品类下销量最高的前3款商品”。老办法怎么写?

-- ❌ 方法一:用子查询 + 相关子查询(慢到哭) SELECT category, product_name, sales FROM products p1 WHERE ( SELECT COUNT(*) FROM products p2 WHERE p2.category = p1.category AND p2.sales > p1.sales ) < 3;

问题在哪?

  • 每查一行p1,都要全表扫一遍p2找比它销量高的——10万行数据,实际执行10万×10万次比较;
  • COUNT(*)里不能加索引,纯CPU硬扛;
  • 代码像俄罗斯套娃,改个排序字段得翻三页。
-- ❌ 方法二:用临时表 + JOIN(维护噩梦) CREATE TEMP TABLE top3_per_cat AS SELECT category, product_name, sales, ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) AS rn FROM products; SELECT category, product_name, sales FROM top3_per_cat WHERE rn <= 3;

问题在哪?

  • 多建一张表,就得管它的生命周期(忘删?占空间;删早了?下游报错);
  • 如果要加“同销量并列第2”,还得换RANK(),临时表逻辑全重写;
  • 每次需求变,都得改DDL+DML两套脚本。

提示:这两种写法在小数据量(<1万行)时还能凑合,但只要业务增长、报表并发一上来,数据库负载立刻飙升——这不是你SQL写得差,是工具没选对。

2.2 窗口函数怎么一招破局?——三行代码,一次扫描,结果自带“上下文”

现在用窗口函数重写:

-- ✅ 一行核心逻辑,清晰、高效、可读 SELECT category, product_name, sales FROM ( SELECT category, product_name, sales, ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) AS rn FROM products ) t WHERE rn <= 3;

它背后的执行逻辑,就像Excel里你选中整列→点“排序”→再点“填充序列”:

  • PARTITION BY category:相当于“按品类分组”,每组独立编号;
  • ORDER BY sales DESC:组内按销量从高到低排;
  • ROW_NUMBER():从1开始,挨个发号(1,2,3…),绝不重复、绝不跳空;
  • 最外层WHERE rn <= 3:只取每组前3名——注意!这里过滤的是“计算后的结果列”,不是原始数据。

关键优势:
✅单次扫描:数据库引擎遍历products表1次,边读边算序号,不用回表、不建临时结构;
✅语义直白:看到PARTITION BY就知道分组,看到ORDER BY就知道排序依据,看到ROW_NUMBER()就知道编号规则;
✅灵活切换:想改成“销量并列第2也算第2名”?把ROW_NUMBER()换成RANK()就行,其他代码不动;
✅零学习成本迁移:你已经会写SELECT ... FROM ... WHERE ...,窗口函数只是在SELECT里多加了个带OVER()的表达式。

2.3 它真能替代90%的“复杂报表逻辑”吗?——看这几个高频场景你就信了

场景以前怎么搞现在怎么写省了多少事
同比/环比:本月销售额 vs 上月先查本月,再查上月,用LEFT JOIN拼日期,手动算差值LAG(sales, 1) OVER (ORDER BY month)直接取上月值不用写两个SELECT,不用JOIN,日期错位风险归零
移动平均:过去7天日均订单量建7张临时表,每天一张,再UNION ALL后AVG()AVG(order_cnt) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)一行搞定,滑动窗口自动伸缩,不用管“哪天没数据”
分组累计:每个客户从注册到现在的总消费额用自关联a.id >= b.id暴力求和,性能崩盘SUM(amount) OVER (PARTITION BY customer_id ORDER BY order_date)索引友好,千万级数据秒出
Top N per Group:每个城市最贵的3套房嵌套3层子查询,加GROUP_CONCAT拼字符串再解析ROW_NUMBER() OVER (PARTITION BY city ORDER BY price DESC)代码长度缩短2/3,执行时间从分钟级降到毫秒级

你会发现:这些不是“炫技场景”,而是日报、周报、BI看板里天天出现的刚需。而窗口函数,就是SQL世界里那个“本该存在、却晚来了十年”的基础能力——它不取代GROUP BY,而是补上GROUP BY做不到的事:既要分组聚合,又要保留明细。


3. 第一步怎么迈?——从安装环境到跑通第一个ROW_NUMBER(),5分钟实操指南

3.1 你不需要装任何新软件,用现有数据库就能开练

很多新手一看到“SQL高级功能”就默认要升级数据库、装插件、改配置……其实大可不必。窗口函数是SQL:2003标准的一部分,主流数据库早已原生支持:

  • ✅PostgreSQL 8.4+(2008年就支持,现在用的都是12/14/15)
  • ✅MySQL 8.0+(2018年发布,目前企业主力版本基本全覆盖)
  • ✅SQL Server 2005+(ROW_NUMBER()最早就是它带火的)
  • ✅Oracle 8i+(更早,但语法略有差异)
  • ✅SQLite 3.25+(2018年加入,轻量级项目够用)

注意:如果你用的是MySQL 5.7或更早版本,确实不支持——但别急着重装!先确认你连接的是哪个实例:公司测试库、本地Docker镜像、还是云服务后台?很多云厂商(如阿里云RDS、腾讯云CynosDB)已默认开启MySQL 8.0,你可能早就“有而不自知”。

3.2 准备一个超简单的测试表(复制粘贴就能跑)

我们不用导入真实业务数据,自己建一张5行的模拟表,足够验证所有逻辑:

-- 创建测试表(MySQL 8.0+ / PostgreSQL / SQL Server 通用写法) CREATE TABLE sales_demo ( id INT PRIMARY KEY, region VARCHAR(20), product VARCHAR(30), amount DECIMAL(10,2) ); -- 插入5条测试数据(故意让region有重复,product有同名) INSERT INTO sales_demo VALUES (1, '华东', '手机A', 5999.00), (2, '华东', '手机B', 3999.00), (3, '华北', '手机A', 5899.00), (4, '华北', '耳机X', 299.00), (5, '华南', '手机B', 4199.00);

执行完后,用SELECT * FROM sales_demo;确认数据长这样:

idregionproductamount
1华东手机A5999.00
2华东手机B3999.00
3华北手机A5899.00
4华北耳机X299.00
5华南手机B4199.00

这就是我们的“练功沙盒”——5行数据,3个地区,3款产品,金额有高低。所有窗口函数实验,都从这张表出发。

3.3 动手写第一个窗口函数:给每个地区销量最高的商品标序号

目标:按地区分组,组内按销量降序排,给每行标个rank_in_region(地区内排名)。

-- ✅ 第一个能跑通的窗口函数(复制即用) SELECT region, product, amount, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rank_in_region FROM sales_demo;

预期结果:

regionproductamountrank_in_region
华东手机A5999.001
华东手机B3999.002
华北手机A5899.001
华北耳机X299.002
华南手机B4199.001

逐部分拆解,确保你真正看懂:

  • ROW_NUMBER():函数名,意思是“给我一个连续不重复的序号”;
  • OVER (...):这是窗口函数的“身份证”,告诉数据库:“接下来我要定义一个计算范围”;
  • PARTITION BY region:把数据按region字段切成几块(华东一块、华北一块、华南一块),每块独立计算;
  • ORDER BY amount DESC:在每块内部,按amount从大到小排——所以华东里手机A(5999)排第1,手机B(3999)排第2;
  • AS rank_in_region:给这个新算出来的列起个名字,方便后续用或展示。

关键理解:ROW_NUMBER()不是对整张表排序后编号,而是“先分块(PARTITION),再在块里排序(ORDER BY),最后块内编号”。这和Excel里“按地区筛选→排序→填序号→取消筛选”逻辑完全一致。

3.4 验证你真的懂了:快速切换三种排名逻辑

现在把ROW_NUMBER()换成另外两个常见函数,观察结果变化——这是检验理解的黄金练习:

-- 对比1:ROW_NUMBER() —— 连续编号,同销量也不同号 SELECT region, product, amount, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rn FROM sales_demo; -- 对比2:RANK() —— 同销量同名次,但跳过后续名次(比如两个第1,下一个就是第3) -- (当前数据没同销量,我们手动改一条:UPDATE sales_demo SET amount=5999.00 WHERE id=3; 再运行) SELECT region, product, amount, RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS rk FROM sales_demo; -- 对比3:DENSE_RANK() —— 同销量同名次,不跳号(两个第1,下一个就是第2) SELECT region, product, amount, DENSE_RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS drk FROM sales_demo;

为什么必须亲手试这三次?
因为文档里写的“RANK跳号、DENSE_RANK不跳号”是结论,而你脑子里形成的画面应该是:

  • RANK()像运动会颁奖:金牌1人,银牌空缺(没人拿),铜牌直接第3名;
  • DENSE_RANK()像班级排名:考100分的3个同学并列第1,下一个98分就是第2名;
  • ROW_NUMBER()像军训报数:不管成绩,按站队顺序喊1、2、3……

只有看到结果差异,你才能条件反射地选对函数——而不是每次写代码前,还得翻文档查区别。


4. 新手必踩的5个坑:现象、原因、一招解决(附错误截图联想)

4.1 坑1:OVER()括号里啥都不写,直接报错“syntax error near OVER”

  • 现象:写SELECT name, ROW_NUMBER() OVER () FROM table;,数据库报错,提示语法错误。
  • 原因:OVER()是窗口函数的强制语法,括号里至少得有ORDER BY(排序依据)。ROW_NUMBER()要求明确“按什么顺序排”,哪怕你只想按主键排,也得写ORDER BY id。
  • 解决:永远记住口诀——“OVER后面不空着,ORDER BY是底线”。如果真不想排序(极少见),用ORDER BY (SELECT NULL)兜底(PostgreSQL/SQL Server支持),但强烈建议显式写ORDER BY id。

4.2 坑2:PARTITION BY字段拼错,结果全乱套却没报错

  • 现象:想按region分组,但误写成PARTITION BY reigon(少个n),查询能跑通,但rank_in_region全是1。
  • 原因:数据库把reigon当成一个不存在的列,返回NULL;PARTITION BY NULL意味着“所有行分到同一组”,所以ROW_NUMBER()在整个表里连续编号,但因为你没写ORDER BY,顺序不确定,看起来像随机。
  • 解决:执行前先用SELECT DISTINCT reigon FROM sales_demo;检查字段名是否存在;或者,在写完PARTITION BY后,立刻补上ORDER BY,双重保险。

4.3 坑3:在WHERE里直接引用窗口函数列,报错“invalid reference to FROM-clause entry”

  • 现象:写SELECT *, ROW_NUMBER() OVER (...) AS rn FROM t WHERE rn <= 3;,报错说rn找不到。
  • 原因:SQL执行顺序是FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY,而窗口函数在SELECT阶段才计算,WHERE阶段它还没出生!
  • 解决:必须用子查询或CTE(公用表表达式)包裹。正确写法:
    -- 子查询方式(兼容性最好) SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rn FROM sales_demo ) t WHERE t.rn <= 3; -- CTE方式(更易读,MySQL 8.0+/PostgreSQL支持) WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rn FROM sales_demo ) SELECT * FROM ranked WHERE rn <= 3;

4.4 坑4:LAG()取上一行,第一行返回NULL,但业务要求填0或默认值

  • 现象:SELECT amount, LAG(amount) OVER (ORDER BY id) AS prev_amount FROM sales_demo;,第一行prev_amount是NULL,下游程序报空指针。
  • 原因:LAG()默认行为就是“没上一行就返回NULL”,这是设计使然,不是bug。
  • 解决:用LAG(..., offset, default_value)第三个参数指定默认值:
    SELECT amount, LAG(amount, 1, 0) OVER (ORDER BY id) AS prev_amount -- 第一行prev_amount=0 FROM sales_demo;
    或者用COALESCE兜底:COALESCE(LAG(amount) OVER (...), 0)。

4.5 坑5:移动平均ROWS BETWEEN写错方向,结果完全反了

  • 现象:想算“截止当前行的累计和”,写了SUM(amount) OVER (ORDER BY id ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING),结果却是下两行的和。
  • 原因:ROWS BETWEEN X AND Y的X必须是起点,Y是终点,且X不能比Y“靠后”。CURRENT ROW AND 1 FOLLOWING是对的(当前+下1行),但1 PRECEDING AND CURRENT ROW才是“上1行+当前行”。初学者常把PRECEDING(之前)和FOLLOWING(之后)记混。
  • 解决:牢记口诀——“PRECEDING是回头看,FOLLOWING是往前瞅”。需要“过去N天”就用N PRECEDING;需要“未来N天”才用N FOLLOWING。调试时,先用ROWS UNBOUNDED PRECEDING(从开头到当前)看累计效果,再逐步收紧范围。

5. 下一步学什么?——从速查表出发的3条渐进路径与我的血泪经验

5.1 路径一:吃透速查表里的“核心函数族”,用2小时建立肌肉记忆

这份PDF不是用来“收藏吃灰”的,而是当作随身词典+错题本用。我建议你按这个顺序精读并动手验证:

阶段重点函数必做练习(基于sales_demo表)验证要点
Day 1 上午ROW_NUMBER(),RANK(),DENSE_RANK()① 按region分组排名;② 按amount全局排名;③ 故意插入两条相同amount,对比三者输出看清“同值时序号是否相同”“后续序号是否跳空”
Day 1 下午LAG(),LEAD()①LAG(amount)取上一行;②LEAD(amount, 2)取下两行;③LAG(amount, 1, 0)设默认值确认NULL出现位置,验证默认值生效
Day 2 上午SUM(),AVG(),COUNT()+OVER①SUM(amount) OVER (PARTITION BY region);②AVG(amount) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW)区分“分组聚合”和“滑动窗口聚合”结果形态

我第一次学时,把PDF打印出来,在每类函数旁手写3行自己的例子(不是抄文档的),然后在数据库里一条条跑。遇到报错不查百度,先看错误提示里哪个单词拼错了——两周后,看到OVER就条件反射想PARTITION和ORDER BY,这才是真正的入门。

5.2 路径二:攻克“帧(frame)”概念——别怕ROWS和RANGE,它们就是Excel的“相对引用”

很多人卡在frame_clause(帧子句),觉得ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING像天书。其实它就是Excel里AVERAGE(B2:B4)的数据库表达:

  • B2:B4=ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING(当前行±1行);
  • B1:B10=ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(从第一行到当前);
  • B5:B5=ROWS BETWEEN CURRENT ROW AND CURRENT ROW(就当前这一行)。

速查表里最关键的帧用法对比表:

场景推荐写法为什么这么写实际效果(以sales_demo按id排序为例)
累计求和(从第一行加到当前行)SUM(amount) OVER (ORDER BY id ROWS UNBOUNDED PRECEDING)UNBOUNDED PRECEDING= 从头开始,CURRENT ROW默认省略id=1时sum=5999;id=2时sum=5999+3999=9998…
移动平均(最近3天)AVG(amount) OVER (ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)2 PRECEDING= 往前推2行,CURRENT ROW= 包含自己,共3行当前行+前两行的平均值
同比计算(今年vs去年)amount - LAG(amount, 12) OVER (ORDER BY year_month)用LAG比ROWS更精准,避免月份缺失导致错位直接取12行前的值相减,不依赖日期连续性

血泪经验:别死记ROWS和RANGE区别(RANGE按值范围,ROWS按物理行数)。95%的业务场景用ROWS就够了。RANGE只在“按金额分段”这类特殊需求才用,新手先跳过。

5.3 路径三:把速查表变成你的“SQL急救包”——3个真实工作流改造案例

学完函数,下一步是用它替换你正在写的低效SQL。我整理了三个高频改造场景,照着做,当天就能感受到性能提升:

案例1:替换“自关联求Top N”的慢查询

  • 原SQL(10万行数据,执行23秒):
    SELECT a.region, a.product, a.amount FROM sales_demo a WHERE 3 > ( SELECT COUNT(*) FROM sales_demo b WHERE b.region = a.region AND b.amount > a.amount );
  • 改造后(同一数据,执行0.08秒):
    SELECT region, product, amount FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rn FROM sales_demo ) t WHERE rn <= 3;

案例2:替换“临时表存中间结果”的维护噩梦

  • 原流程:每天凌晨跑脚本 →CREATE TEMP TABLE tmp1 AS SELECT ...→CREATE TEMP TABLE tmp2 AS SELECT ... FROM tmp1→INSERT INTO report_table SELECT ... FROM tmp2
  • 改造后:单条SQL搞定,无临时表,无DDL风险:
    INSERT INTO report_table SELECT region, AVG(amount) AS avg_amount, SUM(amount) OVER (PARTITION BY region ORDER BY id) AS cumsum_by_region FROM sales_demo GROUP BY region;

案例3:替换“应用层拼接”的脆弱逻辑

  • 原做法:Java代码查出全部销售数据 →for(Product p : list){ p.setRank(getRank(p)); }→ 再传给前端
  • 改造后:数据库直接算好,应用层零逻辑:
    SELECT id, region, product, amount, RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS region_rank, DENSE_RANK() OVER (ORDER BY amount DESC) AS global_rank FROM sales_demo;

从那以后我每次写SQL,只要看到GROUP BY后还想保留明细,或者WHERE里要引用聚合结果,就强制走一遍“能不能用窗口函数重构”的 checklist。不是为了炫技,而是因为——它让代码更短、执行更快、出错更少、交接更容易。希望帮到你。

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