在 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
核心作用 :对原始数据进行行级过滤,剔除不满足条件的数据
关键特性
-
WHERE 执行在分组之前:无法对分组后的聚合结果过滤
-
WHERE 过滤效率极高:尽早过滤无效数据,减少后续计算量(SQL 优化核心:WHERE 条件尽量精准)
-
不能使用聚合函数:如 SUM/COUNT/AVG 等
-
过滤掉 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
实战场景
-
按用户分组统计订单总金额
-
按日期分组统计每日订单量
-
按商品分类分组统计销量
关键知识点
-
分组后,SELECT 只能查询分组字段 + 聚合函数
-
分组会自动排序(无 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
核心作用 :从虚拟表中提取最终需要展示的字段,剔除无关字段
关键知识点
-
SELECT 执行在分组 / 过滤之后:这是为什么不能在 WHERE 中使用 SELECT 别名的原因
-
支持普通字段、聚合函数、表达式、别名
-
规范:禁止使用 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
核心作用 :对最终结果集按照指定字段升序 / 降序排序
关键特性
-
ORDER BY 是最后一步数据处理操作(除 LIMIT 外)
-
支持:单个字段、多个字段、聚合结果排序
-
性能风险:无索引时,大数据量排序会产生
filesort(慢查询核心诱因)
实战示例
-- 按订单总金额降序排序
ORDER BY totalAmount DESC
阶段 8:LIMIT 分页
执行优先级:最低
输入源:VT6
输出源:VT7(最终结果集)
核心作用:截取指定范围的行数据,实现分页查询
实战规范
-
分页必须配合
ORDER BY使用,保证分页数据有序且不重复 -
大数据量分页(如 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;
实际执行顺序
-
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
-
SELECT u.id, u.username, SUM(o.order_amount)
-
ORDER BY totalAmount DESC
-
LIMIT 0,10
四、核心总结
-
执行顺序不可逆:FROM 最先,LIMIT 最后,SELECT 在中间阶段执行
-
虚拟表是核心:所有数据处理都基于临时虚拟表,不直接操作物理表
-
过滤越早性能越高:WHERE 优先过滤数据,减少后续计算量
-
内外连接核心差异:LEFT/RIGHT/FULL JOIN 会执行「添加外部行」步骤
-
WHERE 与 HAVING 分工明确:WHERE 过滤行,HAVING 过滤分组
-
别名使用限制:SELECT 定义的别名,只能在 ORDER BY 中使用,WHERE/GROUP BY/HAVING 均无法使用