华为Java面试被问:SQL执行顺序

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

发生了什么

  1. 扫描orders表,生成虚拟表VT1

  2. 执行JOIN操作:

    • INNER JOIN:只保留匹配的行

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

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

    • FULL JOIN:保留所有行

  3. 生成包含两个表所有列的虚拟表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

此时可以

  1. 选择需要的列

  2. 使用聚合函数

  3. 使用计算表达式

  4. 定义列别名

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;

执行顺序

  1. 执行FROM中的子查询

  2. 对结果集的每一行执行SELECT中的子查询


四、UNION的执行顺序

sql

复制代码
(SELECT id, name FROM table1 WHERE condition1)
UNION ALL
(SELECT id, name FROM table2 WHERE condition2)
ORDER BY name
LIMIT 100;

执行顺序

  1. 执行第一个SELECT查询

  2. 执行第二个SELECT查询

  3. 合并两个结果集(UNION ALL不去重,UNION去重)

  4. 对合并结果排序

  5. 应用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秒。"

高频问题

  1. SELECT中的别名能在WHERE中使用吗?

    • 不能,因为WHERE在SELECT之前执行
  2. HAVING和WHERE有什么区别?

    • WHERE过滤行,HAVING过滤组;WHERE不能用聚合,HAVING可以
  3. ORDER BY可以使用SELECT的别名吗?

    • 可以,因为ORDER BY在SELECT之后执行
  4. LIMIT在SQL执行中是什么位置?

    • 最后执行,在排序完成后才分页

掌握SQL执行顺序,是写出高效SQL、进行查询优化的基础。每次写复杂查询时,都可以在心里过一遍这个执行流程,确保逻辑正确。

相关推荐
遇到困难睡大觉哈哈2 小时前
HarmonyOS收银台设计规范:构建简洁高效的支付体验
华为·harmonyos·设计规范
北方的流星2 小时前
华为访问控制列表的配置
运维·网络·华为
码上成长2 小时前
长耗时接口异步改造总结
前端·git·后端
中文很快乐2 小时前
java后端好习惯---新手养成记
java·开发语言·开发好习惯·踩坑日志·新手养成
Acc1oFl4g2 小时前
Java安全之SpEL表达式注入入门学习
java·学习·安全
风华同学2 小时前
【系统移植篇】系统烧写
java·开发语言·前端
武哥聊编程2 小时前
【从0带做】基于Springboot3+Vue3的生态养殖管理系统
java·学习·vue·毕业设计·springboot
隔山打牛牛2 小时前
如何实现jvm中自定义加载器?
java
菜鸟233号2 小时前
力扣106 从中序与后序遍历序列构造二叉树 java实现
java·算法·leetcode