详细分析SQL语句逻辑执行过程和相关语法

Posted 骏马金龙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详细分析SQL语句逻辑执行过程和相关语法相关的知识,希望对你有一定的参考价值。

1.1 SQL语句的逻辑处理顺序

SQL语句的逻辑处理顺序,指的是SQL语句按照一定的规则,一整条语句应该如何执行,每一个关键字、子句部分在什么时刻执行。

除了逻辑顺序,还有物理执行顺序。物理顺序是SQL语句真正被执行时的顺序(执行计划),它是由各数据库系统的关系引擎中的语句分析器、优化器等等组件经过大量计算、分析决定的。

很多时候因为优化的关系,使得语句最终的物理执行顺序和逻辑顺序不同。按照逻辑顺序,有些应该先执行的过程,可能优化器会指定它后执行。但不管是逻辑顺序还是物理顺序,设计了一条SQL语句,语句最后返回的结果不会也不能因为物理顺序改变了逻辑顺序而改变。

其实,逻辑顺序只是为我们编写、理解SQL语句提供些帮助,除此之外,它毫无用处。而且,是不是真的存在一条严格且完整的执行顺序规则都是不确定的事情。虽然某些书上、网上给出了一些顺序(我个人所知道的比较权威的,是SQL Server的"圣书"技术内幕里介绍过),但在任何一种数据库系统的官方手册上都没有关于这方面的介绍文档。

SQL Server和Oracle在语句的逻辑处理顺序上是一致的,在这方面,它们严格遵守了标准SQL的要求,任何一个步骤都遵循了关系型数据库的范式要求。因为遵循了一些范式要求,导致标准SQL不允许使用某些语法。但是mysql、MariaDB和它们小有不同,它们对标准SQL进行扩展,标准SQL中不允许使用的语法,在MySQL、MariaDB中可能可以使用,但很多时候这会违反关系模型的范式要求。

虽然本文的最初目的是介绍MariaDB/MySQL语句的逻辑处理顺序,但在篇幅上对标准SQL介绍的更多,因为它们符合规范。理解这些规范,实际上是在理解关系模型和集合模型。本文也在多处通过这两个模型来分析为什么标准SQL不允许某些语法,以及为什么MySQL可以支持这些"不标准"的语法

1.2 各数据库系统的语句逻辑处理顺序

以SELECT语句为例。

1.2.1 SQL Server和Oracle的逻辑执行顺序

如下图:

关于本图需要说明的是,虽然图中给出的顺序是DISTINCT比ORDER BY先执行,这也是网上流传的版本。但其实,在DISTINCT和ORDER BY之间的顺序没有严格的界限,甚至ORDER BY的顺序要优先于DISTINCT后文会分析为什么。而且刚刚去翻了下sql server技术内幕中关于逻辑处理顺序的内容,发现它没有对DISTINCT执行位置进行排序,只是在介绍ORDER BY时提了下DISTINCT,我想也是因为DISTINCT和ORDER BY之间没有严格的顺序。

后面关于MySQL和mariadb的两张逻辑顺序图中,将会把DISTINCT和ORDER BY的顺序调换过来。

以下是对上述逻辑执行顺序的描述:

(1).首先从FROM语句中获取要操作的表并计算笛卡尔积。如果有要联接的表,则还获取联接表。对它们计算笛卡尔积,笛卡尔积的结果形成一张虚拟表vt1。

这里就体现了物理顺序和逻辑顺序的一个不同点:按照逻辑顺序,在执行SQL语句之初总是会进行笛卡尔积的计算,如果是两张或多张非常大的表,计算笛卡尔积是非常低效的行为,这是不能容忍的。所以物理顺序会进行一些优化决定,比如使用索引跳过一部分或整个笛卡尔积让计算变得很小。

(2).对虚拟表vt1执行ON筛选语句,得到虚拟表vt2。

(3).根据联接类型,将保留表的外部行添加到vt2中得到虚拟表vt3。

(4).对vt3执行where条件筛选,得到虚拟表vt4。

(5).执行分组,得到虚拟表vt5。

注意,分组之后,整个SQL的操作上下文就变成了分组列,而不再是表中的每一列,后续的一切操作都是围绕所分的组作为操作对象进行操作的。也就是说,不在分组列表中的列不能在后续步骤中使用。例如,使用"group by a"对a列分组,那么后续的select列表中就不能使用b列,除非是对b列进行分组聚合运算。SQL Server、Oracle和MariaDB、Mysql最大的区别就在于此步,后两者可以引用分组列以外的列。

(6).对vt5执行集合操作cube或者rollup,得到虚拟表vt6。

