Skip to content

元数据卡

  • 前置知识:Vol 5 第1章(数据库基础)
  • 预计时间:45 分钟
  • 核心难度:进阶
  • 阅读模式:高度专注
  • 完成标志:能用窗口函数、CTE、OLAP 查询完成多步骤分析任务

你的进度

通过 EDA 你知道了数据的大致形貌——哪几列值得深挖,哪几列几乎全是空值。但预言厅的情报官看了你的图表摇了摇头:'图表漂亮,但我没法从里面查到具体数字。帮我查一下:过去三个月,哪个堡垒的任务完成率最高?'

你发现 pandas 做这种跨表聚合很别扭。你需要的是一把趁手的查询武器——SQL 的分析模式。

你的任务

之前在 Vol 5 里你用 SQL 做 CRUD——插入、更新、删除、简单查询。那是 MySQL 里的日常操作,每次查几行,最多几百行。现在你的数据在数仓里,每次查询扫描几百万行,你需要做跨分组的累计统计、移动平均、同环比。普通的 SELECT-GROUP BY 不够用了。这一章给你分析型 SQL 的武器库。


从行操作到集合操作

分析型 SQL 和事务型 SQL 的区别在于思考方式:

  • 事务型 SQL:我需要这一行,把这个字段改了
  • 分析型 SQL:我需要这一组数据,跨行计算一个值

你在 Vol 5 里用 GROUP BY 做过分组聚合。但 GROUP BY 有一个限制:分组后每组的明细行被折叠成一行聚合值。你想在保持每一行数据的在旁边加上分组聚合结果——这就要用窗口函数。

窗口函数

窗口函数不折叠行,它在每一行旁边计算一个"窗口"内的聚合值。

sql
-- 基础语法:
-- 函数名() OVER (PARTITION BY 分组列 ORDER BY 排序列)

看一个具体的例子。假设你有一张任务日志表:

sql
-- 创建示例表
CREATE TABLE mission_logs (
    mission_id INT,
    log_date DATE,
    resources_used INT
);

INSERT INTO mission_logs VALUES
(1, '2026-06-01', 100),
(1, '2026-06-02', 120),
(1, '2026-06-03', 110),
(2, '2026-06-01', 200),
(2, '2026-06-02', 220);

-- 你想看:每个任务每天的用量,以及这个任务到目前为止的总用量
SELECT
    mission_id,
    log_date,
    resources_used,
    SUM(resources_used) OVER (
        PARTITION BY mission_id
        ORDER BY log_date
    ) AS running_total
FROM mission_logs;

结果:

mission_id | log_date   | resources_used | running_total
-----------+------------+----------------+--------------
1          | 2026-06-01 | 100            | 100
1          | 2026-06-02 | 120            | 220
1          | 2026-06-03 | 110            | 330
2          | 2026-06-01 | 200            | 200
2          | 2026-06-02 | 220            | 420

看到区别了吗?GROUP BY 只能给你两个聚合值(任务 1 = 330,任务 2 = 420)。而窗口函数在保留每行数据的在右侧添加了运行累计值。

常见的窗口函数用途:

sql
-- 排名:每个任务内部的资源使用排名
SELECT
    mission_id, log_date, resources_used,
    ROW_NUMBER() OVER (PARTITION BY mission_id ORDER BY resources_used DESC) AS rank
FROM mission_logs;

-- 移动平均:过去 3 天平均资源用量
SELECT
    mission_id, log_date, resources_used,
    AVG(resources_used) OVER (
        PARTITION BY mission_id
        ORDER BY log_date
        ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    ) AS moving_avg_3d
FROM mission_logs;

-- 滞后:取前一天的值做环比
SELECT
    mission_id, log_date, resources_used,
    LAG(resources_used, 1) OVER (
        PARTITION BY mission_id
        ORDER BY log_date
    ) AS prev_day,
    resources_used - LAG(resources_used, 1) OVER (
        PARTITION BY mission_id
        ORDER BY log_date
    ) AS daily_change
FROM mission_logs;

CTE:把多步查询拆成可读的步骤

分析型查询往往不是一个 SELECT 能搞定的。你需要先算 A,再基于 A 算 B,再基于 B 算 C。CTE(Common Table Expression)让你按步骤写,每个步骤有名字。

sql
WITH daily_usage AS (
    -- 第一步:计算每日使用量总和
    SELECT
        log_date,
        SUM(resources_used) AS total_resources
    FROM mission_logs
    GROUP BY log_date
),
weekly_avg AS (
    -- 第二步:计算每周 7 天移动平均
    SELECT
        log_date,
        total_resources,
        AVG(total_resources) OVER (
            ORDER BY log_date
            ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
        ) AS weekly_ma
    FROM daily_usage
)
-- 第三步:找出低于均值 20% 的异常日
SELECT *
FROM weekly_avg
WHERE total_resources < weekly_ma * 0.8;

CTE 最大好处是每个步骤可以独立测试。先跑 daily_usage,确认结果对,再加下一步。

OLAP 操作:ROLLUP 和 CUBE

当你想从不同维度层级看数据时(总聚合、按类型聚合、按类型+地区聚合),不需要写三个 UNION ALL。用 ROLLUP

sql
SELECT
    mission_type,
    location,
    SUM(resources_used) AS total
FROM mission_logs
GROUP BY ROLLUP (mission_type, location);

结果会返回:

  • 总聚合一行(mission_type = NULL, location = NULL)
  • 按 mission_type 聚合的行(location = NULL)
  • 按 mission_type + location 详细聚合的行

CUBE 更彻底,它会生成所有可能的维度组合聚合。

在 Python 中写分析型 SQL

Pandas 的 groupby + transform 等价于 SQL 窗口函数:

python
import pandas as pd
df = pd.read_sql("SELECT * FROM mission_logs", engine)

# 等价于 SUM OVER (PARTITION BY mission_id ORDER BY log_date)
df["running_total"] = df.groupby("mission_id")["resources_used"].cumsum()

# 等价于移动平均
df["moving_avg_3d"] = (
    df.groupby("mission_id")["resources_used"]
    .transform(lambda x: x.rolling(3, min_periods=1).mean())
)

常见陷阱

  • 窗口函数中忘记 PARTITION BY。结果是全局排序 + 聚合,不是你想要的组内计算。
  • 窗口函数 ORDER BY 的默认窗口是从开头到当前行。如果你只想看当前行(比如排名、移动平均),需要显式指定窗口范围。
  • CTE 里用了不可重复使用的临时表名,导致调试困难。

通关挑战

  • 热身:在 missions 表上写一个窗口函数,计算每个团队成员的平均任务完成时间。
  • 挑战:用 CTE + 窗口函数写一个完整的分析查询:找出连续 3 天资源使用量异常偏高(超出 30 天移动平均的 2 倍标准差)的任务。
  • 观察:把一个 5 步骤的 CTE 查询逐步拆解,每一步输出到中间表,验证每一步的中间结果。

验收标准

  • 能写出包含 PARTITION BYORDER BY 的窗口函数
  • 能用 CTE 把多步分析查询拆成可读的步骤
  • 理解 ROLLUPCUBE 的作用
  • 能在 Python 中用 pandas 复现等价的窗口计算

旅人笔记

SQL 窗口函数是你处理分析问题时最趁手的武器。学会它,你的 SQL 能力从"能查"变成"能分析"。


下一站预告

上一章你从数据中看到了模式。但有些模式是真是假?下一章,用统计推断做判断。

Built with VitePress | Software Systems Atlas