MySQL查询语句执行顺序

每个查询都从在数据库中找到我们需要的数据开始,然后将这些数据过滤到可以尽快处理和理解。由于查询的各个部分都是按顺序执行的,因此了解执行顺序很重要,这样才能知道在哪可以得到什么样的结果。

References:

首先需要说明的是:我并未寻找到任何MySQL官方文档对查询的执行顺序有明确说明,因此只能根据书籍和其他的资料(如微软关于SQL Server的文档)总结出本文。查询的执行会受到许多因素的影响(如缓存),此处所列出的顺序只是描述了一般情况下的查询是什么流程,帮助理解SQL。

查询操作是关系数据据中使用最为频繁的操作,可以将查询操作分为逻辑查询处理及物理查询处理。逻辑查询处理表示执行查询应该产生什么样的结果,而物理查询代表MySQL是如何得到该结果的。两种查询的方法可能完全不同,但是得到的结果必定是相同的。

查询的逻辑执行顺序(Logical Processing Order)如下:

  1. FROM 和 JOIN
    首先执行FROM子句和随后的JOIN,以确定查询要处理的的数据集。这包括此子句中的子查询,并且可能导致在后台创建包含连接表所有行和列的临时表。
  2. WHERE
    一旦有了全部的数据集,就将WHERE的约束应用于每一行,并舍弃不符合的行。每个约束只能直接从FROM子句请求的表中访问列。在大多数数据库中,此刻SELECT部分中的别名不可访问,因为它们可能包含依赖于尚未执行的查询部分的表达式。
  3. SELECT
    查询的SELECT部分所有的表达式都将被计算。
  4. GROUP BY
    根据GROUP BY子句指定的列对数据分组。通常意味着仅在有聚合函数时才需要使用此功能。
  5. HAVING
    如果查询有GROUP BY子句,则HAVING子句中的约束将应用于分组的行,舍弃不满足约束的行。像WHERE子句一样,在大多数数据库中,这一步也无法访问别名。
  6. ORDER BY
    如果ORDER BY子句指定了顺序,则将按指定的数据以升序或降序进行排序。由于SELECT部分的所有表达式均已计算,因此可以在此子句中使用别名。
  7. 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_amountcustomer_name
159.72RILEY BRITTANY
152.68RENNER NEIL
142.70RODRIQUEZ VIOLET
142.66ROMERO MINNIE
141.70ROBINS GREG
139.71RICE JAMIE
137.70ROSS MARILYN
133.70REYNOLDS ROSA
130.68REYES DEBBIE
128.71ROGERS 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在两种数据库下执行的结果:
Snipaste_2021-04-27_11-23-58.png

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的结果为例:
Snipaste_2021-04-27_11-13-57.png

Snipaste_2021-04-27_11-14-17.png

Snipaste_2021-04-27_11-14-41.png

可以看到,在SQL Server中,如果在GROUP BY和HAVING子句中使用SELECT的别名是会报错的。但是这也只能验证SQL Server的文档中描述的顺序是正确的,并不能得出MySQL的执行顺序,所以一切仍然存疑。

逻辑查询描述了应该怎样处理数据,但是实际上数据库不会完全按照逻辑方式查询。MySQL数据库层有Parser和Optimizer两个组件。Parser分析语句,Optimizer对语句进行优化,选择一条最优的路径获取数据,但是必须保证物理查询的最终结果和逻辑查询是一致的。
httpatomoreillycomsourceoreillyimages206456.png

如上图所示,如果命中缓存,甚至不会解析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。

标签: none

添加新评论

ali-01.gifali-58.gifali-09.gifali-23.gifali-04.gifali-46.gifali-57.gifali-22.gifali-38.gifali-13.gifali-10.gifali-34.gifali-06.gifali-37.gifali-42.gifali-35.gifali-12.gifali-30.gifali-16.gifali-54.gifali-55.gifali-59.gif

加载中……