玩技术,Geeker
一个原创技术文章分享网站

SQL逻辑查询语句执行顺序

我的抱怨

我一个搞应用开发的,非要会数据库,这不是专门的数据库开发人员干的事么?话说,小公司也没有数据库开发人员这么个职位吧。好吧,对数据库最深的印象还停留在大学《数据库原理》这堂课上,什么第一范式,第二范式…,这些理论的东西,多多少少还是记得点,至于更深层次的,我不会。所以呢,撸起袖子,开始学习吧。

干程序员,最不怕的就是学习,如果你连学习都怕了,那还是早点退出这行吧。你说是吧。而我今天这篇文章,既不总结什么深奥的理论,也不总结多么高深的架构(我也不会)。就从最基本的SELECT语句开始吧。

最后,这篇文章是我读《MySQL技术内幕:SQL编程》而总结出来的,对于书中有的东西讲的比较“粗”,可能是我的水平没有达到人家作者要求的水平,导致阅读起来,不是很舒服,所以,这篇博文,将会非常细致的进行总结。只有你想不到,没有你做不到。

能看懂么?

先来一段伪代码,首先你能看懂么?

SELECT DISTINCT <select_list>
FROM <left_table>
<join_type> JOIN <right_table>
ON <join_condition>
WHERE <where_condition>
GROUP BY <group_by_list>
HAVING <having_condition>
ORDER BY <order_by_condition>
LIMIT <limit_number>

如果你知道每个关键字的意思,作用,如果你还用过的话,那再好不过了。但是,你知道这些语句,它们的执行顺序你清楚么?如果你非常清楚,你就没有必要再浪费时间继续阅读了;如果你不清楚,非常好,你应该庆幸你阅读到了这么好的一篇文章。

准备工作

首先声明下,一切测试操作都是在MySQL数据库上完成,关于MySQL数据库的一些简单操作,请阅读一下文章:

继续做以下的前期准备工作:

  1. 新建一个测试数据库TestDB;
      create database TestDB;
  2. 创建测试表table1和table2;
     CREATE TABLE table1
     (
         customer_id VARCHAR(10) NOT NULL,
         city VARCHAR(10) NOT NULL,
         PRIMARY KEY(customer_id)
     )ENGINE=INNODB DEFAULT CHARSET=UTF8;
    
     CREATE TABLE table2
     (
         order_id INT NOT NULL auto_increment,
         customer_id VARCHAR(10),
         PRIMARY KEY(order_id)
     )ENGINE=INNODB DEFAULT CHARSET=UTF8;
  3. 插入测试数据;
     INSERT INTO table1(customer_id,city) VALUES('163','hangzhou');
     INSERT INTO table1(customer_id,city) VALUES('9you','shanghai');
     INSERT INTO table1(customer_id,city) VALUES('tx','hangzhou');
     INSERT INTO table1(customer_id,city) VALUES('baidu','hangzhou');
    
     INSERT INTO table2(customer_id) VALUES('163');
     INSERT INTO table2(customer_id) VALUES('163');
     INSERT INTO table2(customer_id) VALUES('9you');
     INSERT INTO table2(customer_id) VALUES('9you');
     INSERT INTO table2(customer_id) VALUES('9you');
     INSERT INTO table2(customer_id) VALUES('tx');
     INSERT INTO table2(customer_id) VALUES(NULL);

    准备工作做完以后,table1和table2看起来应该像下面这样:

     mysql> select * from table1;
     +-------------+----------+
     | customer_id | city     |
     +-------------+----------+
     | 163         | hangzhou |
     | 9you        | shanghai |
     | baidu       | hangzhou |
     | tx          | hangzhou |
     +-------------+----------+
     4 rows in set (0.00 sec)
    
     mysql> select * from table2;
     +----------+-------------+
     | order_id | customer_id |
     +----------+-------------+
     |        1 | 163         |
     |        2 | 163         |
     |        3 | 9you        |
     |        4 | 9you        |
     |        5 | 9you        |
     |        6 | tx          |
     |        7 | NULL        |
     +----------+-------------+
     7 rows in set (0.00 sec)
  4. 准备SQL逻辑查询测试语句
     SELECT a.customer_id, COUNT(b.order_id) as total_orders
     FROM table1 AS a
     LEFT JOIN table2 AS b
     ON a.customer_id = b.customer_id
     WHERE a.city = 'hangzhou'
     GROUP BY a.customer_id
     HAVING count(b.order_id) < 2
     ORDER BY total_orders DESC;

    使用上述SQL查询语句来获得来自杭州,并且订单数少于2的客户。