(7).对分组的最终结果vt6执行having筛选,得到虚拟表vt7。

(8).根据给定的选择列列表,将vt7中的选择列插入到虚拟表vt8中。

注意,选择列是"同时性操作",在选择列中不能使用列别名来引用列表中的其他列。例如 select col1+1 as a,a+1 as b from t1 是错误的,因为"col1+1"和"a+1"之间没有执行上的先后顺序,所以它认为"a+1"中的a列是不存在的。

(9).对vt8进行窗口分组相关的计算,得到虚拟表vt9。

(10).对vt9按照指定的列去除重复行,得到虚拟表vt10。

这一步是将数据复制到内存中相同的临时表结构中进行的,不过该临时表多出了一个唯一性索引列用来做重复消除。

(11).对vt10进行排序,排序后的表为虚拟表vt11。

(12).从vt11中根据top条件挑出其中满足的行,得到虚拟表vt12。

如果没有应用order by,则记录是无序的集合,top挑出的行可能是随机的。也因此top一般要和order by字句一起使用。

(13).将vt12从服务端返回给客户端作为最终查询结果。

1.2.2 MariaDB的逻辑执行顺序

如下图:

MariaDB中,使用的是LIMIT子句实现和TOP子句一样的功能:限制输出行数。且它不支持"WITH CUBE"(直接忽略该关键词)。

但和SQL Server、Oracle最大的不同是对SELECT列表的处理。在MS SQL和Oracle中,select_list是在group by和having子句之后才进行的,这意味着group by分组后,不能在select_list中指定非分组列(除非聚合运算),反过来看,在group by中不能使用select_list中的别名列。

但在MariaDB和MySQL中,select_list是在group by之前进行的。在group by中能够引用select_list中的列,在select_list中也能指定非分组列。

mariadb和mysql在这一点上实际上是"不规范"的,因为它违背了数据库的设计范式。详细内容在后文分析。

1.2.3 MySQL的逻辑执行顺序

如下图:

和MariaDB之间并没有什么区别,仅仅只是MySQL不支持开窗函数over()。

但是注意,从mysql 5.7.5开始,已经默认设置了sql_mode=ONLY_FULL_GROUP_BY,这意味着MySQL默认也将遵循SQL规范,对于那些非分组列又没有进行聚合的列,都不允许出现在select_list中,除非select_list中使用的列是主键或者唯一索引列,之所以允许这样的行为,是因为有功能依赖性决定了它可以这样做,由此保证"规范性"。同样的,为何不规范的问题见后文。

1.3 关于表表达式和虚拟表

派生表、CTE(公用表表达式,有的数据库系统支持)、视图和表函数都是表,我们常称之为"表表达式",只不过它们是虚拟表(这里的虚拟表和上面逻辑执行过程中产生的虚拟表vt不是同一个概念)。它们都必须满足成为表的条件,这也是为什么定义表表达式的时候有些语法不能使用。

从关系模型上去分析。表对应的是关系模型中的关系,表中的列对应的是关系模型中的元素。

一方面,关系和元素都需要有唯一标识的名称,因此表和列也要有名称,即使表表达式也如此。像派生表是嵌套在语句中的,无法在外部给它指定表明,因此必须为它指定一个表别名。同理,表表达式中的别名也一样,必须唯一且必须要有。

另一方面,关系中的元素是无序的,因此表和表表达式中的数据也应当是无序的。虽然有些表表达式中可以使用ORDER BY子句,但这时候的ORDER BY只是为了让TOP/LIMIT子句来挑选指定数量的行,并不是真的会对结果排序。也就是说表表达式挑选出来的行就像表一样,其内数据行仍然是无序的,以后访问它们的时候是按照物理存储顺序进行访问的,即使表表达式的定义语句中使用了ORDER BY子句。

关于数据的无序性和随机性,见下文。

这里还请区分表表达式(虚拟表)和逻辑执行过程中我们想象出来的虚拟表。表表达式是实实在在符合关系模型的表,即使它可能只是一条或几条语句,也不会将相关数据行进行物理的存储,但在关系引擎看来,它就是表。而逻辑执行过程中我们想象出来的虚拟表,只是为了方便理解而描述出来的,实际上不会有这样的表,它们只是按一定规则存放在内存中的一些数据行,虽然某些步骤中可能也会使用系统自建的临时表存放中途的数据,但它们不是表。

1.4 关于表别名和列别名

在SQL语句中,我们避免不了要对表、列使用别名进行引用。关于别名,需要注意两点:

