【数据库】MySQL 实战精通系列 · 第3篇:SQL 核心与复杂查询实战

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

自检问题

  1. SQL 执行顺序是什么?WHERE 和 HAVING 有什么区别?
  2. INNER JOIN 和 LEFT JOIN 的区别?
  3. EXISTS 和 IN 哪个更快?为什么?
  4. 窗口函数和 GROUP BY 的区别?
  5. ROW_NUMBER、RANK、DENSE_RANK 的区别?
  6. 递归 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篇《索引原理与执行计划实战》

相关推荐
一只旭宝1 小时前
【C++复习】四种类型转换 + 常见设计模式 + Redis/MySQL(后端面试复习完整版)
c++·redis·笔记·mysql·设计模式
Dragon_qu·x1 小时前
MongoDB 高可用集群部署
运维·数据库·mongodb·k8s·helm
爱签AI电子合同1 小时前
电子合同大批量怎么测?并发与批量处理维度专项测评
服务器·数据库·人工智能·企业微信·电子签名
JosieBook1 小时前
【数据库】MySQL 实战精通系列 · 第4篇:索引原理与执行计划实战
android·数据库·mysql
m0_547486662 小时前
《数据库原理、技术与应用:MySQL》全套PPT课件2026
数据库·mysql
JosieBook2 小时前
【数据库】MySQL 实战精通系列 · 第1篇:MySQL 全局观与实战环境搭建
数据库·mysql·adb
霸道流氓气质2 小时前
LangGraph式图编排引擎Java实现:状态机模型、Checkpoint恢复与生产级落地实战
java·开发语言·数据库
数据狐(Datafox)2 小时前
淘宝商品详情API实战:多语言代购商城自动同步数据完整方案
开发语言·前端·数据库·爬虫·json
Leo.yuan2 小时前
帆软 Data Agent 技术实践白皮书:NL2BI 的可执行与可追溯分析链路
数据库·microsoft