【FDE系列】阶段2:Day 33:进阶查询 — 窗口函数与 CTE

📚前言

📒FDE系列内容总纲:

【大纲】FDE 前沿部署工程师学习系列教程-CSDN博客

🚄前置课程列表:

见文档结尾附录。


🚀阶段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 取组内 TopNAVG/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 自带的驱动没问题):

  1. 子查询 找出温度高于全部设备平均值的设备;再用 CTE 改写一遍,对比哪种好读

  2. 给所有设备按振动值从高到低用 ROW_NUMBER 排名

  3. 找出每个车间温度最高的那台设备(PARTITION BY + ROW_NUMBER + rn=1)

  4. 用多段 CTE:先算每个工程师的工单数,再找出工单数最多的工程师

  5. 按优先级给工单排名,每个优先级只看最新的 1 张PARTITION BY priority ORDER BY created_at DESC

  6. 进阶挑战:列出每台设备的温度,以及它比"本车间平均温度"高多少(提示:温度 - 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博客

【FDE系列】阶段2:Day 31:SQL 基础 --- 增删改查一把梭-CSDN博客

【FDE系列】阶段2:Day 32:多表查询 --- JOIN 与聚合-CSDN博客

相关推荐
张彦峰ZYF1 小时前
AI时代Java工程师能力地图:哪些会被替代,哪些更加值钱
人工智能·架构·被替代的不是岗位而是任务·生成成本塌陷,验证成本没降·上下文闭合度·可判定性、可逆性·责任可归属性
棣廷1 小时前
初识OpenCV——疲劳检测
人工智能·opencv·目标检测
byte轻骑兵1 小时前
【BlueZ 】Linux 内核蓝牙子系统入门:hci_core 模块与 BlueZ 的交互
linux·人工智能·bluez·电脑蓝牙·嵌入式蓝牙
小白勇闯网安圈1 小时前
第2章 状态 State 详解
python·langchain
Dr_Fourier1 小时前
AWQ量化
c++·人工智能·pytorch·ai
Casbin开源社区1 小时前
OpenAgent 详解:单二进制自托管 AI Agent 平台,30+ 模型接入、RAG 知识库、MCP 工具调用与 Casbin 工具权限
人工智能·golang·开源
BJ_Bonree1 小时前
博睿数据加入ITSS分会,成为国家级信息技术服务标准化体系单位成员!
大数据·运维·数据库·人工智能·可观测性
派大_星1 小时前
基于MediaPipe和传统机器学习的手势识别
python
河图洛水1 小时前
Behavior-1k:2026 挑战赛纯仿真 benchmark
人工智能·机器人