(1).定义了表别名后,在语句中对该表的引用都必须使用别名,而不能使用原表名。

(2).引用别名时,注意查询的逻辑处理过程。在某一阶段只能引用该阶段前面阶段定义的别名,使用该阶段后才定义的别名将报错。

例如下面的两个查询语句,第一个错误原因是不能引用原表名,第二个错误是因为WHERE阶段不能引用SELECT阶段定义的字段别名。

SELECT Student.Name FROM Student AS 学生表
SELECT Name,Sex AS 性别 FROM Student WHERE 性别 = \'\'

下面是正确的写法。

SELECT 学生表.Name FROM Student AS 学生表
SELECT Name,Sex AS性别 FROM Student WHERE Sex = \'\'

1.5 关于数据无序性和ORDER BY

在关系型数据库中,必须时刻都铭记在心的是"集合元素是无序"的,体现在数据库中就是"表中数据行是无序的",除非建立了相关索引。

出于集合模型的考虑,像我们平时看到的有行、有列的二维表数据(下图左边),更应该看作是下图右边的结合结构,因为集合是无序的。

由于数据无序,导致检索数据时都是按照存储时的物理顺序进行访问,如此检索得到的数据行都是随机而不保证任何顺序的,除非指定了ORDER BY子句。而使用ORDER BY查询得到的结果,它因为有序而不满足集合的概念。实际上ORDER BY生成的是一个游标结果。了解SQL的人,都知道能不用游标就尽量不用游标,因为它的效率相比符合集合概念的SQL语句来说,要慢很多个数量级。但也不能一棍子将其打死,因为有时候使用游标确实能比较容易达到查询目标。

SQL中没有使用ORDER BY时,有不少子句的返回结果(虚拟表)都是随机的,因为实在没办法去保证顺序,但却又要求返回数据。例如直接进行 SELECT * from t; ,再例如TOP/LIMIT子句。

纵观整个SQL的各个环节,不难发现很多时候获取随机行数据是不应该的,因为这种不确定性,让我们操作数据时显得更出乎意料、更危险因此,除非不得不显示随机数据,标准SQL都会通过一些手段让获取随机数据的行为失败,而且在可能获取随机数据的时候,一般都会给出相关的建议和提示。

MySQL、mariadb之所以和sql server、oracle的语法相差那么大,归根结底就是对待关系型数据库的范式要求和随机数据的态度不同。MySQL、mariadb总是"偷奸耍滑",在本无法满足关系型数据库范式的时候,它们总是挑选一个随机单行数据出来,让返回结果满足范式要求,最典型的就是group by的处理方式不过MySQL从5.7.5版本开始,已经逐渐走向规范化了。

这里并非是要否认mysql、mariadb的设计模式,正所谓每个数据库系统都有自己对标准SQL的扩展方式,MySQL只是走了一条和标准SQL不同的路而已。而且关系模型的范式本就是人为定义的,为何不能违反呢?甚至可以说,表所满足的范式越强,检索表时的性能越低,nosql就没有关系模型的范式要求。

在后文,将在多处分析标准SQL为什么不允许某些语法,同时还会提到MySQL和mariadb是如何"偷奸耍滑"的。

1.6 关于TOP(或LIMIT)和ORDER BY

TOP和LIMIT是限制输出行数量,它们挑选数据行时是随机的(根据物理访问顺序),所以得到的结果也是随机的。因此,建议TOP/LIMIT和ORDER BY一起使用。但即使如此,仍是不安全的。例如,ORDER BY的列中有重复值,那么TOP/LIMIT的时候如何决定获取哪些行呢?见如下LIMIT的示例(TOP也一样):

MariaDB [test]> select * from Student order by age;
+------+----------+------+--------+
| sid  | name     | age  | class  |
+------+----------+------+--------+
|    6 | zhaoliu  |   19 | Java   |
|    4 | lisi     |   20 | C#     |
|    8 | sunba    |   20 | C++    |
|    3 | zhangsan |   21 | Java   |
|    5 | wangwu   |   21 | Python |
|    1 | chenyi   |   22 | Java   |
|    7 | qianqi   |   22 | C      |
|    2 | huanger  |   23 | Python |
|    9 | yangjiu  |   24 | Java   |
+------+----------+------+--------+
 
MariaDB [test]> select * from Student order by age limit 9;
+------+----------+------+--------+
| sid  | name     | age  | class  |
+------+----------+------+--------+
|    6 | zhaoliu  |   19 | Java   |
|    4 | lisi     |   20 | C#     |
|    8 | sunba    |   20 | C++    |
|    3 | zhangsan |   21 | Java   |
|    5 | wangwu   |   21 | Python |
|    1 | chenyi   |   22 | Java   |
|    7 | qianqi   |   22 | C      |
|    2 | huanger  |   23 | Python |
|    9 | yangjiu  |   24 | Java   |
+------+----------+------+--------+

