一条SQL执行过程:从查询到结果的奇幻之旅
想象一下,你是一位厨师长,SQL就是你的菜谱,MySQL则是整个厨房团队。今天,我们要做一道“部门薪资统计”大餐,来看看厨房团队是如何一步步按你的菜谱工作的。
我们的“菜谱”——复杂SQL示例
SELECT
d.name AS dept_name,
COUNT(e.id) AS emp_count,
AVG(e.salary) AS avg_salary,
MAX(e.salary) AS max_salary
FROM departments d
LEFT JOIN employees e ON e.dept_id = d.id AND e.status = 'active'
INNER JOIN salaries s ON s.emp_id = e.id
WHERE d.region IN ('华东', '华南')
AND s.year = 2024
GROUP BY d.id, d.name
HAVING COUNT(e.id) >= 5
ORDER BY avg_salary DESC
LIMIT 10;
厨房工作流程图解
[准备食材] → [初步加工] → [精细筛选] → [分类汇总] → [质量检查] → [摆盘上菜]
FROM JOIN WHERE GROUP BY HAVING SELECT/ORDER/LIMIT
第一步:准备食材(FROM)
厨房动作:打开departments表,把12个部门的信息全部拿出来放在工作台上。
执行器内部:
- 定位departments表的存储位置
- 读取所有行记录到内存缓冲区
- 为每行打上临时标签“d”(就像给食材贴上“主料”标签)
关键点:这是所有操作的起点,没有部门数据,后面的所有操作都是空谈。
第二步:初步加工(JOIN - 复杂关联处理)
这是最核心也最容易出错的一步,让我们拆开看:
2.1 LEFT JOIN employees(左连接员工表)
厨房比喻:为每个部门找对应的活跃员工,就算某个部门没有活跃员工,也要保留这个部门的信息。
执行器实际动作:
- 从第一个部门(比如id=1)开始
- 到employees表里寻找:
dept_id = 1 AND status = 'active'的员工 - 找到127个符合条件的员工
- 把部门1的信息复制127份,每份配上不同的员工信息
- 遇到部门7时,发现没有活跃员工
- 关键行为:仍然保留部门7的信息,但员工字段全部填NULL
- 继续处理所有12个部门
结果:从12行部门数据,变成了904行中间数据(有些部门匹配多个员工,有些部门只有1行NULL员工数据)
2.2 INNER JOIN salaries(内连接薪资表)
厨房比喻:现在只保留那些有薪资记录的数据,没有薪资记录的全部扔掉。
执行器实际动作:
- 从上一步的904行数据开始
- 对于员工字段为NULL的行(如部门7的那行),直接丢弃
- 对于有员工信息的行,到salaries表里找对应员工的2024年薪资记录
- 找到匹配的就保留,找不到的也丢弃
重要转折:经过这一步,部门7(没有活跃员工)的记录被彻底移除了,因为INNER JOIN不接受NULL匹配。
第三步:精细筛选(WHERE)
厨房比喻:现在我们有892行“部门-员工-薪资”组合,开始按条件筛选。
执行器实际动作:
- 检查每行数据的
d.region字段:必须是“华东”或“华南” - 检查每行数据的
s.year字段:必须是2024 - 两个条件都满足的才保留
有趣的事实:虽然我们的salaries表里可能有多年的数据,但INNER JOIN时已经通过ON条件s.emp_id = e.id隐式筛选了,这里的s.year = 2024更像是双重保险。
结果:892行减少到218行,只剩下华东和华南地区的部门数据。
第四步:分类汇总(GROUP BY)
厨房比喻:把218行数据按部门分类,统计每个部门的信息。
执行器实际动作:
- 创建一个临时“分类桶”数据结构
- 读取第一行数据:部门1,员工A,薪资50000
- 找到“部门1”的桶
- 员工计数+1
- 总薪资增加50000
- 更新最高薪资为50000
- 读取第二行数据:部门1,员工B,薪资60000
- 找到“部门1”的桶
- 员工计数变为2
- 总薪资变为110000
- 更新最高薪资为60000
- 如此处理所有218行
内存变化:执行器在内存中维护一个哈希表,键是(部门ID, 部门名称),值是该部门的统计信息。
第五步:质量检查(HAVING)
厨房比喻:汇总完成后,只保留员工数达到5人以上的部门。
执行器实际动作:
- 遍历每个部门的统计桶
- 检查
COUNT(e.id) >= 5是否成立 - 员工数不足5人的部门被剔除
与WHERE的区别:
- WHERE在分组前过滤单行数据(如“只要华东地区的部门”)
- HAVING在分组后过滤整个分组(如“只要员工数≥5的部门”)
第六步:计算与呈现(SELECT)
厨房比喻:现在为每个合格的部门计算最终展示的数据。
执行器实际动作:
- 对于部门1:员工数=127,总薪资=1820万
dept_name= “技术部”(直接从原数据读取)emp_count= 127avg_salary= 18200000 ÷ 127 ≈ 143,307max_salary= 从统计中读取最大值
- 如此计算每个合格部门
第七步:排序与限量(ORDER BY + LIMIT)
厨房动作:
- 将所有结果按平均薪资从高到低排序
- 只取前10名
- 由于我们只有4个合格部门,所以返回全部4行
优化提示:如果有1000个合格部门,执行器会在内存中排序后,只返回前10行,节省数据传输。
关键知识点总结
1. 执行顺序是铁律
FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
这个顺序永远不会改变,就像做菜必须先备料再烹饪一样。
2. LEFT JOIN的微妙之处
- ON条件在JOIN时立即生效
- WHERE条件在所有JOIN完成后才生效
- 如果把LEFT JOIN的过滤条件写在WHERE里,可能会意外过滤掉NULL行,使LEFT JOIN退化为INNER JOIN
3. 数据量的变化轨迹
departments: 12行
↓ LEFT JOIN employees
中间结果: 904行(膨胀)
↓ INNER JOIN salaries
中间结果: 892行(收缩)
↓ WHERE过滤
中间结果: 218行(大幅收缩)
↓ GROUP BY
分组结果: 6个桶
↓ HAVING过滤
最终分组: 4个桶
↓ SELECT计算
返回结果: 4行
4. 性能优化的黄金法则
- 尽早过滤:WHERE条件能让后续处理的数据量最小化
- 正确使用索引:JOIN条件和WHERE条件的字段应该有索引
- 避免过度GROUP BY:只按必要的字段分组
- LIMIT是好朋友:尽早使用LIMIT可以减少不必要的工作
厨房团队的协作智慧
MySQL执行器就像一个高效的厨房团队:
- 优化器是主厨,决定最佳执行计划
- 执行器是厨师长,指挥各个步骤
- 存储引擎是厨工,负责具体的数据存取
- 缓冲区是工作台,存放中间结果
下次你写SQL时,不妨想象一下这个厨房团队是如何工作的。理解这个流程,不仅能帮你写出更高效的SQL,还能在出现问题时快速定位:“哦,问题可能出在JOIN阶段”或“WHERE条件过滤得太晚了”。
记住,好的SQL不仅是正确的,更是优雅高效的——就像一道精心烹制的菜肴,每一步都恰到好处。

1192

被折叠的 条评论
为什么被折叠?