好吧,这些测试表和测试数据均来自《MySQL技术内幕:SQL编程》,这应该不算抄袭吧,借鉴借鉴啊。

万事俱备,只欠东风。接下来开始这篇文章最正式的部分吧。

SQL逻辑查询语句执行顺序

还记得上面给出的那一长串的SQL逻辑查询规则么?那么,到底哪个先执行,哪个后执行呢?现在,我先给出一个查询语句的执行顺序:

(7)     SELECT 
(8)     DISTINCT <select_list>
(1)     FROM <left_table>
(3)     <join_type> JOIN <right_table>
(2)     ON <join_condition>
(4)     WHERE <where_condition>
(5)     GROUP BY <group_by_list>
(6)     HAVING <having_condition>
(9)     ORDER BY <order_by_condition>
(10)    LIMIT <limit_number>

上面在每条语句的前面都标明了执行顺序号,不要问我怎么知道这个顺序的。我也是读各种“武林秘籍”才得知的,如果你有功夫,去阅读一下MySQL的源码,也会得出这个结果的。

好了,上面我标出了各条查询规则的执行先后顺序,那么各条查询语句是如何执行的呢?这就是我今天这篇博文的重点内容。Go on…

执行FROM语句

在这些SQL语句的执行过程中,都会产生一个虚拟表,用来保存SQL语句的执行结果(这是重点),我现在就来跟踪这个虚拟表的变化,得到最终的查询结果的过程,来分析整个SQL逻辑查询的执行顺序和过程。

第一步,执行FROM语句。我们首先需要知道最开始从哪个表开始的,这就是FROM告诉我们的。现在有了<left_table><right_table>两个表,我们到底从哪个表开始,还是从两个表进行某种联系以后再开始呢?它们之间如何产生联系呢?——笛卡尔积

关于什么是笛卡尔积,请自行Google补脑。经过FROM语句对两个表执行笛卡尔积,会得到一个虚拟表,暂且叫VT1(vitual table 1),内容如下:

+-------------+----------+----------+-------------+
| customer_id | city     | order_id | customer_id |
+-------------+----------+----------+-------------+
| 163         | hangzhou |        1 | 163         |
| 9you        | shanghai |        1 | 163         |
| baidu       | hangzhou |        1 | 163         |
| tx          | hangzhou |        1 | 163         |
| 163         | hangzhou |        2 | 163         |
| 9you        | shanghai |        2 | 163         |
| baidu       | hangzhou |        2 | 163         |
| tx          | hangzhou |        2 | 163         |
| 163         | hangzhou |        3 | 9you        |
| 9you        | shanghai |        3 | 9you        |
| baidu       | hangzhou |        3 | 9you        |
| tx          | hangzhou |        3 | 9you        |
| 163         | hangzhou |        4 | 9you        |
| 9you        | shanghai |        4 | 9you        |
| baidu       | hangzhou |        4 | 9you        |
| tx          | hangzhou |        4 | 9you        |
| 163         | hangzhou |        5 | 9you        |
| 9you        | shanghai |        5 | 9you        |
| baidu       | hangzhou |        5 | 9you        |
| tx          | hangzhou |        5 | 9you        |
| 163         | hangzhou |        6 | tx          |
| 9you        | shanghai |        6 | tx          |
| baidu       | hangzhou |        6 | tx          |
| tx          | hangzhou |        6 | tx          |
| 163         | hangzhou |        7 | NULL        |
| 9you        | shanghai |        7 | NULL        |
| baidu       | hangzhou |        7 | NULL        |
| tx          | hangzhou |        7 | NULL        |
+-------------+----------+----------+-------------+

总共有28(table1的记录条数 * table2的记录条数)条记录。这就是VT1的结果,接下来的操作就在VT1的基础上进行。

执行ON过滤

执行完笛卡尔积以后,接着就进行ON a.customer_id = b.customer_id条件过滤,根据ON中指定的条件,去掉那些不符合条件的数据,得到VT2表,内容如下:

