【MySQL全面教学】MySQL子查询与高级查询Day7(2026年)

写在前面

大家好,欢迎来到MySQL全面教学系列的第7天!昨天我们学习了多表查询与JOIN,掌握了表与表之间关联查询的技能。今天,我们将进入更高级的查询领域------子查询与高级查询

子查询是嵌套在其他查询中的查询,它可以解决很多复杂的业务问题。而窗口函数和CTE(公用表表达式)则是MySQL 8.0带来的强大特性,让复杂的数据分析变得简单优雅。

这是单表查询系列的最后一篇,内容较多但非常实用,让我们开始吧!


目录

    • 写在前面
    • 一、子查询分类
    • 二、WHERE中的子查询
      • [2.1 IN子查询](#2.1 IN子查询)
      • [2.2 EXISTS子查询](#2.2 EXISTS子查询)
      • [2.3 比较运算符子查询](#2.3 比较运算符子查询)
      • [2.4 ALL和ANY/SOME](#2.4 ALL和ANY/SOME)
    • 三、FROM中的子查询(派生表)
      • [3.1 派生表优化](#3.1 派生表优化)
    • 四、SELECT中的子查询(关联子查询)
    • [五、UNION和UNION ALL](#五、UNION和UNION ALL)
      • [5.1 UNION vs UNION ALL](#5.1 UNION vs UNION ALL)
      • [5.2 UNION注意事项](#5.2 UNION注意事项)
    • [六、窗口函数(MySQL 8.0+)](#六、窗口函数(MySQL 8.0+))
      • [6.1 窗口函数语法](#6.1 窗口函数语法)
      • [6.2 排名函数](#6.2 排名函数)
      • [6.3 分组排名](#6.3 分组排名)
      • [6.4 偏移函数(LEAD/LAG)](#6.4 偏移函数(LEAD/LAG))
      • [6.5 聚合窗口函数](#6.5 聚合窗口函数)
    • 七、CTE公用表表达式(WITH子句)
      • [7.1 基本CTE](#7.1 基本CTE)
      • [7.2 多个CTE](#7.2 多个CTE)
      • [7.3 递归CTE](#7.3 递归CTE)
    • 八、实战:复杂业务查询场景
      • [8.1 连续登录天数统计](#8.1 连续登录天数统计)
      • [8.2 分组TOP N](#8.2 分组TOP N)
      • [8.3 同期群分析(Cohort Analysis)](#8.3 同期群分析(Cohort Analysis))
    • 九、踩坑提醒与经验之谈
      • [9.1 子查询性能问题](#9.1 子查询性能问题)
      • [9.2 IN子查询的NULL陷阱](#9.2 IN子查询的NULL陷阱)
      • [9.3 派生表没有索引](#9.3 派生表没有索引)
      • [9.4 窗口函数与GROUP BY混用](#9.4 窗口函数与GROUP BY混用)
    • 十、面试高频考点
      • [10.1 IN和EXISTS的区别?](#10.1 IN和EXISTS的区别?)
      • [10.2 窗口函数和GROUP BY的区别?](#10.2 窗口函数和GROUP BY的区别?)
      • [10.3 如何优化子查询?](#10.3 如何优化子查询?)
      • [10.4 CTE和派生表的区别?](#10.4 CTE和派生表的区别?)
      • [10.5 如何实现分组TOP N?](#10.5 如何实现分组TOP N?)
    • 十一、总结
    • 参考资料
    • 互动话题

一、子查询分类

子查询(Subquery)是嵌套在另一个SQL语句中的SELECT语句。根据返回结果的不同,子查询可以分为以下几类:

子查询类型 返回值 使用场景 示例
标量子查询 单行单列 比较运算 SELECT * FROM t WHERE col = (SELECT ...)
行子查询 单行多列 行比较 SELECT * FROM t WHERE (a,b) = (SELECT ...)
表子查询 多行多列 FROM子句 SELECT * FROM (SELECT ...) AS t

二、WHERE中的子查询

2.1 IN子查询

IN用于判断某值是否在子查询返回的结果集中。

sql 复制代码
-- 查询有订单的用户信息
SELECT * FROM users
WHERE user_id IN (SELECT DISTINCT user_id FROM orders);

-- 查询购买了特定类别商品的用户
SELECT * FROM users
WHERE user_id IN (
    SELECT DISTINCT o.user_id
    FROM orders o
    JOIN order_items oi ON o.order_id = oi.order_id
    JOIN products p ON oi.product_id = p.product_id
    WHERE p.category_id = 1
);

2.2 EXISTS子查询

EXISTS用于判断子查询是否返回结果,返回布尔值。

sql 复制代码
-- 查询有订单的用户(与IN效果相同)
SELECT * FROM users u
WHERE EXISTS (
    SELECT 1 FROM orders o WHERE o.user_id = u.user_id
);

-- 查询没有订单的用户
SELECT * FROM users u
WHERE NOT EXISTS (
    SELECT 1 FROM orders o WHERE o.user_id = u.user_id
);

IN vs EXISTS对比:

特性 IN EXISTS
执行方式 先执行子查询 相关子查询,逐行判断
性能 子查询结果小时快 子查询结果大时快
NULL处理 需注意NULL值 不受NULL影响
使用场景 固定值列表 关联条件复杂时

2.3 比较运算符子查询

sql 复制代码
-- 查询订单金额大于平均订单金额的记录
SELECT * FROM orders
WHERE total_amount > (SELECT AVG(total_amount) FROM orders);

-- 查询每个用户超过其平均订单金额的订单
SELECT o1.*
FROM orders o1
WHERE o1.total_amount > (
    SELECT AVG(o2.total_amount)
    FROM orders o2
    WHERE o2.user_id = o1.user_id
);

2.4 ALL和ANY/SOME

sql 复制代码
-- 查询订单金额大于所有用户平均消费的用户(ALL)
SELECT * FROM users u
WHERE total_spent > ALL (
    SELECT AVG(total_amount) FROM orders GROUP BY user_id
);

-- 查询订单金额大于任意一个用户平均消费的用户(ANY)
SELECT * FROM users u
WHERE total_spent > ANY (
    SELECT AVG(total_amount) FROM orders GROUP BY user_id
);

三、FROM中的子查询(派生表)

子查询在FROM子句中作为临时表使用,必须指定别名。

sql 复制代码
-- 查询每个用户的订单统计
SELECT 
    u.username,
    t.order_count,
    t.total_amount,
    t.avg_amount
FROM users u
JOIN (
    SELECT 
        user_id,
        COUNT(*) AS order_count,
        SUM(total_amount) AS total_amount,
        AVG(total_amount) AS avg_amount
    FROM orders
    GROUP BY user_id
) t ON u.user_id = t.user_id;

3.1 派生表优化

MySQL 5.7+会对派生表进行合并优化:

sql 复制代码
-- MySQL会自动将派生表合并到外层查询
SELECT * FROM (
    SELECT * FROM orders WHERE status = 'completed'
) AS o
WHERE o.amount > 100;

-- 等同于
SELECT * FROM orders WHERE status = 'completed' AND amount > 100;

四、SELECT中的子查询(关联子查询)

SELECT中的子查询会为每一行执行一次,称为关联子查询。

sql 复制代码
-- 查询用户及其订单数量
SELECT 
    u.user_id,
    u.username,
    (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.user_id) AS order_count
FROM users u;

-- 查询每个订单的用户消费排名
SELECT 
    o.order_id,
    o.user_id,
    o.total_amount,
    (SELECT COUNT(*) + 1 
     FROM orders o2 
     WHERE o2.user_id = o.user_id AND o2.total_amount > o.total_amount
    ) AS rank_in_user
FROM orders o;

踩坑提醒: SELECT中的关联子查询性能较差,每行都要执行一次子查询。大数据量时建议使用JOIN替代。


五、UNION和UNION ALL

UNION用于合并多个SELECT语句的结果集。

5.1 UNION vs UNION ALL

特性 UNION UNION ALL
去重 自动去重 不去重
性能 较慢(需要比较) 较快
使用场景 需要唯一结果 允许重复结果
sql 复制代码
-- UNION去重
SELECT city FROM users WHERE age > 30
UNION
SELECT city FROM users WHERE total_spent > 1000;

-- UNION ALL保留所有记录
SELECT 'VIP' AS user_type, user_id, username FROM users WHERE total_spent > 10000
UNION ALL
SELECT '普通', user_id, username FROM users WHERE total_spent <= 10000;

5.2 UNION注意事项

sql 复制代码
-- 列数必须相同
-- 列名以第一个SELECT为准
-- 对应列的数据类型要兼容

SELECT user_id, username FROM users
UNION
SELECT order_id, CAST(total_amount AS CHAR) FROM orders;  -- 类型转换

六、窗口函数(MySQL 8.0+)

窗口函数是MySQL 8.0引入的强大特性,用于在结果集的"窗口"上进行计算。

6.1 窗口函数语法

sql 复制代码
function_name(expression) OVER (
    [PARTITION BY partition_expression]
    [ORDER BY order_expression]
    [frame_clause]
)

6.2 排名函数

函数 说明 相同值处理
ROW_NUMBER() 连续排名 相同值不同排名
RANK() 跳跃排名 相同值同排名,跳过后续
DENSE_RANK() 密集排名 相同值同排名,不跳过
sql 复制代码
-- 查询用户消费排名
SELECT 
    user_id,
    username,
    total_spent,
    ROW_NUMBER() OVER (ORDER BY total_spent DESC) AS row_num,
    RANK() OVER (ORDER BY total_spent DESC) AS rank_num,
    DENSE_RANK() OVER (ORDER BY total_spent DESC) AS dense_rank_num
FROM users;

结果示例:

user_id username total_spent row_num rank_num dense_rank_num
1 张三 10000 1 1 1
2 李四 8000 2 2 2
3 王五 8000 3 2 2
4 赵六 5000 4 4 3

6.3 分组排名

sql 复制代码
-- 按城市分组,查询每个城市的用户消费排名
SELECT 
    city,
    username,
    total_spent,
    RANK() OVER (PARTITION BY city ORDER BY total_spent DESC) AS city_rank
FROM users;

6.4 偏移函数(LEAD/LAG)

sql 复制代码
-- 查询每月销售额及环比变化
SELECT 
    month,
    sales,
    LAG(sales) OVER (ORDER BY month) AS prev_month_sales,
    sales - LAG(sales) OVER (ORDER BY month) AS month_over_month,
    LEAD(sales) OVER (ORDER BY month) AS next_month_sales
FROM monthly_sales;

6.5 聚合窗口函数

sql 复制代码
-- 累计求和
SELECT 
    order_date,
    daily_sales,
    SUM(daily_sales) OVER (ORDER BY order_date) AS cumulative_sales
FROM daily_summary;

-- 移动平均
SELECT 
    date,
    value,
    AVG(value) OVER (
        ORDER BY date 
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS 7_day_avg
FROM daily_data;

七、CTE公用表表达式(WITH子句)

CTE(Common Table Expression)使用WITH子句定义临时结果集,可以多次引用。

7.1 基本CTE

sql 复制代码
-- 定义CTE
WITH user_stats AS (
    SELECT 
        user_id,
        COUNT(*) AS order_count,
        SUM(total_amount) AS total_spent
    FROM orders
    GROUP BY user_id
)
-- 使用CTE
SELECT 
    u.username,
    us.order_count,
    us.total_spent
FROM users u
JOIN user_stats us ON u.user_id = us.user_id;

7.2 多个CTE

sql 复制代码
WITH 
order_stats AS (
    SELECT user_id, COUNT(*) AS order_count FROM orders GROUP BY user_id
),
payment_stats AS (
    SELECT user_id, SUM(amount) AS total_paid FROM payments GROUP BY user_id
)
SELECT 
    u.username,
    COALESCE(os.order_count, 0) AS order_count,
    COALESCE(ps.total_paid, 0) AS total_paid
FROM users u
LEFT JOIN order_stats os ON u.user_id = os.user_id
LEFT JOIN payment_stats ps ON u.user_id = ps.user_id;

7.3 递归CTE

递归CTE用于处理层级数据:

sql 复制代码
-- 查询分类的所有子分类
WITH RECURSIVE category_tree AS (
    -- 锚成员:起始点
    SELECT category_id, category_name, parent_id, 0 AS level
    FROM categories
    WHERE category_id = 1  -- 从根分类开始
    
    UNION ALL
    
    -- 递归成员
    SELECT c.category_id, c.category_name, c.parent_id, ct.level + 1
    FROM categories c
    JOIN category_tree ct ON c.parent_id = ct.category_id
)
SELECT * FROM category_tree;

八、实战:复杂业务查询场景

8.1 连续登录天数统计

sql 复制代码
-- 统计用户最大连续登录天数
WITH login_with_grp AS (
    SELECT 
        user_id,
        login_date,
        DATE_SUB(login_date, INTERVAL 
            ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY
        ) AS grp
    FROM user_logins
)
SELECT 
    user_id,
    COUNT(*) AS consecutive_days
FROM login_with_grp
GROUP BY user_id, grp
ORDER BY consecutive_days DESC;

8.2 分组TOP N

sql 复制代码
-- 查询每个类别销量TOP3的商品
WITH ranked_products AS (
    SELECT 
        category_id,
        product_id,
        product_name,
        sales_volume,
        RANK() OVER (PARTITION BY category_id ORDER BY sales_volume DESC) AS rnk
    FROM products
)
SELECT * FROM ranked_products WHERE rnk <= 3;

8.3 同期群分析(Cohort Analysis)

sql 复制代码
-- 用户留存分析
WITH user_cohorts AS (
    SELECT 
        user_id,
        DATE_FORMAT(MIN(order_date), '%Y-%m') AS cohort_month
    FROM orders
    GROUP BY user_id
),
user_activity AS (
    SELECT DISTINCT
        u.user_id,
        uc.cohort_month,
        DATE_FORMAT(o.order_date, '%Y-%m') AS activity_month,
        PERIOD_DIFF(
            DATE_FORMAT(o.order_date, '%Y%m'),
            DATE_FORMAT(uc.cohort_month, '%Y%m')
        ) AS period_num
    FROM orders o
    JOIN user_cohorts uc ON o.user_id = uc.user_id
)
SELECT 
    cohort_month,
    period_num,
    COUNT(DISTINCT user_id) AS user_count,
    ROUND(COUNT(DISTINCT user_id) * 100.0 / 
        FIRST_VALUE(COUNT(DISTINCT user_id)) OVER (
            PARTITION BY cohort_month ORDER BY period_num
        ), 2
    ) AS retention_rate
FROM user_activity
GROUP BY cohort_month, period_num
ORDER BY cohort_month, period_num;

九、踩坑提醒与经验之谈

9.1 子查询性能问题

问题: 关联子查询性能差

sql 复制代码
-- 低效写法:每行执行一次子查询
SELECT 
    u.*,
    (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.user_id) AS cnt
FROM users u;

-- 高效写法:使用JOIN
SELECT u.*, COALESCE(o.cnt, 0) AS cnt
FROM users u
LEFT JOIN (
    SELECT user_id, COUNT(*) AS cnt FROM orders GROUP BY user_id
) o ON u.user_id = o.user_id;

经验之谈: 能用JOIN就不用子查询,特别是SELECT中的关联子查询。

9.2 IN子查询的NULL陷阱

sql 复制代码
-- 危险!子查询返回NULL会导致结果为空
SELECT * FROM users
WHERE user_id IN (SELECT manager_id FROM employees);  -- 如果manager_id有NULL

-- 安全写法:排除NULL
SELECT * FROM users
WHERE user_id IN (SELECT manager_id FROM employees WHERE manager_id IS NOT NULL);

-- 或者使用EXISTS
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM employees e WHERE e.manager_id = u.user_id);

9.3 派生表没有索引

sql 复制代码
-- 派生表不能直接使用索引
-- 如果需要优化,考虑创建临时表并加索引
CREATE TEMPORARY TABLE tmp_user_stats AS
SELECT user_id, COUNT(*) AS cnt FROM orders GROUP BY user_id;

ALTER TABLE tmp_user_stats ADD INDEX idx_user_id(user_id);

-- 然后使用临时表进行JOIN

9.4 窗口函数与GROUP BY混用

sql 复制代码
-- 错误:窗口函数在GROUP BY之后执行
SELECT 
    category_id,
    SUM(sales),
    RANK() OVER (ORDER BY SUM(sales))  -- 错误!
FROM products
GROUP BY category_id;

-- 正确:使用派生表或CTE
SELECT 
    category_id,
    total_sales,
    RANK() OVER (ORDER BY total_sales)
FROM (
    SELECT category_id, SUM(sales) AS total_sales
    FROM products
    GROUP BY category_id
) t;

十、面试高频考点

10.1 IN和EXISTS的区别?

答案:

  • IN:先执行子查询,将结果缓存,然后外层查询匹配。适合子查询结果小的情况。
  • EXISTS:相关子查询,对外层每行执行子查询,找到匹配即返回。适合子查询结果大的情况。
  • IN遇到NULL会有问题,EXISTS不会。

10.2 窗口函数和GROUP BY的区别?

答案:

  • GROUP BY:将多行聚合成一行,行数减少
  • 窗口函数:保持原有行数,为每行添加计算列
  • GROUP BY后使用聚合函数,窗口函数使用OVER子句

10.3 如何优化子查询?

答案:

  1. 将SELECT中的关联子查询改为JOIN
  2. 将IN子查询改为EXISTS(子查询大时)
  3. 确保子查询的关联字段有索引
  4. 使用派生表时,考虑物化为临时表

10.4 CTE和派生表的区别?

答案:

  • CTE使用WITH定义,可读性更好,可以递归
  • 派生表直接在FROM中定义
  • CTE可以多次引用,派生表需要重复定义
  • MySQL 8.0+推荐优先使用CTE

10.5 如何实现分组TOP N?

答案: 使用窗口函数

sql 复制代码
WITH ranked AS (
    SELECT *, ROW_NUMBER() OVER (PARTITION BY group_id ORDER BY score DESC) AS rn
    FROM scores
)
SELECT * FROM ranked WHERE rn <= N;

十一、总结

今天我们学习了MySQL子查询与高级查询的核心知识:

  1. 子查询分类:标量子查询、行子查询、表子查询
  2. WHERE中的子查询:IN、EXISTS、比较运算符
  3. FROM中的子查询:派生表的使用和优化
  4. SELECT中的子查询:关联子查询的性能问题
  5. UNION/UNION ALL:合并结果集
  6. 窗口函数:ROW_NUMBER、RANK、LEAD/LAG等
  7. CTE:公用表表达式,提高可读性

下一步预告

Day8:MySQL索引原理与优化

明天我们将进入MySQL性能优化的核心领域------索引。你将学习到B+树索引原理、索引类型、索引设计原则,以及如何通过EXPLAIN分析查询性能。索引是数据库优化的基石,掌握它,你就能让查询速度提升百倍!敬请期待!


参考资料

MySQL 8.0 Reference Manual - Subqueries


互动话题

  1. 你在使用子查询时遇到过哪些性能问题?是如何解决的?
  2. 你的MySQL版本是多少?是否已经用上了窗口函数和CTE?
  3. 在实际项目中,你觉得子查询和JOIN哪个更好用?

如果觉得本文对你有帮助,请点赞收藏!明天见!

相关推荐
better_liang1 小时前
每日Java面试场景题知识点之-MySQL底层数据结构B+树
java·数据结构·mysql·性能优化·面试题·b+树·数据库索引
sN2vuQ08W1 小时前
Mysql事物的持久性及原子性
数据库·mysql
极客小云1 小时前
【从 while 循环到可视化智能体:深入拆解 Agent Loop、Codex 风格工具调用、OpenClaw 与 Hermes 背后的技术细节】
数据库·python·大模型·agent·codex·openclaw·hermes
这个DBA有点耶1 小时前
分布式数据库的“分片键”设计:选错可能让性能倒退10倍
数据库·分布式
CodePlayer竟然被占用了1 小时前
Claude Code 出安全插件了:AI 写代码的安全网,终于有人正经做了
人工智能·后端
运维行者_2 小时前
使用Applications Manager监控的关键MongoDB指标
服务器·开发语言·网络·数据库·mongodb·机器学习·云计算
一支黑色の铅笔2 小时前
MongoDB Aggregation Pipeline 常用 Stage 速查
数据库·算法·mongodb
明月_清风2 小时前
Base64:用 33% 的体积膨胀,买一张在文本世界通行的门票
后端
得物技术2 小时前
HorizonVault 技术深潜:如何在 HDD 上做出 100GB/s+ 级大吞吐分布式存储|得物技术
大数据·后端·kafka