当你第一次写 SQL 时,你可能觉得它不过是"查数据的工具"。
但当你第一次要面对真实业务里的报表需求,比如:
- 用户维度统计
- 产品维度统计
- 日期维度统计
- 层级汇总(天 → 月 → 年)
- 多维交叉分析(如按照区域 × 产品统计收入)
- 行级趋势分析(环比、同比、累计、排名)
- 与维度表拼接构建完整事实
- 多张结果集合并
- 一条 SQL 逻辑拆成模块化结构
- 以及最后的性能优化......
你才会意识到一件事:
GG,我的SQL没学好
一、问题起点:从一张简单的 orders 表开始
假设你有一张订单表:
scss
orders(order_id, user_id, product, amount, date)
老板问你一个最简单的问题:
"每个用户一共花了多少钱?"
于是你写下 SQL 世界的第一条聚合:
sql
SELECT user_id, SUM(amount)
FROM orders
GROUP BY user_id;
恭喜,Group By 出现了。
但这只是开始。
二、当用户维度不够,你开始进入"多维度聚合"世界
老板继续问:
"那按日期算呢?比如每天收入多少?"
于是你又写:
sql
SELECT date, SUM(amount)
FROM orders
GROUP BY date;
看起来很正常,对吧?
但是你的脑子里出现一个念头:
"为什么我要写两条 SQL?能不能一次搞定?"
很好,这就是进入"多维聚合"的契机。
三、当你想「一次聚合多个维度」,GROUPING SETS 就出现了
你想把这三种结果一次性算出来:
- 按 user 聚合
- 按 product 聚合
- 全表总额
如果用传统写法,你需要 3 条 SQL。
但 SQL 提供了更优雅的写法:
sql
SELECT user_id, product, SUM(amount)
FROM orders
GROUP BY GROUPING SETS (
(user_id),
(product),
()
);
这里出现了几个重要的知识点:
✔ 什么是 "()"?
表示"无维度聚合",也就是全表总和。
✔ 为什么 GROUPING SETS 重要?
因为任何 BI 系统的核心,就是对数据做多维切片。
GROUPING SETS 正是 SQL 世界里实现多维切片的原语。
你会逐渐理解:
只要聚合维度不止一个,就进入多维分析领域。
四、当维度有层级关系,你需要 ROLLUP
老板提出新需求了:
"给我看每个月的销售额。
再给我看每年的总额。
最后给我一个所有数据的总结。"
你可能会写三层 GROUP BY,但是,不优雅、容易出错、重复逻辑
幸亏,SQL 已经有现成的解决方案:ROLLUP
sql
SELECT year, month, SUM(amount)
FROM sales
GROUP BY ROLLUP (year, month);
ROLLUP 会自动生成三类行:
- (年, 月) → 明细
- (年, NULL) → 年度总额
- (NULL, NULL) → 全局总额
也就是说:
ROLLUP = 顺着层级逐层往上汇总。
这个能力在:
- 日期层级(天 → 月 → 季 → 年)
- 地区层级(城市 → 省份 → 国家)
- 组织层级(部门 → BU → 公司)
五、当维度不仅有层级还有组合,你需要 CUBE
老板说:
"按区域 + 产品看销量,同时还要:
区域汇总、产品汇总、全部汇总。"
这就是典型的二维分析表。
SQL 里一句话:
sql
SELECT region, product, SUM(amount)
FROM sales
GROUP BY CUBE(region, product);
CUBE 会生成:
- (region, product)
- (region, NULL)
- (NULL, product)
- (NULL, NULL)
换句话说:
CUBE = 构建一个完整的 OLAP 多维立方体。
六、当 SQL 越写越长,你必须学会 CTE(WITH)
你发现三件事:
- 多维聚合越来越多
- 中间逻辑必须拆步骤
- 代码如果不拆,会像屎山一样越堆越乱
于是 CTE 出现了:
sql
WITH user_sum AS (
SELECT user_id, SUM(amount) AS total
FROM orders
GROUP BY user_id
),
product_sum AS (
SELECT product, SUM(amount) AS total
FROM orders
GROUP BY product
)
SELECT ...
CTE 的真正意义不是"功能",而是:
把不可读的 SQL 拆成可读的结构化模块。
它让:
- 复杂 SQL 更好维护
- 逻辑更清晰
- 难度降低一个量级
七、当你需要补全信息,JOIN 是必修课
比如订单表里只有 user_id,想知道用户名称:
sql
SELECT o.*, u.name
FROM orders o
JOIN users u ON o.user_id = u.id;
JOIN 的意义是什么?
事实表(orders)只记录"发生了什么",维度表(users)补充"是谁"。
如果想保留所有用户(即使没订单):
sql
LEFT JOIN
如果要表达自身内部关系(例如员工 → 上级):
sql
employees e1 JOIN employees e2
JOIN 本质就是构建:
一个完整的业务语义模型。
没有 JOIN,就没有可理解的数据世界。
八、当你需要"每一行都继续分析",窗口函数是最强的武器
窗口函数是 SQL 的进阶门槛。
当 GROUP BY 解决不了问题时,就是它出场的时候。
例如:
老板问:
"用户内部按订单金额排序。"
sql
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY amount DESC)
想看每天销售额的累计曲线:
sql
SUM(amount) OVER (ORDER BY date)
想做环比分析:
sql
LAG(amount) OVER (ORDER BY date)
窗口函数的核心特性:
不改变行数,但为每行增加分析能力。
这是做趋势分析、排名分析、同比/环比的必备工具。
九、当你开始构建报表体系,你会遇到数据建模
到这里你会自然感受到:
- orders 是事实表
- users/products/date 是维度表
- JOIN 是事实和维度的桥梁
- ROLLUP/CUBE 是 OLAP 的 SQL 实现
而星型模型长这样:
markdown
dim_user
│
dim_product ─── fact_order ─── dim_date
│
dim_region
这不是"技术概念",而是你每天写 SQL 的真实结构。
你可能会突然明白:
原来我的 SQL 一直在做 OLAP,只是我不知道。
十、优化
最基本的优化手段只有三件:
1. 用 EXPLAIN 看数据库是怎么执行的
sql
EXPLAIN SELECT ...
它告诉你:
- 是否走索引
- 是否做全表扫描
- 是否正确过滤
2. 给常用字段建立索引
sql
CREATE INDEX idx_user ON orders(user_id);
JOIN、WHERE、GROUP BY 都依赖索引。
3. 理解过滤下推(Predicate Pushdown)
即使你这么写:
sql
SELECT * FROM (
SELECT * FROM orders
) t
WHERE amount > 100;
数据库依然会优化为:
sql
SELECT * FROM orders WHERE amount > 100;
你知道数据库的这层智能,就能写得更放心。
结语
如果你把整个逻辑压缩成一条线,会是这样的:
- GROUP BY
- 多维聚合
- GROUPING SETS
- ROLLUP
- CUBE
- CTE 做结构化
- JOIN 建语义
- 窗口函数做分析
- 事实表/维度表做建模
- EXPLAIN + 索引做性能
SQL学好对于后端来说还是很重要的,因为:
真实业务分析 SQL 从简单到复杂的自然演化规律。