📚前言
📒FDE系列内容总纲:
🚄前置课程列表:
见文档结尾附录。
🚀阶段2·Day 33:进阶查询 --- 窗口函数与 CTE
FDE 学习系列教程 · 第二阶段 · 第 3 周 · Day 3 预计时长:3 小时 | 难度:★★★★☆ | 前置知识:JOIN、GROUP BY、聚合函数
📌 一句话目标:掌握子查询、CTE(WITH 子句)、窗口函数(ROW_NUMBER / RANK / SUM OVER),用一行 SQL 搞定"分组排名""组内 TopN"这类复杂统计。
🧑🤝🧑 开场:一个 GROUP BY 搞不定的需求
先抛个真实需求:老板让你"找出每个车间温度最高的 2 台设备"。
用昨天学的 GROUP BY 试试?你会发现撞墙了:
GROUP BY 一车间
一车间 → 平均温度 77.0,最高温度 91.0 ← 只剩统计数字
哪两台设备?信息没了!
GROUP BY 的特点是"压扁 "------一个车间好几台设备,分组后每个车间只剩一行聚合值,原始的设备明细全没了。你知道一车间最高温是 91,但你不知道那台设备叫 A3。
你真正想要的是这种效果------行一行不少,但每行额外带一个"组内排名":
设备名 | 温度 | 车间 | 组内温度排名
注塑机A3 | 91 | 一车间 | 1 ← 明细还在!
冲压机B1 | 75 | 一车间 | 2
注塑机A1 | 65 | 一车间 | 3
冲压机B2 | 88 | 二车间 | 1
注塑机A2 | 82 | 二车间 | 2
这,就是**窗口函数(Window Function)**干的活。今天难度上了台阶,别慌,一步步来,每一步都给你拆清楚。
📖 一、子查询:把一个查询的结果当成值或表用
在学窗口函数前,先补一块基础------子查询,就是"查询里套查询"。
场景:找出温度高于平均值的设备
"平均值"本身需要一个查询算出来,然后拿它当 WHERE 的比较标准:
SELECT name, temperature
FROM devices_new
WHERE temperature > (
SELECT AVG(temperature) FROM devices_new -- 子查询:先算出平均温度
);
执行顺序(数据库很聪明,先跑括号里的):
第 1 步:SELECT AVG(temperature) FROM devices_new → 80.2
第 2 步:SELECT name,temperature FROM devices_new
WHERE temperature > 80.2
结果:
name | temperature
注塑机A2 | 82.0
注塑机A3 | 91.0
冲压机B2 | 88.0
子查询能出现的三个位置
| 位置 | 例子 | 含义 |
|---|---|---|
| WHERE 里 | WHERE temp > (SELECT AVG...) |
把子查询结果当比较值 |
| SELECT 里 | SELECT name, (SELECT COUNT(*)...) |
每行配一个子查询算出的列 |
| FROM 里 | FROM (SELECT ...) AS t |
把子查询结果当一张临时表 |
❌ 子查询的痛点
子查询能解决问题,但套多了可读性极差,像俄罗斯套娃:
-- 看三层嵌套,谁不头大
SELECT * FROM (
SELECT department, AVG(temperature) AS avg_t FROM (
SELECT d.temperature, e.department
FROM devices_new d JOIN engineers e ON d.engineer_id=e.id
) x GROUP BY department
) y WHERE y.avg_t > 80;
这时候,CTE 就来救场了。
📖 二、CTE(WITH 子句):给子查询起名字,像流水线一样写
CTT 全称 Common Table Expression,公共表表达式。名字唬人,用法就一句话:
📌
WITH 名字 AS (一段查询)------ 先把一段查询结果起个名字,后面就能像用普通表一样用它。
用 CTE 改写刚才"高于平均值"的例子:
WITH avg_temp AS (
SELECT AVG(temperature) AS avg_val FROM devices_new
)
SELECT d.name, d.temperature
FROM devices_new d
CROSS JOIN avg_temp -- 把单行的平均值拼进来
WHERE d.temperature > avg_temp.avg_val;
💡 CTE 的核心价值是可读性。它让你把一个复杂问题拆成几个有名字的步骤,从上往下像读文章一样读 SQL。嵌套子查询是"套娃",CTE 是"流水线"。
多段 CTE 串联:一段喂给下一段
这个写法在真实报表里非常常见。来看个完整例子------统计每个车间的工单数和平均温度:
WITH
-- 第 1 段:先把设备和工程师关联好,起名为 device_info
device_info AS (
SELECT d.id, d.name, d.temperature, e.department
FROM devices_new d
INNER JOIN engineers e ON d.engineer_id = e.id
),
-- 第 2 段:基于第 1 段,按车间数工单
ticket_count AS (
SELECT di.department, COUNT(t.id) AS ticket_num
FROM device_info di
LEFT JOIN tickets t ON t.device_id = di.id
GROUP BY di.department
),
-- 第 3 段:基于第 1 段,按车间算平均温度
summary AS (
SELECT department, AVG(temperature) AS avg_temp
FROM device_info
GROUP BY department
)
-- 最后:把第 2、3 段拼起来输出
SELECT
s.department AS 车间,
tc.ticket_num AS 工单数,
ROUND(s.avg_temp, 1) AS 平均温度
FROM summary s
JOIN ticket_count tc ON s.department = tc.department
ORDER BY tc.ticket_num DESC;
结果:
车间 | 工单数 | 平均温度
一车间 | 3 | 77.0
二车间 | 2 | 85.0
CTE 流水线示意:
原始三表
│
▼
┌─────────────┐
│ device_info │ 第1段:设备+人关联
└──────┬──────┘
┌───┴───────────────┐
▼ ▼
┌──────────────┐ ┌──────────┐
│ ticket_count │ │ summary │ 第2、3段并行统计
└──────┬───────┘ └────┬─────┘
└────────┬───────────┘
▼
最终 JOIN 输出
📌 CTE 两个要点 :① 每段用逗号
,分隔(最后一段不加逗号);② 后面的段可以直接引用前面段的名字。写复杂查询时,先在纸上拆步骤,再一段段翻译成 CTE。
📖 三、窗口函数:不压扁行,也能做统计
正式进入今天的主角。先看它和 GROUP BY 的本质区别:
┌──────────────────────────────────────────────────────────────┐
│ GROUP BY 把同组多行"压扁"成一行,只留聚合值 │
│ 窗口函数 OVER() 行一行不少,每行额外"开一扇窗"看组内信息 │
│ (排名、累计、平均、总和......) │
└──────────────────────────────────────────────────────────────┘
原始数据: GROUP BY:
A1 一车间 65 一车间 → AVG=77(明细没了)
B1 一车间 75
A3 一车间 91 窗口函数:
A2 二车间 82 A1 一车间 65 组内AVG=77 排名3
B2 二车间 88 B1 一车间 75 组内AVG=77 排名2
A3 一车间 91 组内AVG=77 排名1 ← 每行都在!
A2 二车间 82 组内AVG=85 排名2
B2 二车间 88 组内AVG=85 排名1
窗口函数的通用长相
函数() OVER (
PARTITION BY 分组列 -- 可选:按什么分组(每组独立开一扇窗)
ORDER BY 排序列 -- 可选:窗口内按什么排序
)
-
PARTITION BY≈ GROUP BY 的分组,但不压扁行 -
ORDER BY决定窗口内的先后顺序(排名、累加时必须有)
🖥️ 四、三种排名函数:ROW_NUMBER / RANK / DENSE_RANK
先来最简单的------给所有设备按温度从高到低排名(不分组):
SELECT
name,
temperature,
ROW_NUMBER() OVER (ORDER BY temperature DESC) AS row_num,
RANK() OVER (ORDER BY temperature DESC) AS rank_val,
DENSE_RANK() OVER (ORDER BY temperature DESC) AS dense_rank_val
FROM devices_new
ORDER BY temperature DESC;
当前数据没有并列,三个结果一模一样:
设备名 | 温度 | row_num | rank_val | dense_rank_val
注塑机A3 | 91.0 | 1 | 1 | 1
冲压机B2 | 88.0 | 2 | 2 | 2
注塑机A2 | 82.0 | 3 | 3 | 3
冲压机B1 | 75.0 | 4 | 4 | 4
注塑机A1 | 65.0 | 5 | 5 | 5
三者的区别只有在"出现并列"时才看得出来。假设两台设备都是 88 度并列第 2:
设备名 | 温度 | ROW_NUMBER | RANK | DENSE_RANK
注塑机A3 | 91 | 1 | 1 | 1
冲压机B2 | 88 | 2 | 2 | 2 ← 并列第 2
冲压机B3 | 88 | 3 | 2 | 2 ← 三个函数在这里分叉
注塑机A2 | 82 | 4 | 4 | 3
─────────────────────────────────────────
ROW_NUMBER:强行编号 1,2,3,4(绝不并列,平局也分先后)
RANK: 并列同号,之后跳号 1,2,2,4
DENSE_RANK: 并列同号,之后不跳 1,2,2,3
给你一张选择表:
| 函数 | 编号风格 | 适用场景 |
|---|---|---|
ROW_NUMBER() |
1 2 3 4 连续,永不并列 | 取 TopN、去重留一条(最常用) |
RANK() |
1 2 2 4 跳号 | 比赛排名(两个并列亚军就没季军了) |
DENSE_RANK() |
1 2 2 3 不跳号 | 看"第几档"成绩 |
💡 FDE 现场用得最多的是 ROW_NUMBER------因为"每组取最新一条/取 TopN"本质都要求每组恰好 N 行,RANK 遇到并列可能多出一行。
🖥️ 五、PARTITION BY:分组内排名(核心大招)
光全局排名不够,真正的杀手锏是每个组内部分别排名 。回到开场需求:每个车间温度最高的 2 台设备。
WITH ranked AS (
SELECT
e.department AS 车间,
d.name AS 设备名,
d.temperature AS 温度,
ROW_NUMBER() OVER (
PARTITION BY e.department -- 按车间分组(但不压扁!)
ORDER BY d.temperature DESC -- 每个车间内部按温度降序
) AS 组内排名
FROM devices_new d
INNER JOIN engineers e ON d.engineer_id = e.id
)
SELECT 车间, 设备名, 温度, 组内排名
FROM ranked
WHERE 组内排名 <= 2; -- 每组只留前 2 名
结果:
车间 | 设备名 | 温度 | 组内排名
一车间 | 注塑机A3 | 91.0 | 1
一车间 | 冲压机B1 | 75.0 | 2
二车间 | 冲压机B2 | 88.0 | 1
二车间 | 注塑机A2 | 82.0 | 2
这个"分组 TopN"模板请背下来
┌────────────────────────────────────────────────────────┐
│ WITH 排名表 AS ( │
│ SELECT ..., │
│ ROW_NUMBER() OVER ( │
│ PARTITION BY 分组列 │
│ ORDER BY 排序列 DESC │
│ ) AS rn │
│ FROM ... │
│ ) │
│ SELECT * FROM 排名表 WHERE rn <= N; ← 每组取前 N │
└────────────────────────────────────────────────────────┘
这个套路能解决一大批业务问题:
-
每个车间温度最高的 2 台设备
-
每个客户最近的 3 笔订单
-
每个部门工资最高的人
-
每台设备最新的一条告警记录(
ORDER BY created_at DESC取 rn=1) -
每个分类下销量 Top 5 的商品
📌 PARTITION BY vs GROUP BY 一句话区分:
GROUP BY 车间:一车间 3 行 → 1 行(明细消失)
PARTITION BY 车间:一车间 3 行 → 还是 3 行,每行多个"组内排名"
🖥️ 六、SUM / AVG OVER:让每行带着组的统计值
窗口函数不止能排名,还能在每行上附加聚合值,且不损失明细。
需求:列出每台设备,同时显示它所在车间的平均温度和总温度:
SELECT
d.name AS 设备名,
d.temperature AS 温度,
e.department AS 车间,
ROUND(AVG(d.temperature) OVER (PARTITION BY e.department), 1) AS 车间均温,
SUM(d.temperature) OVER (PARTITION BY e.department) AS 车间总温,
COUNT(*) OVER (PARTITION BY e.department) AS 车间设备数
FROM devices_new d
INNER JOIN engineers e ON d.engineer_id = e.id
ORDER BY e.department, d.temperature DESC;
结果:
设备名 | 温度 | 车间 | 车间均温 | 车间总温 | 车间设备数
注塑机A3 | 91.0 | 一车间 | 77.0 | 231.0 | 3
冲压机B1 | 75.0 | 一车间 | 77.0 | 231.0 | 3 ← 同组的统计值,每行都带一份
注塑机A1 | 65.0 | 一车间 | 77.0 | 231.0 | 3
冲压机B2 | 88.0 | 二车间 | 85.0 | 170.0 | 2
注塑机A2 | 82.0 | 二车间 | 85.0 | 170.0 | 2
这在做报表时特别有用:既显示每台设备的明细,又在同一行给出"它所在组的整体水平",方便对比"这台设备比车间平均高还是低"。
钻研不通也别死磕,窗口函数完整语法一次消化不了正常,先把两个最常用的模式记住:
ROW_NUMBER + PARTITION BY取组内 TopN 、AVG/SUM OVER (PARTITION BY)带组统计。其余的(移动平均、LAG/LEAD 取上下行等)后面用到时再回来查。
窗口函数分类速查(混个眼熟即可)
| 类别 | 函数 | 干嘛用 |
|---|---|---|
| 排名 | ROW_NUMBER / RANK / DENSE_RANK |
编号、排名 |
| 聚合 | SUM/AVG/COUNT/MAX/MIN OVER(...) |
不压扁行做聚合 |
| 取值 | FIRST_VALUE / LAST_VALUE |
取窗口内第一/最后一行的值 |
| 偏移 | LAG / LEAD |
取上一行 / 下一行的值(算环比常用) |
| 分布 | NTILE(n) |
把结果平均切成 n 份 |
🖥️ 七、综合实战:工单分析组合拳
把今天学的串起来。需求:找出每个上报人最新的那一张工单(每人只看最近 1 条)。
WITH ranked_tickets AS (
SELECT
e.name AS 上报人,
t.title AS 标题,
t.priority AS 优先级,
t.created_at AS 创建时间,
ROW_NUMBER() OVER (
PARTITION BY t.reporter_id -- 按上报人分组
ORDER BY t.created_at DESC -- 组内按时间倒序,最新的排第1
) AS rn
FROM tickets t
INNER JOIN engineers e ON t.reporter_id = e.id
)
SELECT 上报人, 标题, 优先级, 创建时间
FROM ranked_tickets
WHERE rn = 1; -- 每人最新的一张
结果:
上报人 | 标题 | 优先级 | 创建时间
李工 | 例行保养 | 低 | ...
王工 | 振动超标 | 中 | ... ← 王工有2张,只留时间最新的
赵工 | 温度严重超标 | 高 | ...
📌 这是 FDE 数据处理的高频模式------"每组最新一条 "。设备的最新状态、订单的最新进度、用户的最近登录,全是
PARTITION BY 主体 ORDER BY 时间 DESC+rn = 1。
📝 本课小结
| 知识点 | 一句话记住 |
|---|---|
| 子查询 | 查询套查询,结果可当值/表用 |
| CTE | WITH 名字 AS(...),把复杂查询拆成命名步骤,像流水线 |
| 窗口函数 | 函数() OVER(PARTITION BY... ORDER BY...),不压扁行 |
| ROW_NUMBER | 连续编号不并列,取 TopN 首选 |
| RANK | 并列跳号 1,2,2,4 |
| DENSE_RANK | 并列不跳 1,2,2,3 |
| PARTITION BY | 分组但保留明细(区别于 GROUP BY 压扁) |
| 组内 TopN 模板 | CTE 里 ROW_NUMBER 编号,外层 WHERE rn<=N |
| 最新一条 | PARTITION BY 主体 ORDER BY 时间 DESC + rn=1 |
| 聚合窗口 | AVG/SUM OVER(PARTITION BY...) 每行带组统计 |
🧠 核心认知:GROUP BY 是"望远镜",只看每组的汇总;窗口函数是"放大镜+标尺",每一行都在,还能标出它在组里的位置。CTE 则让复杂 SQL 变得能读、能改、能维护。
📋 课后练习
在 DBeaver 里完成(窗口函数 SQLite 3.25+ 支持,DBeaver 自带的驱动没问题):
-
用子查询 找出温度高于全部设备平均值的设备;再用 CTE 改写一遍,对比哪种好读
-
给所有设备按振动值从高到低用 ROW_NUMBER 排名
-
找出每个车间温度最高的那台设备(PARTITION BY + ROW_NUMBER + rn=1)
-
用多段 CTE:先算每个工程师的工单数,再找出工单数最多的工程师
-
按优先级给工单排名,每个优先级只看最新的 1 张 (
PARTITION BY priority ORDER BY created_at DESC) -
进阶挑战:列出每台设备的温度,以及它比"本车间平均温度"高多少(提示:温度 - AVG OVER)
🔭 下节预告
三天下来,查询的本事学了不少。明天进入 FDE 最"脏"也最接地气的工作------数据清洗。
客户发来一份 Excel 导出:有完全重复的行、温度写成中文"八十二"、手机号一会儿带横杠一会儿空着、设备名大小写空格五花八门......这种数据直接进库就是灾难。明天教你去重、补缺失值、格式对齐、类型转换、数据脱敏,把一坨脏数据洗成漂漂亮亮的结构化数据。准备好当"数据清洁工",明天见!
🌍附录:前置课程列表
阶段一:
【FDE系列】阶段1Day 1:AI 层级关系 --- 四个嵌套的圈-CSDN博客
【FDE系列】阶段1Day 2:AI 三阶段发展史 --- 会认 → 会判断 → 会创造-CSDN博客
【FDE系列】阶段1Day 3:符号 AI vs 机器学习 --- 两条路线的本质区别-CSDN博客
【FDE系列】阶段1Day 4:Transformer 的历史意义 --- 2017 年的分水岭-CSDN博客
【FDE系列】阶段1Day 5:本周复习与自测 --- 检验你的 AI 认知地基-CSDN博客
【FDE系列】阶段1Day 6:Transformer 架构 --- 一张图纸盖出千千万万栋楼-CSDN博客
【FDE系列】阶段1Day 7:LLM 本质 --- 文字接龙机器-CSDN博客
【FDE系列】阶段1Day 8:Token --- 模型眼中的最小单位-CSDN博客
【FDE系列】阶段1Day 9:AI 幻觉 --- 为什么会一本正经地胡说八道-CSDN博客
【FDE系列】阶段1Day 10:上下文窗口 --- 模型的记忆力上限 + 本周复习-CSDN博客
【FDE系列】阶段1Day 11:Prompt --- 给模型立规矩-CSDN博客
【FDE系列】阶段1Day 12:Memory --- 让模型记住上下文
【FDE系列】阶段1Day 13:RAG --- 给模型配图书管理员-CSDN博客
【FDE系列】阶段1Day 14:Tool Use --- 让模型动手操作-CSDN博客
【FDE系列】阶段1Day 15:MCP --- 统一的工具接口标准 + 第三周复习-CSDN博客
【FDE系列】阶段1Day 16:什么是 FDE --- 把 AI 变成客户结果的人-CSDN博客
【FDE系列】阶段1Day 17:FDE vs 传统实施 --- 三大本质区别-CSDN博客
【FDE系列】阶段1Day 18:FDE 三重身份 + C6 胜任力模型-CSDN博客
【FDE系列】阶段1Day 19:七阶段行动路径 + 行业经验的价值-CSDN博客
【FDE系列】阶段1Day 20:阶段总结与产出物 --- 第一阶段收官-CSDN博客
阶段二:
【FDE系列】阶段2:Day 21:Python 环境搭建 --- 写出你的第一行代码-CSDN博客
【FDE系列】阶段2:Day 22:变量、数据类型、条件判断 --- Python 的"记忆"和"判断"-CSDN博客
【FDE系列】阶段2:Day 23:循环与函数 --- 让代码跑 100 遍、把逻辑打包复用-CSDN博客
【FDE系列】阶段2:Day 24:数据结构 --- 列表、字典、集合、元组-CSDN博客
【FDE系列】阶段2:Day 25:文件读写与 JSON --- 让程序连通外部数据(第一周收官)-CSDN博客
【FDE系列】阶段2:Day 26:模块化编程 --- 把代码拆成"抽屉柜"-CSDN博客
【FDE系列】阶段2:Day 27:异常处理与日志 --- 让程序"摔不烂、查得到"-CSDN博客
【FDE系列】阶段2:Day 28:FastAPI 入门 --- 把你的函数变成 API 服务-CSDN博客
【FDE系列】阶段2:Day 29:FastAPI 进阶 --- Pydantic 模型与完整 CRUD 实战-CSDN博客
【FDE系列】阶段2:Day 30:生产代码规范 --- 测试、类型注解、配置管理(第二周收官)-CSDN博客