MySQL查询语句执行顺序
每个查询都从在数据库中找到我们需要的数据开始,然后将这些数据过滤到可以尽快处理和理解。由于查询的各个部分都是按顺序执行的,因此了解执行顺序很重要,这样才能知道在哪可以得到什么样的结果。
References:
- 《MySQL技术内幕:SQL编程》
- https://dev.mysql.com/doc/refman/8.0/en/select.html MySQL SELECT语句官方文档
- https://docs.microsoft.com/en-us/sql/t-sql/queries/select-transact-sql?redirectedfrom=MSDN&view=sql-server-ver15#logical-processing-order-of-the-select-statement SQL Server关于查询语句逻辑处理顺序的介绍
- https://stackoverflow.com/questions/24127932/mysql-query-clause-execution-order?answertab=votes#tab-top
- https://sqlbolt.com/lesson/select_queries_order_of_execution
- https://www.oreilly.com/library/view/high-performance-mysql/9780596101718/ch04.html
- https://dev.mysql.com/doc/internals/en/optimizer-code.html
- https://dev.mysql.com/doc/internals/en/select-steps.html
首先需要说明的是:我并未寻找到任何MySQL官方文档对查询的执行顺序有明确说明,因此只能根据书籍和其他的资料(如微软关于SQL Server的文档)总结出本文。查询的执行会受到许多因素的影响(如缓存),此处所列出的顺序只是描述了一般情况下的查询是什么流程,帮助理解SQL。
查询操作是关系数据据中使用最为频繁的操作,可以将查询操作分为逻辑查询处理及物理查询处理。逻辑查询处理表示执行查询应该产生什么样的结果,而物理查询代表MySQL是如何得到该结果的。两种查询的方法可能完全不同,但是得到的结果必定是相同的。
查询的逻辑执行顺序(Logical Processing Order)如下:
- FROM 和 JOIN
首先执行FROM子句和随后的JOIN,以确定查询要处理的的数据集。这包括此子句中的子查询,并且可能导致在后台创建包含连接表所有行和列的临时表。 - WHERE
一旦有了全部的数据集,就将WHERE的约束应用于每一行,并舍弃不符合的行。每个约束只能直接从FROM子句请求的表中访问列。在大多数数据库中,此刻SELECT部分中的别名不可访问,因为它们可能包含依赖于尚未执行的查询部分的表达式。 - SELECT
查询的SELECT部分所有的表达式都将被计算。 - GROUP BY
根据GROUP BY子句指定的列对数据分组。通常意味着仅在有聚合函数时才需要使用此功能。 - HAVING
如果查询有GROUP BY子句,则HAVING子句中的约束将应用于分组的行,舍弃不满足约束的行。像WHERE子句一样,在大多数数据库中,这一步也无法访问别名。 - ORDER BY
如果ORDER BY子句指定了顺序,则将按指定的数据以升序或降序进行排序。由于SELECT部分的所有表达式均已计算,因此可以在此子句中使用别名。 - LIMIT / OFFSET
最后,舍弃LIMIT和OFFSET指定范围之外的行,剩余的数据集被返回。
以Sakila示例数据库为例的一个SQL如下:
SELECT
SUM( p.amount ) AS total_payment_amount,
CONCAT( c.last_name, " ", c.first_name ) AS customer_name
FROM
payment p
LEFT JOIN customer c ON p.customer_id = c.customer_id
WHERE
c.last_name LIKE "R%"
/* 因为racecoder开头是R,所以这里只查R开头的名字:) */
GROUP BY
customer_name
HAVING
total_payment_amount > 50
ORDER BY
total_payment_amount DESC
LIMIT 10;
查询结果如下:
total_payment_amount | customer_name |
---|---|
159.72 | RILEY BRITTANY |
152.68 | RENNER NEIL |
142.70 | RODRIQUEZ VIOLET |
142.66 | ROMERO MINNIE |
141.70 | ROBINS GREG |
139.71 | RICE JAMIE |
137.70 | ROSS MARILYN |
133.70 | REYNOLDS ROSA |
130.68 | REYES DEBBIE |
128.71 | ROGERS TERESA |
这条SQL查询了所有付费用户中,last_name以R字母开头,并且总付费最多的前10人(金额需大于50)。
不能在WHERE子句中使用SELECT中的列的别名,这是理解解析过程的关键。
可以看到除了在WHERE子句中,其他子句都使用了SELECT中的别名(total_payment_amount或customer_name),如果在WHERE子句中使用customer_name,会报错:Unknown column 'customer_name' in 'where clause'
。由此可以推断出SELECT是在这些子句之前执行的。
这个结果和书中和很多人的文章都不一样,甚至和SQL Server的文档都不一样,但是事实如此,不然以上的别名又该如何解释?我想大部分将SELECT放在后面的人都是受到SQL Server的那篇文档的影响,或许是MySQL和SQL Server不一样,又或许是我的方法有问题。总之没有官方定论,一切都是猜测,但是我觉得能理解SQL执行从FROM开始其实就已经足够。这里也给出SQL Server的执行结果做个对比,由于我本地并没有安装SQL Server,所以只是找了个 在线 的测试了一下,在页面的右上方可以切换数据库类型,你可以测试相同的SQL在两种数据库下执行的结果:
SELECT continent AS cont, SUM(population) AS total_pop, AVG(gdp) AS avg_gdb
FROM world
WHERE continent <> 'Caribbean'
GROUP BY continent
HAVING AVG(gdp) > 3000000
ORDER BY avg_gdb DESC;
下面以SQL Server的执行这个SQL的结果为例:
可以看到,在SQL Server中,如果在GROUP BY和HAVING子句中使用SELECT的别名是会报错的。但是这也只能验证SQL Server的文档中描述的顺序是正确的,并不能得出MySQL的执行顺序,所以一切仍然存疑。
逻辑查询描述了应该怎样处理数据,但是实际上数据库不会完全按照逻辑方式查询。MySQL数据库层有Parser和Optimizer两个组件。Parser分析语句,Optimizer对语句进行优化,选择一条最优的路径获取数据,但是必须保证物理查询的最终结果和逻辑查询是一致的。
如上图所示,如果命中缓存,甚至不会解析SQL,直接将缓存结果返回。比如合理的索引,可以让约束通过索引快速的过滤数据,而不需要逐行排查。
最后,附上MySQL官方所谓的SELECT执行过程,感觉啥都没说:
以下显示了在/sql/sql_select.cc
中的handle_select()
(处理查询的服务器代码)函数的结构:
handle_select()
mysql_select()
JOIN::prepare()
setup_fields()
JOIN::optimize() /* optimizer is from here ... */
optimize_cond()
opt_sum_query()
make_join_statistics()
get_quick_record_count()
choose_plan()
/* Find the best way to access tables */
/* as specified by the user. */
optimize_straight_join()
best_access_path()
/* Find a (sub-)optimal plan among all or subset */
/* of all possible query plans where the user */
/* controls the exhaustiveness of the search. */
greedy_search()
best_extension_by_limited_search()
best_access_path()
/* Perform an exhaustive search for an optimal plan */
find_best()
make_join_select() /* ... to here */
JOIN::exec()
SELECT执行步骤如下:
JOIN::prepare
- 初始化并将JOIN structure链接到st_select_lex。
- 对于每一项执行fix_fields()(执行fix_fields()之后,就了解有关项的所有信息)。
- 将HAVING移动到WHERE(如果可能的话)。
- 初始化存储过程(如果有)。
JOIN::optimize
- Single select优化。(原谅我实在不知道怎么翻译比较合适)
- 创建第一个临时表(如果需要)。
- JOIN::exec
执行select(可能创建第二个临时表)。 - JOIN::cleanup
删除所有临时表,进行其他清理。 - JOIN::reinit
准备要执行SELECT(以及JOIN::exec)的所有structure。