+-------------+----------+----------+-------------+
| customer_id | city     | order_id | customer_id |
+-------------+----------+----------+-------------+
| 163         | hangzhou |        1 | 163         |
| 163         | hangzhou |        2 | 163         |
| 9you        | shanghai |        3 | 9you        |
| 9you        | shanghai |        4 | 9you        |
| 9you        | shanghai |        5 | 9you        |
| tx          | hangzhou |        6 | tx          |
+-------------+----------+----------+-------------+

VT2就是经过ON条件筛选以后得到的有用数据,而接下来的操作将在VT2的基础上继续进行。

添加外部行

这一步只有在连接类型为OUTER JOIN时才发生,如LEFT OUTER JOINRIGHT OUTER JOINFULL OUTER JOIN。在大多数的时候,我们都是会省略掉OUTER关键字的,但OUTER表示的就是外部行的概念。

LEFT OUTER JOIN把左表记为保留表,得到的结果为:

+-------------+----------+----------+-------------+
| customer_id | city     | order_id | customer_id |
+-------------+----------+----------+-------------+
| 163         | hangzhou |        1 | 163         |
| 163         | hangzhou |        2 | 163         |
| 9you        | shanghai |        3 | 9you        |
| 9you        | shanghai |        4 | 9you        |
| 9you        | shanghai |        5 | 9you        |
| tx          | hangzhou |        6 | tx          |
| baidu       | hangzhou |     NULL | NULL        |
+-------------+----------+----------+-------------+

RIGHT OUTER JOIN把右表记为保留表,得到的结果为:

+-------------+----------+----------+-------------+
| customer_id | city     | order_id | customer_id |
+-------------+----------+----------+-------------+
| 163         | hangzhou |        1 | 163         |
| 163         | hangzhou |        2 | 163         |
| 9you        | shanghai |        3 | 9you        |
| 9you        | shanghai |        4 | 9you        |
| 9you        | shanghai |        5 | 9you        |
| tx          | hangzhou |        6 | tx          |
| NULL        | NULL     |        7 | NULL        |
+-------------+----------+----------+-------------+

FULL OUTER JOIN把左右表都作为保留表,得到的结果为:

+-------------+----------+----------+-------------+
| customer_id | city     | order_id | customer_id |
+-------------+----------+----------+-------------+
| 163         | hangzhou |        1 | 163         |
| 163         | hangzhou |        2 | 163         |
| 9you        | shanghai |        3 | 9you        |
| 9you        | shanghai |        4 | 9you        |
| 9you        | shanghai |        5 | 9you        |
| tx          | hangzhou |        6 | tx          |
| baidu       | hangzhou |     NULL | NULL        |
| NULL        | NULL     |        7 | NULL        |
+-------------+----------+----------+-------------+

添加外部行的工作就是在VT2表的基础上添加保留表中被过滤条件过滤掉的数据,非保留表中的数据被赋予NULL值,最后生成虚拟表VT3。

由于我在准备的测试SQL查询逻辑语句中使用的是LEFT JOIN,过滤掉了以下这条数据:

| baidu       | hangzhou |     NULL | NULL        |

现在就把这条数据添加到VT2表中,得到的VT3表如下:

+-------------+----------+----------+-------------+
| customer_id | city     | order_id | customer_id |
+-------------+----------+----------+-------------+
| 163         | hangzhou |        1 | 163         |
| 163         | hangzhou |        2 | 163         |
| 9you        | shanghai |        3 | 9you        |
| 9you        | shanghai |        4 | 9you        |
| 9you        | shanghai |        5 | 9you        |
| tx          | hangzhou |        6 | tx          |
| baidu       | hangzhou |     NULL | NULL        |
+-------------+----------+----------+-------------+

接下来的操作都会在该VT3表上进行。

执行WHERE过滤

对添加外部行得到的VT3进行WHERE过滤,只有符合<where_condition>的记录才会输出到虚拟表VT4中。当我们执行WHERE a.city = 'hangzhou'的时候,就会得到以下内容,并存在虚拟表VT4中:

+-------------+----------+----------+-------------+
| customer_id | city     | order_id | customer_id |
+-------------+----------+----------+-------------+
| 163         | hangzhou |        1 | 163         |
| 163         | hangzhou |        2 | 163         |
| tx          | hangzhou |        6 | tx          |
| baidu       | hangzhou |     NULL | NULL        |
+-------------+----------+----------+-------------+