从两次查询结果中看到,即使都是对age进行升序排列,但age=20的两行前后顺序不一致,age=22的行顺序也不一致。

因此一般会给另一个建议,为了确保数据一定是符合预期的,在order by中应该再加一列(最好具有唯一性)作为决胜属性,例如对age排序后再按照sid排序,这样就能保证返回结果不是随机的。

select * from Student order by age,sid;
select * from Student order by age,sid limit 9;

1.7 关于DISTINCT和GROUP BY

DISTINCT子句用于消除select_list列的重复行,这很容易理解。大多数情况下,DISTINCT子句在功能上都可以认为等价于group by子句。有些DISTINCT不适合做的操作,可以在GROUP BY中来完成。

例如下面两个SQL语句是等价的:

select distinct class,age from Student; 
select class,age from Student group by class,age;

正因为等价,很多时候对DISTINCT的优化行为总是和GROUP BY的优化行为一致。以下是sql server上对上述两条语句的执行计划:

select distinct class,age from Student;
  |--Sort(DISTINCT ORDER BY:([test].[dbo].[Student].[class] ASC, [test].[dbo].[Student].[age] ASC))
       |--Table Scan(OBJECT:([test].[dbo].[Student]))
 select class,age from Student group by class,age;
  |--Sort(DISTINCT ORDER BY:([test].[dbo].[Student].[class] ASC, [test].[dbo].[Student].[age] ASC))
       |--Table Scan(OBJECT:([test].[dbo].[Student]))

从结果中看到,执行DISTINCT去除重复行时,默认就带有了排序过程。实际上,DISTINCT几乎总是会将数据复制到内存中的一张临时表中进行,该临时表的结构和前面得到的虚拟表字段结构几乎一致,但却多了一个唯一性索引列用来做重复消除。

但如果DISTINCT结合GROUP BY子句呢?其实不建议这么做。这里也不讨论这种问题。

1.8 关于DISTINCT和ORDER BY

既然DISTINCT默认就带了排序行为,那此时再指定ORDER BY会如何?例如下面的语句:

select distinct class,age from Student ORDER BY age desc; 

在SQL Server中的执行计划如下:

select distinct class,age from Student ORDER BY age desc;
  |--Sort(DISTINCT ORDER BY:([test].[dbo].[Student].[age] DESC, [test].[dbo].[Student].[class] ASC))
       |--Table Scan(OBJECT:([test].[dbo].[Student]))

其实和前面没什么区别,无非是先对order by列进行排序而已。但是从这里能看出,DISTINCT和ORDER BY字句其实没有严格的逻辑执行先后顺序,甚至ORDER BY指定的排序列还优先于DISTINCT的排序行为。

但是,DISTINCT和ORDER BY结合时,order by的排序列是有要求的:排序列必须是select_list中的列(distinct很多时候都可以看作group by)。例如 select distinct a,b from t order by c; 是错误的。但MySQL和mariadb又在这里进行了扩展,它们的排序列允许非select_list中的列。

先说标准SQL为何不允许使用非select_list中的列,这归根结底还是关系型数据库的范式问题。假如DISTINCT消除了部分列的重复值,最终将只返回一条重复记录,而如果使用非select_list的列排序,将要求返回一条重复记录的同时还要返回每个重复值对应的多条记录以便排序,而在要求范式的关系表中是无法整合这样的结果。

例如表中数据如下:

MariaDB [test]> select sid,age,class from Student order by class;
+------+------+--------+
| sid  | age  | class  |
+------+------+--------+
|    7 |   22 | C      |
|    4 |   20 | C#     |
|    8 |   20 | C++    |
|    1 |   22 | Java   |
|    3 |   21 | Java   |
|    6 |   19 | Java   |
|    9 |   24 | Java   |
|    2 |   23 | Python |
|    5 |   21 | Python |
+------+------+--------+

现在对class列进行去重。

MariaDB [test]> select distinct class from Student order by class;
+--------+
| class  |
+--------+
| C      |
| C#     |
| C++    |
| Java   |
| Python |
+--------+

现在假设order by的排序列能使用sid进行排序。那么期待的结果将是根据如下数据进行返回的:

