【MySQL】SELECT 语句执行流程

在 Java 全栈开发中,SQL 优化是性能调优的核心环节,而理解 SELECT 语句的底层执行顺序 ,是写出高效 SQL、排查慢查询、设计联查逻辑的基础。绝大多数开发者仅记住了 SQL 的书写顺序 ,却忽略了数据库实际执行顺序,这也是导致 SQL 性能瓶颈、联查逻辑错误的核心原因。

本文将从数据库底层虚拟表(Virtual Table,VT)流转的角度,逐阶段拆解 SELECT 执行逻辑,覆盖单表 / 多表联查、内外连接、分组排序、分页全场景,结合真实开发案例,让你彻底吃透 SQL 执行原理。

🌟**【青柠代码录】--- 青柠来相伴,代码更简单** 🌟

🔥**【全栈】博客合集:** https://www.yuque.com/u12587869/zplytb/ur5ohwqxd2axtiny 🔥

🎯**【Java】面试题:** https://www.yuque.com/u12587869/zplytb/eh7yqzitiab693og 🎯


一、先明确核心:SQL 书写顺序 ≠ 执行顺序

这是所有开发者必须牢记的基础规则,也是本文的核心前提:

1. SQL 标准书写顺序(语法顺序)

复制代码
SELECT 
  DISTINCT 字段列表
FROM 
  表1 
  JOIN 表2 ON 关联条件
WHERE 
  行过滤条件
GROUP BY 
  分组字段
HAVING 
  分组过滤条件
ORDER BY 
  排序字段 ASC/DESC
LIMIT 
  分页偏移量, 条数;

2. MySQL 底层实际执行顺序(核心重点)

复制代码
FROM` → `JOIN/ON` → `WHERE` → `GROUP BY` → `HAVING` → `SELECT` → `DISTINCT` → `ORDER BY` → `LIMIT

二、逐阶段深度解析:虚拟表流转 + 实战逻辑

数据库执行 SQL 时,不会直接操作物理表 ,而是通过虚拟表(VT) 逐阶段处理数据,每一步都会生成一个临时虚拟表,作为下一步的输入源。

我们结合常用的订单 + 用户联表示例,完整拆解执行流程。

基础测试表(表结构)

复制代码
-- 用户表
CREATE TABLE user (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(50) NOT NULL,
  age TINYINT,
  create_time DATETIME
);
-- 订单表
CREATE TABLE `order` (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL,
  order_no VARCHAR(32) NOT NULL,
  order_amount DECIMAL(10,2),
  order_status TINYINT COMMENT '1-待支付 2-已完成 3-已取消'
);

阶段 1:FROM + 多表联查(笛卡尔积 → ON 筛选 → 添加外部行)

执行优先级:最高

核心作用 :加载数据源,生成原始数据虚拟表 VT1多表联查细分步骤

步骤 1.1 执行 CROSS JOIN 生成笛卡尔积(VT1-1)

当执行 FROM 表1 JOIN 表2 时,数据库首先不考虑任何关联条件 ,直接计算两张表的笛卡尔积(表 1 的每一行与表 2 的每一行组合)。

  • 风险:笛卡尔集会产生海量无效数据,开发中绝对禁止无关联条件的联查

  • 示例:user 表 100 行 + order 表 1000 行 → 笛卡尔积 = 100*1000=10 万行数据

步骤 1.2 执行 ON 条件筛选(VT1-2)

在笛卡尔积的基础上,执行ON关联条件,过滤掉不匹配的数据,得到有效联查数据。

复制代码
-- 执行 ON user.id = order.user_id,过滤无效组合
ON user.id = order.user_id

关键知识点ON联查过滤,仅作用于多表组合阶段,优先于 WHERE 执行。

步骤 1.3 添加外部行(VT1-3)

