由于最近需要做一些sql query性能提升的研究,因此研究了一下sql语句的解决过程。在园子里看了下,大家写了很多相关的文章,大家的侧重点各有不同。本文是我在看了各种资料后手机总结的,会详细的,一步一步的讲述一个sql语句的各个关键字的解析过程,欢迎大家互相学习。
sql语句的解析顺序
简单的说一个sql语句是按照如下的顺序解析的:
1. from from后面的表标识了这条语句要查询的数据源。和一些子句如,(1-j1)笛卡尔积,(1-j2)on过滤,(1-j3)添加外部列,所要应用的对象。from过程之后会生成一个虚拟表vt1。
(1-j1)笛卡尔积 这个步骤会计算两个相关联表的笛卡尔积(cross join) ,生成虚拟表vt1-j1。
(1-j2)on过滤 这个步骤基于虚拟表vt1-j1这一个虚拟表进行过滤,过滤出所有满足on 谓词条件的列,生成虚拟表vt1-j2。
(1-j3)添加外部行 如果使用了外连接,保留表中的不符合on条件的列也会被加入到vt1-j2中,作为外部行,生成虚拟表vt1-j3。
2. where 对vt1过程中生成的临时表进行过滤,满足where子句的列被插入到vt2表中。
3. group by 这个子句会把vt2中生成的表按照group by中的列进行分组。生成vt3表。
4. having 这个子句对vt3表中的不同的组进行过滤,满足having条件的子句被加入到vt4表中。
5. select 这个子句对select子句中的元素进行处理,生成vt5表。
(5-1)计算表达式 计算select 子句中的表达式,生成vt5-1
(5-2)distinct 寻找vt5-1中的重复列,并删掉,生成vt5-2
(5-3)top 从order by子句定义的结果中,筛选出符合条件的列。生成vt5-3表
order by 从vt5-3中的表中,根据order by 子句的条件对结果进行排序,生成vc6表。
客户,订单的查询例子
首先创建一个customers表,插入如下数据:
customerid | city |
fissa | madrid |
frndo | madrid |
krlos | madrid |
mrphs | zion |
创建一个orders表,插入如下数据:
orderid | customerid |
1 | frndo |
2 | frndo |
3 | krlos |
4 | krlos |
5 | krlos |
6 | mrphs |
7 | null |
假如我们想要查询来自madrid的,订单数小于3的客户,并把他们的订单数显示出来,结果按照订单数从小到大进行排序。
复制代码 代码如下:
select c.customerid, count(o.orderid) as numorders
from dbo.customers as c
left outer join dbo.orders as o
on c.customerid = o.customerid
where c.city = ‘madrid’
group by c.customerid
having count(o.orderid) < 3
order by numorders
查询结果为:
customerid | numorders |
fissa | 0 |
frndo | 2 |
下面我们会详细的讲述sql是怎样计算出这个结果的:
from子句
from子句标识了需要查询的表,如果指定了表操作,会从左到右的处理,每一个基于一个或者两个表的表操作都会返回一个输出表。左边表的输出结果会作为下一个表操作的输入结果。例如,交表相关的操作有 (1-j1)笛卡尔积,(1-j2)on过滤器,(1-j3)添加外部列。from句子生成虚拟表vt1。
step 1-j1:执行笛卡尔积(cross join)
笛卡尔积会把左右两个表每一行所有可能的组合都列出来生成表vt1-j1,如果左表有m列,右表有n列,那么笛卡尔积之后生成的vt1-j1表将会有m×n列。
step 1-j1这个步骤等价于执行:
select * from customers c cross join orders o
执行结果为:(共有4×7列)
c.customerid | c.city | o.orderid | o.customerid |
fissa | madrid | 1 | frndo |
fissa | madrid | 2 | frndo |
fissa | madrid | 3 | krlos |
fissa | madrid | 4 | krlos |
fissa | madrid | 5 | krlos |
fissa | madrid | 6 | mrphs |
fissa | madrid | 7 | null |
frndo | madrid | 1 | frndo |
frndo | madrid | 2 | frndo |
frndo | madrid | 3 | krlos |
frndo | madrid | 4 | krlos |
frndo | madrid | 5 | krlos |
frndo | madrid | 6 | mrphs |
frndo | madrid | 7 | null |
krlos | madrid | 1 | frndo |
krlos | madrid | 2 | frndo |
krlos | madrid | 3 | krlos |
krlos | madrid | 4 | krlos |
krlos | madrid | 5 | krlos |
krlos | madrid | 6 | mrphs |
krlos | madrid | 7 | null |
mrphs | zion | 1 | frndo |
mrphs | zion | 2 | frndo |
mrphs | zion | 3 | krlos |
mrphs | zion | 4 | krlos |
mrphs | zion | 5 | krlos |
mrphs | zion | 6 | mrphs |
mrphs | zion | 7 | null |
step 1-j2:应用on过滤,(join 条件)
on过滤条件是sql的三个过滤条件(on,where,having)中最先执行的,on过滤条件应用于前一步生成的虚拟表(vt1-j1),满足on过滤条件的行会被加入到虚拟表vt1-j2中。在应用了on 过滤之后,生成的vt1-j2表如下所示:
c.customerid | c.city | o.orderid | o.customerid |
frndo | madrid | 1 | frndo |
frndo | madrid | 2 | frndo |
krlos | madrid | 3 | krlos |
krlos | madrid | 4 | krlos |
krlos | madrid | 5 | krlos |
mrphs | zion | 6 | mrphs |
step 1-j3:添加外部列
这个步骤只会出现在使用了外连接的情况。对于外连接(left,right, or full),你可以标记一个或者两个表作为保留表。作为保留表意味着你希望这个表里面的所有列都被返回,即使它里面的数据不满足on子句的过滤条件。left outer join 把左边的表标记为保留表,rightouter join把右边的表作为保留表,full outer join把两个表都标记为保留表.step 1-j3为根据vt1-j2中的虚拟表,添加了保留表中不满足on 条件的列,在未保留表中没有对应的列,因此标记为null。这个过程生成了虚拟表vt1-j3。
c.customerid | c.city | o.orderid | o.customerid |
fissa | madrid | null | null |
frndo | madrid | 1 | frndo |
frndo | madrid | 2 | frndo |
krlos | madrid | 3 | krlos |
krlos | madrid | 4 | krlos |
krlos | madrid | 5 | krlos |
mrphs | zion | 6 | mrphs |
如果from子句中有多个表操作运算,sql会按照从左到右的顺序处理,左边生成的临时表结果作为右边表的输入表。
step 2 where 子句
where过滤被应用到前一步生成的临时表中,根据where过滤条件生成临时表vt2。
注意:由于数据现在还没有被分组,因此现在你不能使用聚合运算-例如:你不能使用这样的句子 where orderdate = max(orderdate)。另外你也不能使用select子句中创建的变量别名,因为现在还没有处理select子句-例如你不能写这样的句子:select year(orderdate) as orderyear . . . where orderyear > 2008.
应用这个过滤
where c.city = 'madrid'
这时生成的临时表vt2的内容如下:
c.customerid | c.city | o.orderid | o.customerid |
fissa | madrid | null | null |
frndo | madrid | 1 | frndo |
frndo | madrid | 2 | frndo |
krlos | madrid | 3 | krlos |
krlos | madrid | 4 | krlos |
krlos | madrid | 5 | krlos |
在这个例子中,你需要在on子句中使用on c.customerid = o.customerid过滤,没有订单的客户在1-j2这一步中被过滤掉,但是在1-j3这一步中作为外部列又被加回来。但是,由于你只想返回来自madrid的客户,因此你需要在where子句中过滤城市(where c.city = ‘madrid’),如果你放在on过滤中,不属于madrid的客户在添加外部列中会被添加回来。
关于on 和 where 的区别需要在这里说明一下,on 和where 的主要区别在于 on 实在添加外部列之前进行过滤,where 是在之后。on过滤掉的列会在1-j3中添加回来。如果你不需要添加外部列,那么这两个过滤是相同的。
step 3 group by子句
这个子句会把前一步中生成的临时表中的数据进行分组,每一行都会分到并且只分到一个组里,生成虚拟表vt3。vt3表中包含了vt2表中所有的数据,和分组标识符。
这是生成的临时表vt3的内容如下:
groups c.customerid |
c.customerid | c.city | o.orderid | o.customerid |
fissa | fissa | madrid | null | null |
frndo | frndo | madrid | 1 | frndo |
frndo | madrid | 2 | frndo | |
krlos | madrid | 3 | krlos | |
krlos | krlos | madrid | 4 | krlos |
krlos | madrid | 5 | krlos |
sql最终返回的结果中,每一个分组必须只能返回一行(除非被过滤掉),因此当一个sql语句中使用了group by时,在group by后面处理的子句,如select,having子句等,只能使用出现在group by后面的列,对于没有出现group by后面的列必须使用聚合函数(如 max ,min,count,avg等),保证每一个group只返回一行。
step 4 having子句
having子句用来过滤前一步生成的临时表,并且只作用于分组后的数据,满足having条件的group被添加到虚拟表vt4中。
当应用了这个过滤:
复制代码 代码如下:
having count(o.orderid) < 3
之后,生成的vt4表内容如下:
groups c.customerid |
c.customerid | c.city | o.orderid | o.customerid |
fissa | fissa | madrid | null | null |
frndo | frndo | madrid | 1 | frndo |
frndo | madrid | 2 | frndo |
需要注意的一点是,这里面使用的是count(o.orderid),而不是count(*),由于这个查询中添加了外部列,count方法会忽略null的列,导致出现了你不想要的结果。
step 5 select 子句
尽管出现在sql语句的最前面,select在第五步的时候才被处理,select子句返回的表会最终返回给调用者。这个子句包含三个子阶段:(5-1)计算表达式,(5-2) 处理distinct,(5-3)应用top过滤。
step 5-1 计算表达式
select子句中的表达式可以返回或者操作前一步表中返回的基本列。如果这个sql语句是一个聚合查询,在step 3之后,你只能使用group by中的列,对不属于group集合中的列必须使用聚合运算。不属于from表中基本列的必须为其起一个别名,如year(orderdate) as orderyear。
注意:在select子句中创建的别名,不能在之前的step中使用,即使在select子句中也不能。原因是sql的很多操作是同时操作(all at once operation),至于什么是all-at-once operation这里就不再介绍了。因此,select子句中创建的别名只能在后面的子句中使用,如order by。例如:select year(orderdate) as orderyear . . . order by orderyear。
在这个例子中:
复制代码 代码如下:
select c.customerid, count(o.orderid) as numorders
结果会得到一个虚拟表vt5-1:
c.customerid | numorders |
fifssa | 0 |
frndo | 2 |
step 5-2:应用distinct子句
如果sql语句中使用了distinct,sql会把重复列去掉,生成虚拟表vt5-2。
step 5-3:应用top选项
top选项是t-sql提供的一个功能,用来表示显示多少行。基于order by子句定义的顺序,指定个数的列会被查询出来。这个过程生成虚拟表vt5-3。
正如上文提到的,这一步依赖于order by定义的顺序来决定哪些列应该显示在前面。如果你没有指定结果的order by顺序,也没有使用with ties子句 ,每一次的返回结果可能会不一致。
在我们的例子中,step 5-3被省略了,因为我们没有使用top关键字。
step 6:order by子句
前一步返回的虚拟表在这一步被排序,根据order by子句指定的顺序,返回游标vc6。order by子句也是唯一一个可以使用select子句创建的别名的地方。
注意:这一步和之前不同的地方在于,这一步返回的结果是一个游标,而不是表。sql是基于集合理论的,一个集合没有对他的行定义顺序,它只是一个成员的逻辑集合,因此成员的顺序并不重要。带有order by子句的sql返回一个按照特定序列组织每一行的对象。ansi 把这样的一个对象叫游标。理解这一点对你了解sql很重要。
上面的步骤如图所示:
本书中主要内容是参照 inside microsoft sql server 2008:t-sql query,中的内容,大家如果想深入了解sql查询相关的知识,可以找这本书看看,我这有英文原版的pdf,需要的可以找我要。