SELECT 语法和逻辑处理顺序
虽然 select语句的完整语法较复杂,但其主要子句可归纳如下:
(8)
(12)into <new_table>
(1)
(3)
(2)
(4)
(5)
(6)
(7)
(10)order by <order_by_list>
其中,每个关键字都是一个独立的逻辑处理步骤,而关键字之前的数字代表了它在查询语句中的逻辑处理顺序。
SQL和其他编程语言最显著的不同之处就是代码处理的顺序。对于大多数编程语言,代码的书写顺序就是它的处理顺序。对于SQL,第一个处理的子句是from子句,而select子句,虽然是第一个出现的,但几乎是最后一个处理的。
基于客户/订单情景的样例查询
use tempdb
if object_id('Customers','U')is not nullbegin
end
if object_id('Orders','U')is not nullbegin
end
Go
create table Customers(
customer_id int,
name
gender
)
insert Customers select 1,'AmitPaul','M'
insert Customers select 2,'DhaniLennevald','M'
insert Customers select 3,'MarieFredriksson','F'
insert Customers select 4,'PerGessle','M'
create table Orders(
order_id
customer_id
product_id
)
insert Orders select 1,3,'B000XGJH1O'
insert Orders select 2,2,'B000GP8448'
insert Orders select 3,2,'B000FPOJOS'
insert Orders select 4,1,'B000FQ2D5E'
insert Orders select 5,3,'B000G0HJ3K'
insert Orders select 6,2,'B0011WMIME'
insert Orders select 7,1,'B000IONGWM'
select *from Customers
select *from Orders
查询语句的要求是返回订单总数不超过2的男性客户的客户编号、姓名及订单总数,并按照订单总数升序排序。
select c.customer_id,min(c.name)as name
,count(o.order_id)as order_count
步骤1:生成笛卡尔积(CROSS JOIN)
笛卡尔积是在from子句中出现的头两个表之间进行的,结果便是生成了虚拟表VT1。VT1包含了左表中每一行和右表中每一行的所有可能的组合(左表就是查询语句中出现在join关键字之前的表)。如果左表有n行,右表有m行,VT1就有n×m行。VT1中的字段是由源表的名称限定的(作前缀的),如果查询中指定了表的别名,这个前缀也可以使用别名。在随后的步骤中(步骤2以及后面的步骤),如果对某个字段名的引用是有歧义的(字段名出现在多个输入表中),那么该字段名必须是表限定的(比如,c.customer_id)。对于只出现在一个输入表中的字段名,指定表限定符是可选的(比如,o.order_id或order_id)。
select c.customer_id as [c.customer_id],c.nameas [c.name]
步骤2:应用ON过滤条件(JOIN条件)
select [Match?]=
以上语句可以用来模拟出虚拟表VT1应用on过滤条件之后的逻辑结果,其中只有<join_condition>='TRUE'的记录被写入到虚拟表VT2中,可以用以下脚本来模拟VT2:
select [Match?]=
步骤3:添加外部行
这个步骤只和outer join有关。通过指定outer join的类型(left、right或full),可以把一个或两个输入表标记为保留表。将一个表标记为保留表,意味着该表的所有行都会被返回,即使是被<join_condition>滤除的。left outer join将左表标记为保留表,right outer join将右表标记为保留表,而full outer join把两个表都标记为保留表。步骤3返回VT2中的行,加上保留表中没有在步骤2中匹配到的行。这些添加进来的行被称作外部行。外部行中属于非保留表的属性(字段值)被赋值为null。虚拟表VT3就这样生成了。
在这个例子中,要求返回订单总数不超过2的男性客户信息及订单总数,因此结果中可能包含订单数为0的客户,也就是说在Customers表中有用户信息,但是在Orders表中没有匹配的行。通过在Customers和Orders表之间执行left outer join,将Customers表标记为保留表,就可以够返回没有下过订单的客户,这里只有Per Gessle没有匹配的订单,因此作为外部行添加到虚拟表VT3中,而对于Orders表中的属性被赋予null。
select [Match?]='OUTER',c.customer_id as [c.customer_id]
以上脚本可以模拟外部行,再加上虚拟表VT2中的行,就生成了虚拟表VT3,模拟VT3的脚本如下:
select [Match?]=
union
select [Match?]='OUTER',c.customer_id as [c.customer_id]
以上脚本可以简化为:
select c.customer_id as [c.customer_id],c.nameas [c.name]
注意: 如果进行join的表超过两个,那么在VT3与from子句中的第三个表之间执行步骤1至3。如果from子句中有更多的表,就需要继续重复这个过程,而最终的虚拟表就作为下一个步骤的输入。
步骤4:应用WHERE过滤条件
where过滤条件应用到上个步骤返回的虚拟表中的所有行。只有对于<where_condition>条件为真的行成为这个步骤返回的虚拟表VT4的一部分。
VT3中的客户Marie Fredriksson这一行被去掉了,因为该客户的gender属性为'F'。此时,虚拟表VT4就生成了。模拟VT4的脚本如下:
select c.customer_id as [c.customer_id],c.nameas [c.name]
注意1:因为此时数据还未进行分组(group by),所以不能使用聚集过滤条件,比如,不可以这样写:where orderdate = max (orderdate)。同样,也不能引用select列表中创建的字段别名,因为select列表还没有被处理,比如,不可以这样写:select year(orderdate) as orderyear...where orderyear > 2008。
注意2:包含outer join子句的查询有个令人困惑的地方,应该在on过滤条件中还是在where过滤条件中指定逻辑表达式。两者主要的区别是,on是在添加外部行(步骤3)之前应用的,where是在步骤3之后应用的。保留表中被on过滤条件排除的行不是确定的,因为步骤3会把它添加回来;而被where过滤条件排除的行是确定的。
查询语句要求只保留来自男性客户的记录,所以必须在where子句中指定该过滤条件(where c.gender='M')。如果将该条件放在on子句中,那么在步骤3中,不满足on过滤条件,但是在保留表中出现的记录,比如客户Marie Fredriksson,就会被添加回来,结果就可能和用户所期望的不一致:
select c.customer_id as [c.customer_id],c.nameas [c.name]
提示: 只有在使用outer join的时候,on和where子句之间才有逻辑区别。当使用inner join的时候,在哪里指定逻辑表达式都无所谓了,因为步骤3被跳过了。过滤条件一个接一个地被应用,不会有中间步骤。
步骤5:分组(GROUPBY)
上个步骤中返回的表中的记录按照组来排列。group by子句的字段列表中的每个唯一的值的组合,就成了一个组。上个步骤中的每个基本行都附属于一个(并且只有一个)组。虚拟表VT5生成了。VT5包含两个部分:GROUPS部分由实际的组构成,而RAW部分由附加在组上的来自上个步骤的基本行构成。
set nocount on
declare my_cursor cursor
for select c.customer_id as [c.customer_id],c.nameas [c.name]
open my_cursor
declare @c_customer_id char(16)
print 'GROUPS
print 'c.customer_id
fetch nextfrom my_cursor into @c_customer_id,@name,@gender
while @@fetch_status=0
print replicate('-',104)
close my_cursor
deallocate my_cursor
如果查询中指定了group by子句,接下来的所有步骤(having、select等等)就只能使用在每个组中的结果为标量(单个)值的表达式。换句话说,结果要么是group by列表中含有的字段/表达式,例如c.customer_id,要么是聚集函数,比如count(o.order_id)。有这个限制的原因是,最终结果集中的每个组只能生成一条记录(被过滤掉的除外)。设想一下如果select列表中指定的是select c.customer_id,o.order_id,那么对于客户Dhani Lennevald来说应该返回什么样的结果。该组中将出现两个不同的order_id值;因此答案就是不确定的。SQL不会接受这样的请求。另一方面,如果指定:select c.customer_id,count(o.order_id)as order_count,那么Dhani Lennevald的答案就是确定的:就是3。
在这个阶段中,认为NULL之间是相等的。就是说,所有的NULL都被分到一个组里,好比是一个已知的值。
步骤6:应用CUBE或ROLLUP选项
步骤7:应用HAVING过滤条件
having过滤条件作用在上个步骤返回的虚拟表的组上。只有对于<having_condition>条件为真的组,才会加入到这个步骤返回的虚拟表VT7中。having过滤条件是第一个,也是唯一一个作用在分组数据上的过滤条件。模拟VT7的脚本如下:
set nocount on
set ansi_warnings off
if object_id('tempdb.dbo.#VT','U')is not nullbegin
end
;with CTE_OrderCount(customer_id,order_count)as(
select c.customer_id,count(o.order_id)
)select c.customer_id as [c.customer_id],c.nameas [c.name]
declare my_cursor cursor
for select *from #VT
open my_cursor
declare @c_customer_id char(16)
print 'GROUPS
print 'c.customer_id
fetch nextfrom my_cursor into @c_customer_id,@name,@gender
while @@fetch_status=0
print replicate('-',118)
close my_cursor
deallocate my_cursor
注意: 在这里使用count(o.order_id)而不是count(*),这一点是很重要的。因为这里是outer join,虽然客户Per Gessle没有订单,但是结果集里包含该客户的信息,因此count(*)的值为1;然而o.order_id的值为null,count(<表达式>)会像其他聚集函数一样忽略null,所以count(o.order_id)的值为0。
步骤8:处理SELECT列表
尽管select列表最先出现在查询中,但却是在第八个步骤处理的。select阶段构建出最终返回给调用者的表。select列表中的表达式可以使用上个步骤的虚拟表中的基本字段和经过处理的基本字段。模拟VT8的脚本如下:
select c.customer_id,min(c.name)as name
重要1: 如果是个聚集查询,即查询中包含group by这个步骤,那么select列表中可以直接引用出现在GROUPS部分(group by列表)中的字段;如果引用的是RAW部分的字段,就必须是聚集的。
在这个例子中,要求返回的字段是客户编号、姓名及订单总数,其中,customer_id是GROUPS部分中的字段,因此可以直接引用;除此之外,name是RAW部分中的字段,因此要放到聚集函数中,由于同一组中的name值是一样的,所以这个聚集函数可以是min()、max()或其他函数;而订单总数是计算出来的列,是在order_id(RAW部分中的字段)上使用了count()聚集函数。
对于不是基本字段的表达式应该使用别名(Alias),从而在结果表中获得一个字段名。例如,min(c.name)as name,count(o.order_id)as order_count。
这个瞬时操作的概念可能比较难理解。例如,在大多数编程环境中,在变量之间交换值的时候需要一个临时变量。然而,在SQL中交换表中字段的值,可以这样做:
use tempdb
if object_id('Table_A','U')is not nullbegin
end
Go
create table Table_A(column_1 int,column_2int)
insert Table_A select 1,23
insert Table_A select 4,56
insert Table_A select 7,89
select *from Table_A
update Table_A set column_1=column_2,column_2=column_1
select *from Table_A
步骤9:应用DISTINCT子句
步骤10:应用ORDERBY子句
上个步骤中返回的行会按照order by子句中指定的字段列表来排序,从而返回游标VC10。这个步骤是第一个,也是唯一可以重新使用select列表中创建的字段别名的步骤。模拟VC10的脚本如下:
select c.customer_id,min(c.name)as name
,count(o.order_id)as order_count
按照ANSISQL:1992和ANSISQL:1999的标准,如果指定了distinct,那么order by子句中的表达式就只能访问上个步骤返回的虚拟表VT9。也就是说,只能对select列表中的表达式进行排序。ANSI SQL:1992中即使在没有指定distinct的时候也有同样的限制。然而,ANSI SQL:1999通过允许select阶段中的输入和输出虚拟表都能被访问,来加强对order by的支持。就是说,如果没有指定distinct,就可以在order by子句中指定任何select子句中允许的表达式。就是说,最终结果集中没有返回的表达式也可以排序。T-SQL始终采用ANSI SQL:1999的执行方式。
select distinct customer_id
以上脚本的目的是按照订单编号的顺序返回唯一的客户编号,但无法通过语法检查。因为编号为3的客户有1、5两张订单,编号为2的客户有2、3和6三张订单,在使用distinct消除重复的customer_id过程中,同一customer_id的不同order_id将无法处理。
如果把上述脚本中的distinct去掉,就可以执行。其含义是:按照订单编号的顺序返回客户编号。
select *from Customers where customer_id in (
order by步骤中认为null之间是相等的。就是说,null在排序时被分在一起。ANSI把有关排序时,null和已知值比较的大小问题留给实施方。T-SQL在排序时认为null比已知值小(即null排在前面)。
select c.customer_id,o.order_id
步骤11:应用TOP选项
如果查询中同时指定了top子句和order by子句,但order by列表有重复值,那么结果就不确定了。要在这种情况下保证结果的确定性,就需要在top子句中使用with ties选项。此时,SQLServer会检查实际返回的最后一条记录,并返回表中所有与该记录排序值相同的其他记录。
之前提到过,带有order by子句的查询返回的是游标,而不是表表达式。通过指定非标准的、非关系型的top选项,含order by子句的查询返回的是关系型结果,因此可以用于表表达式:
select *from Customers where customer_id in (
如果查询中指定了top子句,但没有order by子句,那么SQLServer会返回最先访问到的指定数量的行。尽管执行计划被限制为表扫描或聚集索引扫描,但实际上只需读取所需的数据页。
这样的查询主要用来大致看一下表结构和数据样本,但如果用在过程化的代码中,则缺乏关系型含义,而且它返回的结果是不确定的。有人说,“不具确定性的编程是一种犯罪”。你同意吗?
步骤12:将查询结果保存到新创建的表中