仅当使用 LEFT JOIN / RIGHT JOIN / FULL JOIN 时执行,INNER JOIN 无此步骤。

  • LEFT JOIN:保留左表所有数据,右表无匹配则补 NULL

  • RIGHT JOIN:保留右表所有数据,左表无匹配则补 NULL

  • FULL JOIN:保留左右表所有数据

  • 实战:LEFT JOIN 是最常用的外连接,用于查询「主表全量数据 + 关联表匹配数据」

多表迭代规则

若联查 3 张及以上表,数据库会重复上述 3 个步骤 ,直到所有表处理完成,最终生成原始数据虚拟表 VT1


阶段 2:WHERE 条件过滤

执行优先级:第二

输入源:VT1

输出源:VT2

核心作用 :对原始数据进行行级过滤,剔除不满足条件的数据

关键特性

  1. WHERE 执行在分组之前:无法对分组后的聚合结果过滤

  2. WHERE 过滤效率极高:尽早过滤无效数据,减少后续计算量(SQL 优化核心:WHERE 条件尽量精准)

  3. 不能使用聚合函数:如 SUM/COUNT/AVG 等

  4. 过滤掉 NULL 值:VT2 中不包含不符合条件的行

实战示例

复制代码
-- 过滤:订单状态为已完成、用户年龄大于18岁
WHERE order.order_status = 2 AND user.age > 18

阶段 3:GROUP BY 分组

执行优先级:第三

输入源:VT2

输出源:VT3

核心作用 :将 VT2 中的数据按照指定字段分组聚合,一组数据合并为一行

复制代码
// 分组前:原始多行数据(明细)
用户101 订单1 —— 100
用户101 订单2 —— 200
用户101 订单3 —— 150
​
用户102 订单1 —— 300
用户102 订单2 —— 50
​
// 第一步:分组连线(相同用户连在一起)
用户101 订单1 ——┓
用户101 订单2 ——┣  同一组
用户101 订单3 ——┛
          ↓ 合并
用户101  —————— 总金额450  (合并为1行)
​
用户102 订单1 ——┓
用户102 订单2 ——┛
          ↓ 合并
用户102  —————— 总金额350  (合并为1行)
​
// 最终结果:一组数据 → 合并成一行
用户101  450
用户102  350

实战场景

  • 按用户分组统计订单总金额

  • 按日期分组统计每日订单量

  • 按商品分类分组统计销量

关键知识点

  1. 分组后,SELECT 只能查询分组字段 + 聚合函数

  2. 分组会自动排序(无 ORDER BY 时,默认按分组字段升序)

实战示例

复制代码
-- 按用户ID分组,统计每个用户的订单总金额
GROUP BY user.id

阶段 4:HAVING 分组过滤

执行优先级:第四

输入源:VT3

输出源:VT4

核心作用 :对分组后的聚合结果进行过滤

WHERE 与 HAVING 核心区别

对比项 WHERE HAVING
执行阶段 分组之前 分组之后
过滤对象 原始行数据 分组聚合结果
聚合函数 不允许使用 允许使用
作用表 物理表 / 虚拟表 VT1 虚拟表 VT3
优化价值 减少数据量,提升性能 仅过滤分组结果

实战示例

复制代码
-- 过滤:订单总金额大于1000元的用户分组
HAVING SUM(order.order_amount) > 1000

阶段 5:SELECT 字段提取

执行优先级:第五

输入源:VT4

输出源:VT5-1

核心作用 :从虚拟表中提取最终需要展示的字段,剔除无关字段

关键知识点

  1. SELECT 执行在分组 / 过滤之后:这是为什么不能在 WHERE 中使用 SELECT 别名的原因

  2. 支持普通字段、聚合函数、表达式、别名

  3. 规范:禁止使用 SELECT *,只查询业务需要的字段(减少 IO,提升性能)

实战示例

复制代码
-- 提取分组字段、聚合结果、用户名称
SELECT 
  user.id AS userId,
  user.username,
  SUM(order.order_amount) AS totalAmount

阶段 6:DISTINCT 去重

执行优先级:第六

输入源:VT5-1

输出源:VT5-2

核心作用 :剔除结果集中完全重复的行