select distinct class from Student order by sid;
+------+--------+
| sid  | class  |
+------+--------+
|    7 | C      |
+---------------+
|    4 | C#     |
+---------------+
|    8 | C++    |
+---------------+
|    1 |        |
|    3 | Java   |
|    6 |        |
|    9 |        |
+---------------+
|    2 |        |
|    5 | Python |
+------+--------+ 

这样的结构已经违反了关系型数据库的范式要求。因此,sql server和oracle会直接对该语句报错。

但是MySQL/mariadb就允许在order by中使用非select_list列进行排序。它们是如何"偷奸耍滑"的呢?还是上面违反关系模型范式的数据结构,MySQL和mariadb会从Java和Python对应的sid中挑选第一行(order by已经对其排序,因此不是随机数据),然后和Java、Python分别组成一行,得到如下虚拟表:

+------+--------+
| sid  | class  |
+------+--------+
|    7 | C      |
+---------------+
|    4 | C#     |
+---------------+
|    8 | C++    |
+---------------+
|    1 | Java   |
+---------------+
|    2 | Python |
+------+--------+ 

然后将此虚拟表中非select_list中的列都去掉,得到最终结果。真的是最终结果吗?

MariaDB [test]> select distinct class from Student order by sid; 
+--------+
| class  |
+--------+
| Java   |
| Python |
| C#     |
| C      |
| C++    |
+--------+

虽然返回的结果内容上和前面分析的一致,但是顺序却不一致,影响因素就是"order by sid"。

其实认真观察结果,很容易就发现它们是根据sid排序后再对class去重得到的结果。也就是说,ORDER BY子句比DISTINCT子句先执行了。稍稍分析一下,这里先以sid排序,得到如下虚拟结果:

+--------+------+
| class  | sid  |
+--------+------+
| Java   |    1 |
| Python |    2 |
| Java   |    3 |
| C#     |    4 |
| Python |    5 |
| Java   |    6 |
| C      |    7 |
| C++    |    8 |
| Java   |    9 |
+--------+------+

再对class去重,得到如下虚拟结果:

+------+--------+
| sid  | class  |
+---------------+
|    1 |        |
|    3 | Java   |
|    6 |        |
|    9 |        |
+------+--------+
|    2 |        |
|    5 | Python |
+------+--------+
|    4 | C#     |
+---------------+
|    7 | C      |
+---------------+
|    8 | C++    |
+---------------+

最后去掉非select_list中的列sid,得到最终结果。

1.9 关于标准SQL的GROUP BY

如果让我给SQL语句的逻辑执行顺序划分为两段式,我会将"三八线"划在GROUP BY这里。因为在GROUP BY之前甚至完全没有GROUP BY子句的语句部分,操作的对象都是表中的每行数据,也就是说操作的上下文环境是表的数据行。而在GROUP BY之后,操作的对象是组而不再是行,也就是说操作的上下文将从表中的数据行变成组。

直白一点说,GROUP BY之前,关系引擎的目光集中在数据行的细节上,GROUP BY之后,关系引擎的目光则集中在组上。至于每个分组中的行,对关系引擎来说是透明的,它不在乎组中行这种细节性的东西是否存在,而且按照关系模型的要求,也不应该认为它们存在。注意,这里说的是标准SQL,而MySQL和mariadb又"偷奸耍滑"去了。

举个例子就很容易理解GROUP BY前后侧重点的变化过程。

以下是Student表的内容。

MariaDB [test]> select * from Student;
+------+----------+------+--------+
| sid  | name     | age  | class  |
+------+----------+------+--------+
|    1 | chenyi   |   22 | Java   |
|    2 | huanger  |   23 | Python |
|    3 | zhangsan |   21 | Java   |
|    4 | lisi     |   20 | C#     |
|    5 | wangwu   |   21 | Python |
|    6 | zhaoliu  |   19 | Java   |
|    7 | qianqi   |   22 | C      |
|    8 | sunba    |   20 | C++    |
|    9 | yangjiu  |   24 | Java   |
+------+----------+------+--------+

现在按照class进行分组。下面是分组后经过我加工的表结构:

其中第一列是分组得到的结果,我把它和原表的数据结合在一起了。注意,这是一个不符合关系模型范式要求的结构。

在分组前,关

以上是关于详细分析SQL语句逻辑执行过程和相关语法的主要内容,如果未能解决你的问题,请参考以下文章

Mybaits 源码解析 ----- Select 语句的执行过程分析(下篇)(Mapper方法是如何调用到XML中的SQL的?)全网最详细,没有之一

查询语句执行的5个阶段

Spark Sql源码详细分析

Spark Sql源码详细分析

Spark Sql源码详细分析

MySql中Sql的执行过程