MySQL 实战精通系列 · 第3篇:SQL 核心与复杂查询实战
本篇目标:从"会写 SELECT"提升到"能用 SQL 解决真实电商业务问题",掌握多表连接、子查询、窗口函数、CTE。
文章目录
- [MySQL 实战精通系列 · 第3篇:SQL 核心与复杂查询实战](#MySQL 实战精通系列 · 第3篇:SQL 核心与复杂查询实战)
-
- [一、先搞懂 SQL 执行顺序](#一、先搞懂 SQL 执行顺序)
-
- [1.1 书写顺序 vs 执行顺序](#1.1 书写顺序 vs 执行顺序)
- [1.2 执行顺序图解](#1.2 执行顺序图解)
- 二、多表连接
-
- [2.1 连接类型](#2.1 连接类型)
- [2.2 实战:查询订单及用户信息](#2.2 实战:查询订单及用户信息)
- [2.3 自连接:查询分类树](#2.3 自连接:查询分类树)
- [2.4 多表连接:订单 + 用户 + 订单项 + 商品](#2.4 多表连接:订单 + 用户 + 订单项 + 商品)
- 三、子查询
-
- [3.1 子查询类型](#3.1 子查询类型)
- [3.2 实战:查询有订单的用户](#3.2 实战:查询有订单的用户)
- [3.3 实战:查询没有订单的用户](#3.3 实战:查询没有订单的用户)
- 四、聚合与分组
-
- [4.1 常用聚合函数](#4.1 常用聚合函数)
- [4.2 实战:统计每个用户的订单数和总额](#4.2 实战:统计每个用户的订单数和总额)
- [4.3 WHERE vs HAVING](#4.3 WHERE vs HAVING)
- 五、窗口函数(重点)
-
- [5.1 什么是窗口函数?](#5.1 什么是窗口函数?)
- [5.2 常用窗口函数](#5.2 常用窗口函数)
- [5.3 实战1:每个用户最近一笔订单](#5.3 实战1:每个用户最近一笔订单)
- [5.4 实战2:每个品类销售额 Top 3 商品](#5.4 实战2:每个品类销售额 Top 3 商品)
- [5.5 实战3:每日新增用户与7日留存](#5.5 实战3:每日新增用户与7日留存)
- [5.6 实战4:累计销售额](#5.6 实战4:累计销售额)
- [5.7 实战5:同比环比](#5.7 实战5:同比环比)
- [六、CTE 与递归 CTE](#六、CTE 与递归 CTE)
-
- [6.1 普通 CTE](#6.1 普通 CTE)
- [6.2 递归 CTE:查询分类树](#6.2 递归 CTE:查询分类树)
- [七、10 道电商实战 SQL](#七、10 道电商实战 SQL)
-
- [7.1 查询每个用户最近一笔订单](#7.1 查询每个用户最近一笔订单)
- [7.2 查询每个品类销售额 Top 3 商品](#7.2 查询每个品类销售额 Top 3 商品)
- [7.3 查询每日新增用户数](#7.3 查询每日新增用户数)
- [7.4 查询7日留存](#7.4 查询7日留存)
- [7.5 查询累计销售额](#7.5 查询累计销售额)
- [7.6 查询每个用户的订单总额排名](#7.6 查询每个用户的订单总额排名)
- [7.7 查询订单及其明细](#7.7 查询订单及其明细)
- [7.8 查询没有下单的用户](#7.8 查询没有下单的用户)
- [7.9 查询分类树](#7.9 查询分类树)
- [7.10 查询每月销售额与环比](#7.10 查询每月销售额与环比)
- [八、窗口函数 vs 自连接性能对比](#八、窗口函数 vs 自连接性能对比)
- 九、本篇实战任务
- 十、本篇小结
一、先搞懂 SQL 执行顺序
很多人写 SQL 靠感觉,其实 SQL 有严格的执行顺序。
1.1 书写顺序 vs 执行顺序
书写顺序(你写的) 执行顺序(MySQL 跑的)
───────────────────── ─────────────────────
SELECT FROM
FROM WHERE
WHERE GROUP BY
GROUP BY HAVING
HAVING SELECT
ORDER BY ORDER BY
LIMIT LIMIT
1.2 执行顺序图解
FROM ① 先确定从哪张表查
│
↓
WHERE ② 过滤行
│
↓
GROUP BY ③ 分组
│
↓
HAVING ④ 过滤分组
│
↓
SELECT ⑤ 选择列、计算表达式
│
↓
ORDER BY ⑥ 排序
│
↓
LIMIT ⑦ 限制返回行数
实战意义:
WHERE里不能用SELECT的别名,因为WHERE先执行HAVING里可以用聚合函数,因为它在GROUP BY之后ORDER BY里可以用SELECT的别名
sql
-- 错误:WHERE 里不能用别名
SELECT price * quantity AS total
FROM order_items
WHERE total > 100; -- 报错
-- 正确:用原表达式
SELECT price * quantity AS total
FROM order_items
WHERE price * quantity > 100;
-- 正确:ORDER BY 可以用别名
SELECT price * quantity AS total
FROM order_items
ORDER BY total DESC;
二、多表连接
2.1 连接类型
┌─────────┐ ┌─────────┐
│ users │ │ orders │
│ A B C │ │ 1 2 3 4 │
└─────────┘ └─────────┘
INNER JOIN:只返回两表都匹配的行
A-1, B-2, C-3
LEFT JOIN:返回左表全部,右表不匹配补 NULL
A-1, B-2, C-3, 右表独有的 4 不出现
RIGHT JOIN:返回右表全部,左表不匹配补 NULL
1-A, 2-B, 3-C, 4 补 NULL
FULL JOIN:MySQL 不支持,用 UNION 模拟
2.2 实战:查询订单及用户信息
sql
-- INNER JOIN:只查有用户的订单
SELECT o.id, o.total_amount, u.username
FROM orders o
INNER JOIN users u ON o.user_id = u.id
LIMIT 10;
-- LEFT JOIN:查所有订单,没有用户的显示 NULL
SELECT o.id, o.total_amount, u.username
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LIMIT 10;
2.3 自连接:查询分类树
sql
-- 查询每个分类及其父分类名
SELECT c.id, c.name AS category_name, p.name AS parent_name
FROM categories c
LEFT JOIN categories p ON c.parent_id = p.id;
结果:
| id | category_name | parent_name |
|---|---|---|
| 1 | 手机 | NULL |
| 2 | 电脑 | NULL |
| 3 | 苹果 | 手机 |
| 4 | 小米 | 手机 |
| 5 | 华为 | 手机 |
2.4 多表连接:订单 + 用户 + 订单项 + 商品
sql
SELECT
o.id AS order_id,
u.username,
oi.product_name,
oi.quantity,
oi.price,
oi.quantity * oi.price AS subtotal,
o.total_amount,
o.status
FROM orders o
INNER JOIN users u ON o.user_id = u.id
INNER JOIN order_items oi ON oi.order_id = o.id
WHERE o.id = 1;
三、子查询
3.1 子查询类型
标量子查询:返回单个值
SELECT * FROM users WHERE id = (SELECT MAX(user_id) FROM orders);
IN 子查询:返回一列多值
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders);
EXISTS 子查询:返回布尔值
SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
相关子查询:子查询引用外层表
SELECT * FROM users u WHERE (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) > 5;
3.2 实战:查询有订单的用户
sql
-- 方式1:IN
SELECT * FROM users
WHERE id IN (SELECT DISTINCT user_id FROM orders);
-- 方式2:EXISTS(通常更快)
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
-- 方式3:JOIN
SELECT DISTINCT u.*
FROM users u
INNER JOIN orders o ON o.user_id = u.id;
性能对比:
- 小数据量:三者差不多
- 大数据量:
EXISTS通常优于IN,JOIN取决于索引
3.3 实战:查询没有订单的用户
sql
-- LEFT JOIN + IS NULL
SELECT u.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;
-- NOT EXISTS
SELECT * FROM users u
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
四、聚合与分组
4.1 常用聚合函数
| 函数 | 作用 |
|---|---|
| COUNT | 计数 |
| SUM | 求和 |
| AVG | 平均 |
| MAX | 最大 |
| MIN | 最小 |
| GROUP_CONCAT | 拼接字符串 |
4.2 实战:统计每个用户的订单数和总额
sql
SELECT
u.id,
u.username,
COUNT(o.id) AS order_count,
IFNULL(SUM(o.total_amount), 0) AS total_spent
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.username
HAVING order_count > 0
ORDER BY total_spent DESC
LIMIT 10;
4.3 WHERE vs HAVING
sql
-- WHERE:分组前过滤行
SELECT user_id, COUNT(*) AS cnt
FROM orders
WHERE status = 1 -- 先过滤已支付订单
GROUP BY user_id;
-- HAVING:分组后过滤分组
SELECT user_id, COUNT(*) AS cnt
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 5; -- 再过滤订单数大于5的
执行顺序
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
↑ ↑
过滤行 过滤分组
五、窗口函数(重点)
5.1 什么是窗口函数?
窗口函数在不减少行数的前提下,对每一行计算聚合值。
普通聚合:
GROUP BY user_id → 每个用户只返回一行
窗口函数:
OVER (PARTITION BY user_id) → 每行都保留,附加聚合值
5.2 常用窗口函数
| 函数 | 作用 |
|---|---|
| ROW_NUMBER | 行号,不重复 |
| RANK | 排名,并列跳号 |
| DENSE_RANK | 排名,并列不跳号 |
| LAG | 上一行 |
| LEAD | 下一行 |
| SUM OVER | 累计求和 |
| AVG OVER | 移动平均 |
5.3 实战1:每个用户最近一笔订单
sql
SELECT *
FROM (
SELECT
o.*,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders o
) t
WHERE rn = 1;
5.4 实战2:每个品类销售额 Top 3 商品
sql
SELECT *
FROM (
SELECT
p.category_id,
p.name,
SUM(oi.quantity * oi.price) AS sales,
RANK() OVER (PARTITION BY p.category_id ORDER BY SUM(oi.quantity * oi.price) DESC) AS rk
FROM order_items oi
INNER JOIN products p ON oi.product_id = p.id
GROUP BY p.category_id, p.id, p.name
) t
WHERE rk <= 3;
5.5 实战3:每日新增用户与7日留存
sql
-- 每日新增用户
SELECT
DATE(created_at) AS dt,
COUNT(*) AS new_users
FROM users
GROUP BY DATE(created_at)
ORDER BY dt;
-- 7日留存(简化版)
SELECT
DATE(u.created_at) AS reg_date,
COUNT(DISTINCT u.id) AS new_users,
COUNT(DISTINCT CASE WHEN DATEDIFF(o.created_at, u.created_at) = 7 THEN u.id END) AS day7_retention
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY DATE(u.created_at);
5.6 实战4:累计销售额
sql
SELECT
DATE(created_at) AS dt,
SUM(total_amount) AS daily_sales,
SUM(SUM(total_amount)) OVER (ORDER BY DATE(created_at)) AS cumulative_sales
FROM orders
WHERE status = 1
GROUP BY DATE(created_at)
ORDER BY dt;
5.7 实战5:同比环比
sql
SELECT
DATE_FORMAT(created_at, '%Y-%m') AS month,
SUM(total_amount) AS sales,
LAG(SUM(total_amount)) OVER (ORDER BY DATE_FORMAT(created_at, '%Y-%m')) AS last_month,
ROUND(
(SUM(total_amount) - LAG(SUM(total_amount)) OVER (ORDER BY DATE_FORMAT(created_at, '%Y-%m')))
/ LAG(SUM(total_amount)) OVER (ORDER BY DATE_FORMAT(created_at, '%Y-%m')) * 100,
2
) AS growth_rate
FROM orders
WHERE status = 1
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
ORDER BY month;
六、CTE 与递归 CTE
6.1 普通 CTE
CTE 让复杂 SQL 更易读。
sql
WITH user_order_stats AS (
SELECT
user_id,
COUNT(*) AS order_count,
SUM(total_amount) AS total_spent
FROM orders
WHERE status = 1
GROUP BY user_id
)
SELECT
u.username,
s.order_count,
s.total_spent
FROM users u
INNER JOIN user_order_stats s ON u.id = s.user_id
WHERE s.total_spent > 10000
ORDER BY s.total_spent DESC;
6.2 递归 CTE:查询分类树
sql
WITH RECURSIVE category_tree AS (
-- 锚点:顶级分类
SELECT id, name, parent_id, 1 AS level, CAST(name AS CHAR(255)) AS path
FROM categories
WHERE parent_id = 0
UNION ALL
-- 递归:子分类
SELECT c.id, c.name, c.parent_id, ct.level + 1, CONCAT(ct.path, ' > ', c.name)
FROM categories c
INNER JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree ORDER BY path;
结果:
| id | name | parent_id | level | path |
|---|---|---|---|---|
| 1 | 手机 | 0 | 1 | 手机 |
| 3 | 苹果 | 1 | 2 | 手机 > 苹果 |
| 4 | 小米 | 1 | 2 | 手机 > 小米 |
| 5 | 华为 | 1 | 2 | 手机 > 华为 |
| 2 | 电脑 | 0 | 1 | 电脑 |
七、10 道电商实战 SQL
7.1 查询每个用户最近一笔订单
sql
SELECT *
FROM (
SELECT o.*, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders o
) t WHERE rn = 1;
7.2 查询每个品类销售额 Top 3 商品
sql
SELECT *
FROM (
SELECT p.category_id, p.name,
SUM(oi.quantity * oi.price) AS sales,
RANK() OVER (PARTITION BY p.category_id ORDER BY SUM(oi.quantity * oi.price) DESC) AS rk
FROM order_items oi
INNER JOIN products p ON oi.product_id = p.id
GROUP BY p.category_id, p.id, p.name
) t WHERE rk <= 3;
7.3 查询每日新增用户数
sql
SELECT DATE(created_at) AS dt, COUNT(*) AS new_users
FROM users
GROUP BY DATE(created_at)
ORDER BY dt;
7.4 查询7日留存
sql
SELECT
DATE(u.created_at) AS reg_date,
COUNT(DISTINCT u.id) AS new_users,
COUNT(DISTINCT CASE WHEN DATEDIFF(o.created_at, u.created_at) = 7 THEN u.id END) AS day7
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY DATE(u.created_at);
7.5 查询累计销售额
sql
SELECT
DATE(created_at) AS dt,
SUM(total_amount) AS daily,
SUM(SUM(total_amount)) OVER (ORDER BY DATE(created_at)) AS cumulative
FROM orders WHERE status = 1
GROUP BY DATE(created_at);
7.6 查询每个用户的订单总额排名
sql
SELECT
u.username,
SUM(o.total_amount) AS total,
RANK() OVER (ORDER BY SUM(o.total_amount) DESC) AS rk
FROM users u
INNER JOIN orders o ON o.user_id = u.id
WHERE o.status = 1
GROUP BY u.id, u.username;
7.7 查询订单及其明细
sql
SELECT o.id AS order_id, u.username, oi.product_name, oi.quantity, oi.price,
oi.quantity * oi.price AS subtotal
FROM orders o
INNER JOIN users u ON o.user_id = u.id
INNER JOIN order_items oi ON oi.order_id = o.id
WHERE o.id = 1;
7.8 查询没有下单的用户
sql
SELECT u.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;
7.9 查询分类树
sql
WITH RECURSIVE category_tree AS (
SELECT id, name, parent_id, 1 AS level, CAST(name AS CHAR(255)) AS path
FROM categories WHERE parent_id = 0
UNION ALL
SELECT c.id, c.name, c.parent_id, ct.level + 1, CONCAT(ct.path, ' > ', c.name)
FROM categories c
INNER JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree ORDER BY path;
7.10 查询每月销售额与环比
sql
SELECT
DATE_FORMAT(created_at, '%Y-%m') AS month,
SUM(total_amount) AS sales,
LAG(SUM(total_amount)) OVER (ORDER BY DATE_FORMAT(created_at, '%Y-%m')) AS last_month
FROM orders WHERE status = 1
GROUP BY DATE_FORMAT(created_at, '%Y-%m');
八、窗口函数 vs 自连接性能对比
sql
-- 方式1:窗口函数(推荐)
SELECT * FROM (
SELECT o.*, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders o
) t WHERE rn = 1;
-- 方式2:自连接
SELECT o.*
FROM orders o
INNER JOIN (
SELECT user_id, MAX(created_at) AS max_time
FROM orders GROUP BY user_id
) t ON o.user_id = t.user_id AND o.created_at = t.max_time;
| 方式 | 可读性 | 性能 | 推荐场景 |
|---|---|---|---|
| 窗口函数 | 高 | 好 | MySQL 8.0+ |
| 自连接 | 低 | 一般 | 老版本 MySQL |
| 子查询 | 中 | 差 | 小数据量 |
九、本篇实战任务
任务清单
- 写出 SQL 执行顺序
- 用 INNER JOIN / LEFT JOIN 查询订单和用户
- 用自连接查询分类树
- 用子查询查询有/无订单的用户
- 用 GROUP BY 统计每个用户订单数
- 用窗口函数实现 Top N
- 用窗口函数实现累计销售额
- 用递归 CTE 查询分类树
- 完成 10 道电商实战 SQL
自检问题
- SQL 执行顺序是什么?WHERE 和 HAVING 有什么区别?
- INNER JOIN 和 LEFT JOIN 的区别?
- EXISTS 和 IN 哪个更快?为什么?
- 窗口函数和 GROUP BY 的区别?
- ROW_NUMBER、RANK、DENSE_RANK 的区别?
- 递归 CTE 怎么用?
十、本篇小结
第3篇 核心收获
│
├── SQL 执行顺序
│ └── FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
│
├── 多表连接
│ ├── INNER JOIN
│ ├── LEFT JOIN
│ └── 自连接
│
├── 子查询
│ ├── IN / EXISTS
│ └── 相关子查询
│
├── 聚合与分组
│ ├── GROUP BY / HAVING
│ └── COUNT / SUM / AVG / MAX / MIN
│
├── 窗口函数
│ ├── ROW_NUMBER / RANK / DENSE_RANK
│ ├── LAG / LEAD
│ └── SUM OVER / AVG OVER
│
└── CTE
├── 普通 CTE
└── 递归 CTE
下一篇:第4篇《索引原理与执行计划实战》