但是在使用WHERE子句时,需要注意以下两点:

  1. 由于数据还没有分组,因此现在还不能在WHERE过滤器中使用where_condition=MIN(col)这类对分组统计的过滤;
  2. 由于还没有进行列的选取操作,因此在SELECT中使用列的别名也是不被允许的,如:SELECT city as c FROM t WHERE c='shanghai';是不允许出现的。

执行GROUP BY分组

GROU BY子句主要是对使用WHERE子句得到的虚拟表进行分组操作。我们执行测试语句中的GROUP BY a.customer_id,就会得到以下内容:

+-------------+----------+----------+-------------+
| customer_id | city     | order_id | customer_id |
+-------------+----------+----------+-------------+
| 163         | hangzhou |        1 | 163         |
| baidu       | hangzhou |     NULL | NULL        |
| tx          | hangzhou |        6 | tx          |
+-------------+----------+----------+-------------+

得到的内容会存入虚拟表VT5中,此时,我们就得到了一个VT5虚拟表,接下来的操作都会在该表上完成。

执行HAVING过滤

HAVING子句主要和GROUP BY子句配合使用,对分组得到的VT5虚拟表进行条件过滤。当我执行测试语句中的HAVING count(b.order_id) < 2时,将得到以下内容:

+-------------+----------+----------+-------------+
| customer_id | city     | order_id | customer_id |
+-------------+----------+----------+-------------+
| baidu       | hangzhou |     NULL | NULL        |
| tx          | hangzhou |        6 | tx          |
+-------------+----------+----------+-------------+

这就是虚拟表VT6。

SELECT列表

现在才会执行到SELECT子句,不要以为SELECT子句被写在第一行,就是第一个被执行的。

我们执行测试语句中的SELECT a.customer_id, COUNT(b.order_id) as total_orders,从虚拟表VT6中选择出我们需要的内容。我们将得到以下内容:

+-------------+--------------+
| customer_id | total_orders |
+-------------+--------------+
| baidu       |            0 |
| tx          |            1 |
+-------------+--------------+

不,还没有完,这只是虚拟表VT7。

执行DISTINCT子句

如果在查询中指定了DISTINCT子句,则会创建一张内存临时表(如果内存放不下,就需要存放在硬盘了)。这张临时表的表结构和上一步产生的虚拟表VT7是一样的,不同的是对进行DISTINCT操作的列增加了一个唯一索引,以此来除重复数据。

由于我的测试SQL语句中并没有使用DISTINCT,所以,在该查询中,这一步不会生成一个虚拟表。

执行ORDER BY子句

对虚拟表中的内容按照指定的列进行排序,然后返回一个新的虚拟表,我们执行测试SQL语句中的ORDER BY total_orders DESC,就会得到以下内容:

+-------------+--------------+
| customer_id | total_orders |
+-------------+--------------+
| tx          |            1 |
| baidu       |            0 |
+-------------+--------------+

可以看到这是对total_orders列进行降序排列的。上述结果会存储在VT8中。

执行LIMIT子句

LIMIT子句从上一步得到的VT8虚拟表中选出从指定位置开始的指定行数据。对于没有应用ORDER BY的LIMIT子句,得到的结果同样是无序的,所以,很多时候,我们都会看到LIMIT子句会和ORDER BY子句一起使用。

MySQL数据库的LIMIT支持如下形式的选择:

LIMIT n, m

表示从第n条记录开始选择m条记录。而很多开发人员喜欢使用该语句来解决分页问题。对于小数据,使用LIMIT子句没有任何问题,当数据量非常大的时候,使用LIMIT n, m是非常低效的。因为LIMIT的机制是每次都是从头开始扫描,如果需要从第60万行开始,读取3条数据,就需要先扫描定位到60万行,然后再进行读取,而扫描的过程是一个非常低效的过程。所以,对于大数据处理时,是非常有必要在应用层建立一定的缓存机制(貌似现在的大数据处理,都有缓存哦)。各位,请期待我的缓存方面的文章哦。

总结

