SQL执行顺序完全指南:从WHERE到ORDER BY的完整旅程
理解SQL的执行顺序是写出高效查询、优化性能、排查问题的关键。下图展示了SQL查询的完整执行流程:
代码
flowchart TD
A[开始执行SQL查询] --> B
subgraph B [第1步: FROM/JOIN]
B1[加载表数据]
B2[执行所有JOIN操作]
B3[生成虚拟表VT1]
end
B --> C[第2步: WHERE条件过滤]
C --> D[生成虚拟表VT2]
D --> E[第3步: GROUP BY分组]
E --> F[生成虚拟表VT3]
F --> G[第4步: HAVING分组过滤]
G --> H[生成虚拟表VT4]
H --> I[第5步: SELECT投影]
I --> J[生成虚拟表VT5]
J --> K[第6步: DISTINCT去重]
K --> L[生成虚拟表VT6]
L --> M[第7步: ORDER BY排序]
M --> N[生成虚拟表VT7]
N --> O[第8步: LIMIT/OFFSET分页]
O --> P[返回最终结果集]
一、核心执行顺序详解
标准SQL查询结构
sql
-- 完整SQL查询示例
SELECT DISTINCT
user_id,
COUNT(*) as order_count,
SUM(amount) as total_amount
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'completed'
AND u.country = 'China'
AND o.order_date >= '2024-01-01'
GROUP BY user_id
HAVING total_amount > 1000
ORDER BY total_amount DESC
LIMIT 10
OFFSET 0;
执行顺序口诀
text
从哪来 → 筛什么 → 怎么分 → 分组筛 → 选什么
→ 去重吗 → 怎么排 → 要多少
二、8个执行阶段的深度解析
阶段1:FROM & JOIN(数据源准备)
sql
-- 实际执行的第一步
FROM orders o
JOIN users u ON o.user_id = u.id
发生了什么:
-
扫描
orders表,生成虚拟表VT1 -
执行JOIN操作:
-
INNER JOIN:只保留匹配的行 -
LEFT JOIN:保留左表所有行,右表无匹配则NULL -
RIGHT JOIN:保留右表所有行,左表无匹配则NULL -
FULL JOIN:保留所有行
-
-
生成包含两个表所有列的虚拟表VT2
性能提示:
sql
-- 优化前(性能差)
SELECT *
FROM large_table lt
JOIN huge_table ht ON lt.id = ht.large_id -- 大表连接大表
WHERE lt.status = 'active';
-- 优化后(性能好)
SELECT *
FROM (
SELECT id, name -- 先过滤和减少列
FROM large_table
WHERE status = 'active'
) filtered_lt
JOIN huge_table ht ON filtered_lt.id = ht.large_id;
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案
【点击此处即可/免费获取】
阶段2:WHERE(行级过滤)
sql
WHERE o.status = 'completed'
AND u.country = 'China'
AND o.order_date >= '2024-01-01'
重要规则:
-
此时不能使用SELECT中的别名
-
对每一行进行判断,符合条件的进入下一阶段
-
WHERE在GROUP BY之前执行
sql
-- ❌ 错误:WHERE中不能使用SELECT别名
SELECT user_id, SUM(amount) as total
FROM orders
WHERE total > 1000 -- 报错!total还未计算
GROUP BY user_id;
-- ✅ 正确:使用原始表达式
SELECT user_id, SUM(amount) as total
FROM orders
WHERE amount > 100 -- 对每行amount判断
GROUP BY user_id;
阶段3:GROUP BY(分组聚合)
sql
GROUP BY user_id
分组过程:
text
原始数据:
user_id | amount
--------|-------
1 | 100
1 | 200
2 | 150
2 | 250
2 | 300
分组后:
组1: user_id=1 → [100, 200]
组2: user_id=2 → [150, 250, 300]
特殊情况:
sql
-- GROUP BY有多列
SELECT country, city, COUNT(*)
FROM users
GROUP BY country, city; -- 按country和city组合分组
-- GROUP BY表达式
SELECT YEAR(order_date) as year, COUNT(*)
FROM orders
GROUP BY YEAR(order_date); -- 按表达式分组
阶段4:HAVING(组级过滤)
sql
HAVING total_amount > 1000
与WHERE的区别:
| WHERE | HAVING |
|---|---|
| 在分组前过滤行 | 在分组后过滤组 |
| 不能使用聚合函数 | 可以使用聚合函数 |
| 作用于每一行 | 作用于每一组 |
sql
-- 同时使用WHERE和HAVING
SELECT user_id, SUM(amount) as total
FROM orders
WHERE status = 'completed' -- 先过滤行
GROUP BY user_id
HAVING total > 1000; -- 再过滤组
阶段5:SELECT(列投影)
sql
SELECT
user_id,
COUNT(*) as order_count,
SUM(amount) as total_amount
此时可以:
-
选择需要的列
-
使用聚合函数
-
使用计算表达式
-
定义列别名
sql
-- 复杂SELECT示例
SELECT
user_id,
COUNT(*) as order_count,
SUM(amount) as total_amount,
AVG(amount) as avg_amount,
MAX(amount) as max_amount,
MIN(amount) as min_amount,
SUM(amount) * 0.1 as tax_amount, -- 计算表达式
CONCAT('User_', user_id) as user_tag -- 字符串函数
FROM orders
GROUP BY user_id;
阶段6:DISTINCT(去重)
sql
SELECT DISTINCT ...
去重时机:在SELECT之后,ORDER BY之前
sql
-- 去重整个结果集
SELECT DISTINCT country, city
FROM users;
-- 去重特定列(MySQL不支持,需用GROUP BY)
-- ❌ 错误:SELECT country, DISTINCT city FROM users;
-- ✅ 正确:使用GROUP BY模拟
SELECT country, city
FROM users
GROUP BY country, city; -- 达到去重效果
阶段7:ORDER BY(排序)
sql
ORDER BY total_amount DESC
关键点:
-
此时可以使用SELECT中的别名
-
可以多列排序
-
可以使用表达式排序
sql
-- 多列排序
ORDER BY total_amount DESC, order_date ASC;
-- 使用表达式排序
ORDER BY
CASE
WHEN total_amount > 1000 THEN 1
WHEN total_amount > 500 THEN 2
ELSE 3
END,
user_id;
-- 按SELECT中未显示的列排序
SELECT user_id, SUM(amount) as total
FROM orders
GROUP BY user_id
ORDER BY COUNT(*) DESC; -- 按聚合结果排序
阶段8:LIMIT & OFFSET(分页)
sql
LIMIT 10
OFFSET 0
执行顺序最后:在排序完成后才应用分页
sql
-- 分页查询优化(避免深度分页)
-- ❌ 性能差:OFFSET越大越慢
SELECT * FROM large_table
ORDER BY id
LIMIT 10 OFFSET 1000000; -- 需要扫描1000010行
-- ✅ 性能好:使用WHERE条件
SELECT * FROM large_table
WHERE id > 1000000 -- 记录上次查询的最大ID
ORDER BY id
LIMIT 10;
三、子查询的执行顺序
相关子查询 vs 非相关子查询
sql
-- 非相关子查询:先执行子查询
SELECT name, salary
FROM employees
WHERE salary > (
SELECT AVG(salary) -- 先执行,返回单一值
FROM employees
);
-- 相关子查询:对外部查询每一行执行一次
SELECT e.name, e.salary, d.name
FROM employees e
WHERE salary > (
SELECT AVG(salary) -- 对每个部门的e.dept_id执行
FROM employees
WHERE dept_id = e.dept_id
);
子查询在SELECT中
sql
SELECT
user_id,
order_count,
(SELECT MAX(amount) FROM orders o2
WHERE o2.user_id = o1.user_id) as max_amount -- 对每组执行
FROM (
SELECT user_id, COUNT(*) as order_count
FROM orders
GROUP BY user_id
) o1;
执行顺序:
-
执行FROM中的子查询
-
对结果集的每一行执行SELECT中的子查询
四、UNION的执行顺序
sql
(SELECT id, name FROM table1 WHERE condition1)
UNION ALL
(SELECT id, name FROM table2 WHERE condition2)
ORDER BY name
LIMIT 100;
执行顺序:
-
执行第一个SELECT查询
-
执行第二个SELECT查询
-
合并两个结果集(UNION ALL不去重,UNION去重)
-
对合并结果排序
-
应用LIMIT
五、窗口函数的执行顺序
sql
SELECT
user_id,
order_date,
amount,
SUM(amount) OVER (
PARTITION BY user_id
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) as running_total
FROM orders
WHERE status = 'completed'
ORDER BY user_id, order_date;
执行时机 :在WHERE之后,ORDER BY之前,但晚于普通GROUP BY
六、CTE(公用表表达式)的执行顺序
sql
WITH monthly_sales AS (
-- 第一步:执行CTE查询
SELECT
user_id,
DATE_TRUNC('month', order_date) as month,
SUM(amount) as monthly_total
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY user_id, DATE_TRUNC('month', order_date)
),
user_avg AS (
-- 第二步:基于第一个CTE执行第二个CTE
SELECT
user_id,
AVG(monthly_total) as avg_monthly
FROM monthly_sales
GROUP BY user_id
)
-- 第三步:执行主查询
SELECT
ms.user_id,
ms.month,
ms.monthly_total,
ua.avg_monthly
FROM monthly_sales ms
JOIN user_avg ua ON ms.user_id = ua.user_id
WHERE ms.monthly_total > ua.avg_monthly
ORDER BY ms.user_id, ms.month;
七、执行顺序对性能的影响
案例1:WHERE位置错误
sql
-- ❌ 性能差:JOIN大量数据后再过滤
SELECT u.name, o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed' -- 在外连接后过滤,效率低
AND u.country = 'China';
-- ✅ 性能好:先过滤再JOIN
SELECT u.name, o.amount
FROM (
SELECT * FROM users
WHERE country = 'China'
) u
LEFT JOIN (
SELECT * FROM orders
WHERE status = 'completed'
) o ON u.id = o.user_id;
案例2:聚合函数位置错误
sql
-- ❌ 错误:SELECT中使用WHERE过滤的列
SELECT
user_id,
SUM(amount) as total,
AVG(CASE WHEN status = 'completed' THEN amount END) as avg_completed
FROM orders
WHERE status IN ('completed', 'pending') -- 过滤了'cancelled'
GROUP BY user_id;
-- ✅ 正确:所有状态都参与计算
SELECT
user_id,
SUM(amount) as total,
AVG(CASE WHEN status = 'completed' THEN amount END) as avg_completed,
COUNT(CASE WHEN status = 'cancelled' THEN 1 END) as cancelled_count
FROM orders
-- 不在这里过滤状态
GROUP BY user_id;
案例3:索引利用优化
sql
-- 表orders有索引:idx_status_date(status, order_date)
-- ❌ 无法充分利用索引
SELECT * FROM orders
WHERE YEAR(order_date) = 2024 -- 函数导致索引失效
AND status = 'completed';
-- ✅ 充分利用索引
SELECT * FROM orders
WHERE order_date >= '2024-01-01'
AND order_date < '2025-01-01'
AND status = 'completed'; -- 可以用(status, order_date)索引
八、各数据库的执行差异
MySQL执行特点
sql
-- MySQL允许在GROUP BY中省略非聚合列
SELECT user_id, product_id, COUNT(*) -- ❌ 不符合SQL标准但MySQL允许
FROM orders
GROUP BY user_id; -- product_id不在GROUP BY中
-- 关闭ONLY_FULL_GROUP_BY模式可允许
SET SESSION sql_mode = '';
PostgreSQL执行特点
sql
-- PostgreSQL严格执行SQL标准
SELECT user_id, product_id, COUNT(*) -- ❌ 报错
FROM orders
GROUP BY user_id;
-- ✅ 必须明确GROUP BY所有非聚合列
SELECT user_id, product_id, COUNT(*)
FROM orders
GROUP BY user_id, product_id;
执行计划查看
sql
-- MySQL
EXPLAIN
SELECT user_id, COUNT(*)
FROM orders
WHERE status = 'completed'
GROUP BY user_id;
-- PostgreSQL
EXPLAIN ANALYZE
SELECT user_id, COUNT(*)
FROM orders
WHERE status = 'completed'
GROUP BY user_id;
-- SQL Server
SET SHOWPLAN_ALL ON;
SELECT user_id, COUNT(*)
FROM orders
WHERE status = 'completed'
GROUP BY user_id;
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案
【点击此处即可/免费获取】
九、面试深度回答要点
基础回答
"SQL执行顺序是:FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT。理解这个顺序能避免很多错误,比如WHERE中不能使用SELECT别名。"
进阶回答
"从查询优化器角度看,实际执行可能重排序,但逻辑顺序不变。关键要理解:1) WHERE在GROUP BY前,用于行过滤;2) HAVING在GROUP BY后,用于组过滤;3) SELECT别名只能在ORDER BY和HAVING中使用;4) 窗口函数在WHERE后但ORDER BY前执行。"
实战回答
"在优化SQL时,我利用执行顺序:1) 在WHERE中尽早过滤数据;2) 避免在WHERE中对索引列使用函数;3) 将复杂条件从HAVING移到WHERE(如果可能);4) 对于深度分页,用WHERE id > last_id替代OFFSET。比如最近优化的一个查询,通过调整条件位置,从5秒降到0.2秒。"
高频问题
-
SELECT中的别名能在WHERE中使用吗?
- 不能,因为WHERE在SELECT之前执行
-
HAVING和WHERE有什么区别?
- WHERE过滤行,HAVING过滤组;WHERE不能用聚合,HAVING可以
-
ORDER BY可以使用SELECT的别名吗?
- 可以,因为ORDER BY在SELECT之后执行
-
LIMIT在SQL执行中是什么位置?
- 最后执行,在排序完成后才分页
掌握SQL执行顺序,是写出高效SQL、进行查询优化的基础。每次写复杂查询时,都可以在心里过一遍这个执行流程,确保逻辑正确。