实战场景

  • 统计不重复的用户下单数

  • 查询不重复的商品分类

  • 去重统计枚举类型数据

优化提醒

DISTINCT 会执行全表扫描 / 排序,大数据量下慎用,优先通过 WHERE 条件、联查逻辑避免重复数据。


阶段 7:ORDER BY 排序

执行优先级:第七

输入源:VT5-2

输出源:VT6

核心作用 :对最终结果集按照指定字段升序 / 降序排序

关键特性

  1. ORDER BY 是最后一步数据处理操作(除 LIMIT 外)

  2. 支持:单个字段、多个字段、聚合结果排序

  3. 性能风险:无索引时,大数据量排序会产生filesort(慢查询核心诱因)

实战示例

复制代码
-- 按订单总金额降序排序
ORDER BY totalAmount DESC

阶段 8:LIMIT 分页

执行优先级:最低

输入源:VT6

输出源:VT7(最终结果集)

核心作用:截取指定范围的行数据,实现分页查询

实战规范

  1. 分页必须配合ORDER BY使用,保证分页数据有序且不重复

  2. 大数据量分页(如 LIMIT 100000,10)。优化:使用主键 ID 分页替代偏移量分页

实战示例

复制代码
-- 分页:取前10条数据
LIMIT 0, 10

三、完整 SQL + 执行顺序验证

我们将所有阶段整合,写出标准业务 SQL,对照执行顺序:

复制代码
-- 书写顺序
SELECT 
  user.id AS userId,
  user.username,
  SUM(o.order_amount) AS totalAmount
FROM 
  user u
  LEFT JOIN `order` o ON u.id = o.user_id
WHERE 
  o.order_status = 2 
  AND u.age > 18
GROUP BY 
  u.id, u.username
HAVING 
  totalAmount > 1000
ORDER BY 
  totalAmount DESC
LIMIT 0, 10;

实际执行顺序

  1. FROM user u LEFT JOIN order o ON u.id = o.user_id

  2. WHERE o.order_status = 2 AND u.age > 18

  3. GROUP BY u.id, u.username

  4. HAVING totalAmount > 1000

  5. SELECT u.id, u.username, SUM(o.order_amount)

  6. ORDER BY totalAmount DESC

  7. LIMIT 0,10


四、核心总结

  1. 执行顺序不可逆:FROM 最先,LIMIT 最后,SELECT 在中间阶段执行

  2. 虚拟表是核心:所有数据处理都基于临时虚拟表,不直接操作物理表

  3. 过滤越早性能越高:WHERE 优先过滤数据,减少后续计算量

  4. 内外连接核心差异:LEFT/RIGHT/FULL JOIN 会执行「添加外部行」步骤

  5. WHERE 与 HAVING 分工明确:WHERE 过滤行,HAVING 过滤分组

  6. 别名使用限制:SELECT 定义的别名,只能在 ORDER BY 中使用,WHERE/GROUP BY/HAVING 均无法使用

相关推荐
李慕婉学姐2 小时前
Springboot养老服务管理系统c0t92vu6(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
霖霖总总2 小时前
[Redis小技巧10]深入 Redis Stream:从原理到生产级实践
数据库·redis
扑克中的黑桃A2 小时前
基于代价模型的连接条件下推:复杂SQL查询的性能优化实践
数据库
数据知道2 小时前
MongoDB分片集群监控:详解Balancer状态与Chunk分布分析
数据库·mongodb
⑩-2 小时前
Redis内存淘汰策略?如何处理大Key?
java·数据库·redis
Y001112363 小时前
Day3-MySQL-SQL-2
数据库·sql·mysql
V1ncent Chen3 小时前
从零学SQL 07 数据过滤
数据库·sql·mysql·数据分析
A10169330713 小时前
maven导入spring框架
数据库·spring·maven
代码探秘者3 小时前
【Java集合】ArrayList :底层原理、数组互转与扩容计算
java·开发语言·jvm·数据库·后端·python·算法