文章略长,但都是干货。仔细阅读完,肯定有收获的。好歹是总结完了,个人认为还是比书上的内容清晰一点,好懂一点。如果觉的文章不错,对你有帮助,你也可以打赏我。你也可以加果冻想的微信公众号,期待你与我交流。

2015年1月17日 于深圳。

===修改日志===

2015年8月26日 补上了SQL语句的执行顺序中的第六步。

打赏
分享到:更多 ()

评论 46

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
  1. #0

    非常好的一片sql执行流程文章, 赞~

    _一心一亿2年前 (2015-05-13)回复
  2. #0

    仔细看了,觉得很不错,不过博主大人,有一个问题我真的想请教您一下,那个sql语句的执行顺序你是怎么知道的呀,最近我们要做一个能够输出执行顺序的工具,难道真的只有读完那些优化器部分的代码吗?望不吝赐教

    张骏川2年前 (2015-07-22)回复
    • 我有是在读《MySQL技术内幕:SQL编程》这本书时,总结的。对于你说的,需要确切的知道SQL的逻辑执行顺序,我想最好的办法还是去读源代码,目前能够把SQL的执行顺序完全写透的书,还没有看到,也希望你能和我分享你最终的结果。

      果冻想2年前 (2015-07-23)回复
  3. #0

    写的挺好,可是顺序6没有了

    remember2年前 (2015-08-26)回复
    • 恩,之前我重新阅读时发现了这个问题,准备下班回去改,后来也忘了。今天你提出来了,直接改掉了。谢谢你的指点。你看的好仔细的啊。哈哈[嘻嘻]

      果冻想2年前 (2015-08-26)回复
  4. #0

    SELECT sid,AVG(score) AS score_avg FROM sc GROUP BY sid HAVING score_avg > 60
    经试验,这条语句是没问题的,但是别名在Having子句中是可以使用的。。。为什么??

    pipi2年前 (2015-09-16)回复
    • 的确是这样的,这条SQL语句的确可以运行。由于MySQL的查询解释器会在执行时进行各种优化,所以有些东西在这篇文章中并没有总结出来,而对于MySQL具体的执行顺序,也很少有资料能够说的清。我这里也是边学习,边总结。

      果冻想2年前 (2015-09-16)回复
  5. #0

    谢谢你这篇好的文章,在这里 http://www.jellythink.com/archives/924#-group-by- 进行GROUP BY的时候虚拟表中并没有分组的信息,那执行聚合函数的时候又是如何知道每个组中的元素的个数呢?谢谢!

    农药2年前 (2015-11-08)回复
    • 不好意思回复晚了,可能在临时表中。

      果冻想2年前 (2015-11-10)回复
  6. #0

    楼主,请教个问题.没什么group by 后order_id显示是1 而不是 2?

    李刚2年前 (2015-12-04)回复
  7. #0

    群主大人,我想问问 ,怎么才能跟踪虚拟表那 ????、

    11758171752年前 (2016-04-15)回复
  8. #0
  9. #0

    按照这个说法 只要有两个表的join操作 第一步的from那里, 笛卡尔积 是无论如何避免不了的了?

    nara0071年前 (2016-05-18)回复
    • 应该是避免不了的。

      果冻想1年前 (2016-05-18)回复
      • 一位学长说:不会直接做笛卡尔积,会先根据 on 子句条件,进行条件查找,直接生成连接表;而不会,先生成笛卡尔积表,再进行筛选。

        ForrestLyu1个月前 (09-06)回复
        • 不做笛卡尔乘积,怎么筛选哦?

          果冻想1个月前 (09-09)回复
  10. #0

    感谢楼主的这篇文章,不过还有个地方个人不太明白。
    执行完GROUP BY之后的记录中 并没有重复的记录了
    然后楼主在此基础上进行HAVING count(b.order_id) < 2 的过滤 得出结果 这里我个人不太理解

    离_火1年前 (2016-07-10)回复
    • 恩,group by是分组的概念,分组以后,再使用having对组里元素进行筛选。

      果冻想1年前 (2016-07-15)回复
  11. #0

    full outer join mysql 是不支持的吧

    db新兵1年前 (2016-08-01)回复
    • 恩,不支持,但是可以使用left join和right join加union模拟。

      果冻想1年前 (2016-08-02)回复
    • 测试过,是不支持

      zouzls7个月前 (03-22)回复
  12. #0

    HAVING () 如果是对 vt5 进行过滤 难道不是 有三条记录 吗 但结果是只有两条 是不是对 vt4 进行过滤 或者 从另个角度去理解 mysql 是按照您这样说的 一步一步 执行的吗 或者是不是 将两个组合在一起进行 执行了

    db新兵1年前 (2016-08-01)回复
    • 对VT5过滤,怎么会得到三条记录?

      果冻想1年前 (2016-08-02)回复
  13. #0

    挺棒的一篇文章,因为没有博览群书,只是从看的舒适度和是否容易理解的角度评价

    nice~nice1年前 (2016-08-12)回复
  14. #0

    楼主请教个问题,在VT4用group by获取VT5表的时候,为什么把order_id=2的163过滤了,这个地方为什么过滤的不是order_id=1的163呢?是因为163在原始表table2中的排列顺序是order_id=1的纪录在前导致的么?这里还有一个问题如果having的条件是count(b.order_id)<3的条件的话,按照上面的操作顺序,从VT4到VT5的时候会漏掉order_id=2的163的这一条记录吧?然后执行HAVING的时候就漏掉了这一条记录吧?求解答一些,谢谢!

    zhangxiaoyuan1年前 (2016-09-16)回复
    • 是会漏掉order_id=2这条记录的。当按照某个字段进行group by分组时,数据库会按照某种规则对冲突的字段进行丢失的。

      果冻想1年前 (2016-09-16)回复
      • 哦哦,那就可以理解了,谢谢

        zhangxiaoyuan1年前 (2016-09-16)回复
      • 在VT5中没有order_id=2;但是不代表丢失吧
        如果用having count(b.order_id)<3;在执行SELECT a.customer_id, COUNT(b.order_id) 会出现
        customer_id | total_order
        ————-+————-
        163 | 2
        baidu | 0
        tx | 1

        libaitian1年前 (2016-10-17)回复
  15. #0

    楼主,我想问下看执行计划的时候经常会看到left_table会根据where条件使用到索引,这算不算先把left_table 过滤了一遍呀?而不是from就笛卡尔积两个表

    hustmsn10个月前 (12-18)回复
  16. #0

    SELECT a.customer_id cid, COUNT(b.order_id) as total_orders
    FROM table1 AS a
    LEFT JOIN table2 AS b
    ON a.customer_id = b.customer_id
    WHERE a.city = ‘hangzhou’
    GROUP BY cid
    HAVING count(b.order_id) < 2
    ORDER BY total_orders DESC;

    我定义的这个cid,在select中定义的,在group by 中可以使用,这样是不是应该说明select比group by 先执行?

    Owen Zou10个月前 (12-26)回复
    • 这里很有争议,整个执行过程,顺序都有争议,每个版本的都不一样,具体的需要阅读MySQL源码。

      果冻想9个月前 (01-06)回复
  17. #0

    楼主的文章很不错,我记得group by 和having是可以使用select中的别名的,例如select from_unixtime(create_time, ‘%Y-%m-%d’) as t, count(1) as num from t_table group by t having num > 2。但是按照楼主的说法,在执行group by 和having时,select并没有执行,这和别名矛盾,楼主该如何解释?谢谢。是不是mysql在执行前,将sql做了优化?将别名替换成了实际内容?若是如此的话,where里面应该也要能使用别名?

    李飞8个月前 (02-18)回复
  18. #0

    题主写的很用心啊,由浅入深,真好,想问一下表格用的什么编辑器啊,看起来真漂亮

    dsa5个月前 (05-05)回复
  19. #0

    如果多表连接,按道理应该是重复步骤1-3吗?为什么我用 explain sql 查看到的执行顺序和这个不符呢,是mysql自动优化的原因吗?

    Dulk2个月前 (08-18)回复
    • 有。每个版本实现逻辑都不一样。

      果冻想2个月前 (08-22)回复
  20. #0

    您好,

    jinsha1个月前 (09-10)回复
  21. #0

    是这样的,VT5通过HAVING count(b.order_id) < 2 得到VT6 应该只有order_id为1的记录 为什么反而没有 求教

    jinsha1个月前 (09-10)回复
  22. #0

    形象生动。😁

    fanhaobai1个月前 (09-16)回复

在这里玩技术,享受技术带来的疯狂

捐赠名单关于果冻