• 存储过程——公用表表达式(CTE)


    shanzm-2020年5月15日 23:07:32

    0. 背景说明

    在编写存储过程的时候,大多数情况都会使用公用表表达式,本文对公用表表达式(CTE)作简单的介绍

    ①准备工作
    创建一个产品表T_Product

    CREATE TABLE [dbo].[T_Product]
    (
    	[Id] [bigint] IDENTITY(1,1) NOT NULL,--自增字段Id
    	[Category] [nvarchar](50) NOT NULL,--产品种类
    	[Name] [nvarchar](50) NOT NULL,--产品名
    	[Price] [decimal](18, 0) NOT NULL,--产品价格
    )
    

    随便插入一些数据即可


    ②子表达式

    了解公用表表达式之前我们要回顾一下SQL语句中的子查询

    之前在我们在select语句中可以使用子查询,

    例如:

    select * from (select  * from  T_Product where Category='clothes' )T where T.Price>500`
    

    这里就是使用了一个子查询:查询出T_Product表中的所有的种类为clothes的产品,子查询的结果表定义别名为T,我们在T表中继续查询所有Price大于500的产品


    ③表变量

    子查询虽然不复杂,但是使用多层的子查询嵌套,则SQL语句会变得难以阅读,

    当然,我们可以使用表变量,即:定义一个表变量,然后将子查询的结果集存入表变量中,以供后续的使用。

    例如:

    --声明一个表变量
    declare @T table
    (
        Id bigint not null ,
        Category nvarchar(50) not null, 
        Name nvarchar(50) not nul ,
        Prince decimal not null 
    )
    --将子查询的数据插入表变量中
    insert into @T select * from T_Product where Category='clothes'
    
    --使用表变量
    select * from @T where  Price>500
    

    ④公用表表达式

    将子查询放在了表变量@T中,这样做将使SQL语句更容易维护,但又会带来另一个问题,就是性能的损失。

    由于表变量实际上使用了临时表,从而增加了额外的I/O开销,因此,表变量的方式并不太适合数据量大且频繁查询的情况。

    为此,在SQL Server 2005中提供了另外一种解决方案,这就是公用表表达式(CTE),使用CTE,可以使SQL语句的可维护性,同时,CTE要比表变量的效率高得多。

    这里就先演示一下使用CTE,具体的语法细节后面做说明

    --声明一个CTE
    with T as 
    (
        select * from T_Product where Category='clothes'
    )
     
    --使用CTE
    select * from T where Price >500
    

    这里我们就是使用公用表表达式替换这里的子查询的结果集。

    所以使用使用with&as定义的公用表表达式也称为子查询部分(subquery factoring)

    同时,公用表表达式在存储过程中可以用于代替视图的使用,在某个存储过程中需要使用是个结果集,我们没必要为其创建一个视图,可以使用公用表表达式。



    1. 定义及语法细节

    1.1 基本定义

    定义:公用表表达式(Common Table Expression) 是SQL Server2005版本的引入的一个特性。CTE可以看组是一个临时的结果集,可以再接下来来的一个select,insert,update,delete,merge语句中多次引用。使用公用表达式CTE可以让语句更加清晰简练。

    简而言之:公用表表达式就是用于临时存储结果集。

    1.2 基本语法

    创建一个公用表表达式,并使用改公用表表达式的基本语法:

    with  subquery_name(column1,column2,column3……)--定义一个CTE
    as 
    (
        select  column1,column2,column3  from  table_name
    )
    select * from subquery_name-- 引用CTE
    

    注意事项:

    • 公用表表达式只能使用在其定义后的第一个sql语句中,否则会报错

    • 定义CTE的时候,可以省略列名,即最终的该CTE的列名就是该CTE中select查询的结果,但是我现在觉得尽量不要省略,可以方便后续阅读。

    正确示例:

    with temp as
    (
    select * from T_Product where Category='外套'
    )
    select * from temp as temp1 left join temp as temp2 on temp1.Id =temp2.Id
    

    上面是声明了一个公用表表达式temp,在其之后的第一句sql语句中使用到temp,完全没有问题。

    错误示例:

    with temp as
    (
    select * from T_Product where Category='外套'
    )
    select * from temp  --公用表表达式temp之后的第一句sql语句
    select * from temp as temp1 left join temp as temp2 on temp1.Id =temp2.Id--公用表表达式temp之后的第二句sql语句
    

    结果是报错:显示第一句sql语句已执行,但是第二句sql语句报错“对象名'temp'无效”

    正是因为只有CTE之后的第一句sql语句可以使用该CTE,所以如果CTE的表达式名称与某个数据表或视图重名,则紧跟在该CTE后面的SQL语句使用的仍然是CTE

    CTE语法细节:

    1. 如果将 CTE 用在属于批处理的一部分的语句中,那么在CTE之前的语句必须以分号结尾。

      【补充】:批处理是指从应用程序一次性地发送一组完整sql语句到sql server上执行,批处理的所有语句被当做一个整体,被成批地分析,编译和执行,所有的批处理 指令以GO 作为结束标志。同时写多个批处理,如果前面所有的批处理没有问题,最后一个有错误那么前面所有的批处理都不会执行

    2. CTE中的sql语句是不能使用order by语句,除非select语句中有top(其实这里我也没有想明白,理论上子查询使用order by完全是没有问题的)

    3. 前面的with子句定义的查询在后面的with子句中可以使用。但是一个with子句内部不能嵌套with子句

    1.3 多个CTE同时声明

    这里就要思考一个问题了,那就是若是在语句sql语句中需要多个公用表表达式,我们可以连续的声明多个公用表表达式,但是注意只需要在第一个公用表表达式上使用with,之后相连的则不需在使用with

    with temp1 as
    ( 
        select * from T_Product where Category='外套'
    )
    ,temp2 as--注意这里不在需要使用with ,但是不要忘记as
    (
        select * from T_Product where Category='裤子'
    )
    select * from temp1 ,temp2 where temp1.Price=temp2.Price
    

    1.4 CTE嵌套使用

    一次声明的多个公用表表达式,后面的公用表表达式可以使用前面的公用表表达式
    例如:

    with temp1 as 
    (
        select * from T_Product where Category='外套'
    )
    ,temp2 as 
    (
        select * from temp1 where Price>200--在相连的第二个CTE中使用第一个CTE
    )
    select * from temp2
    


    2. CTE递归查询

    2.1 简介

    递归查询主要用于层次结构的查询,从叶级(Leaf Level)向顶层(Root Level)查询,或从顶层向叶级查询,或递归的路径(Path)。

    递归 CTE 定义至少必须包含两个 CTE 查询定义,一个定位点成员和一个递归成员。可以定义多个定位点成员和递归成员;但必须将所有定位点成员查询定义置于第一个递归成员定义之前。

    第一个子查询称作定点(Anchor)子查询:定点查询只是一个返回有效表的查询,用于设置递归的初始值;
    第二个子查询称作递归子查询:该子查询调用CTE名称,触发递归查询,实际上是递归子查询调用递归子查询;
    两个子查询使用union all,求并集;

    2.2 准备工作

    创建一个公司表,公司中的部门是分等级的,PId即该部门的上一级部门

    CREATE TABLE [dbo].[Company]
    (
    	[Id] [bigint] IDENTITY(1,1) NOT NULL,
    	[PId] [bigint] NOT NULL,
    	[Name] [nvarchar](50) NOT NULL,
    )
    

    插入数据,我们使用行政等级模拟上下级部门:

    Id        PId       Name
    --------- --------- ----------
    1         0         中国
    2         1         江苏省
    3         2         苏州市
    4         3         吴中区
    5         1         山东省
    6         5         济南市
    7         5         青岛市
    8         5         烟台市
    9         2         南京市
    11        9         玄武区
    

    2.3 计算每个部门的等级序列

    USE [ShanTest]
    GO 
    
    with temp as 
    (
    	select *,0 as Level from Company where Pid =0
    	union all
    	select c.Id,c.Pid,c.Name,temp.Level+1 as Level
    	from  Company as c,temp  where temp.Id=c.Pid
    )
    select * from temp
    

    运行测试结果:

    Id        PId       Name           Level
    --------- --------- ------------   ------------
    1         0         中国            0
    2         1         江苏省          1
    5         1         山东省          1
    6         5         济南市          2
    7         5         青岛市          2
    8         5         烟台市          2
    3         2         苏州市          2
    9         2         南京市          2
    11        9         玄武区          3
    4         3         吴中区          3
    

    简单的理一理这里的递归:

    首先:

    select *,0 as Level from Company where Pid =0
    

    结果是:

    Id        PId       Name           Level
    --------- --------- ------------   ------------
    1         0         中国            0
    

    接着:

    union all
    select c.Id,c.Pid,c.Name,temp.Level+1 as Level from  Company as c,temp  where c.Pid=temp .Id
    

    第一次递归结果:

    Id        PId       Name           Level
    --------- --------- ------------   ------------
    1         0         中国            0
    2         1         江苏省          1
    5         1         山东省          1
    

    第二次递归结果:

    Id        PId       Name           Level
    --------- --------- ------------   ------------
    1         0         中国            0
    2         1         江苏省          1
    5         1         山东省          1
    6         5         济南市          2
    7         5         青岛市          2
    8         5         烟台市          2
    3         2         苏州市          2
    9         2         南京市          2
    

    第三次递归结果:

    Id        PId       Name           Level
    --------- --------- ------------   ------------
    1         0         中国            0
    2         1         江苏省          1
    5         1         山东省          1
    6         5         济南市          2
    7         5         青岛市          2
    8         5         烟台市          2
    3         2         苏州市          2
    9         2         南京市          2
    11        9         玄武区          3
    4         3         吴中区          3
    

    【注意】:

    • 递归CTE可能会出现无限递归。从而大量消耗SQL Server的服务器资源.因此,SQL Server提供了OPTION选项,可以设定最大的递归次数。

      这个最大递归次数往往是根据数据所代表的具体业务相关的,比如这里,我们定义最大递归数是2:option(maxrecursion 2)

    • 递归查询没有显式的递归终止条件,只有当递归子查询返回空结果集(没有数据行返回)或是超出了递归次数的最大限制时,才停止递归。

    USE [ShanTest]
    GO 
    
    with temp as 
    (
    	select *,0 as Level from Company where Pid =0
    	union all
    	select c.Id,c.Pid,c.Name,temp.Level+1 as Level
    	from  Company as c,temp  where c.Pid=temp .Id
    )
    select * from temp
    option(maxrecursion 2)--设置最大递归数是2
    

    则结果现实如下,即最大递归两次则只能查询到市一级,无法查询到区一级

    Id        PId       Name           Level
    --------- --------- ------------   ------------
    1         0         中国            0
    2         1         江苏省          1
    5         1         山东省          1
    6         5         济南市          2
    7         5         青岛市          2
    8         5         烟台市          2
    3         2         苏州市          2
    9         2         南京市          2
    
    消息 530,级别 16,状态 1,第 6 行
    语句被终止。完成执行语句前已用完最大递归 2。
    

    2.4 查询所有的子级与父级匹配结果

    作为层级结构,可以使用自连接查询每个部门的上级部门:

    --隐式内连接
    select  a.Id ,a.Pid ,a.Name ,b.Name as PName
    from Company a ,Company b where a.Pid=b.Id 
    
    --显式内连接:
    select a.Id ,a.Pid ,a.Name ,b.Name as PName
    from Company a inner join Company b on a.Pid =b.Id
    

    查询结果:

    Id       Pid      Name         PName
    -------- -------- ---------   ----------
    2        1        江苏省       中国
    3        2        苏州市       江苏省
    4        3        吴中区       苏州市
    5        1        山东省       中国
    6        5        济南市       山东省
    7        5        青岛市       山东省
    8        5        烟台市       山东省
    9        2        南京市       江苏省
    11       9        玄武区       南京市
    

    下面演示使用递归CTE实现,所有的子级匹配所有的父级

    with subq as
    (
    	select  Id ,Pid ,Name  ,Name as PName
    	from Company  where Pid =0
    	union all
    	select  c.Id ,c.Pid,c.Name  ,s.Name as PName 
    	from subq as s inner join  Company as c on s.Id =c.Pid
    	--from subq as s,Company as c where s.Id=c.Pid
    )
    select * from subq 
    
    Id       Pid      Name          PName
    -------- -------- ----------   ---------
    1        0        中国          中国
    2        1        江苏省        中国
    5        1        山东省        中国
    6        5        济南市        山东省
    7        5        青岛市        山东省
    8        5        烟台市        山东省
    3        2        苏州市        江苏省
    9        2        南京市        江苏省
    11       9        玄武区        南京市
    4        3        吴中区        苏州市
    

    理解递归的方式就是,从头理一理:

    首先:

    select  Id ,Pid ,Name  ,Name as PName from Company  where Pid =0
    

    结果是:

    Id       Pid      Name          PName
    -------- -------- ----------   ---------
    1        0        中国          中国
    

    接着

    select  c.Id ,c.Pid,c.Name  ,s.Name as PName  from subq as s inner join  Company as c on s.Id =c.Pid
    

    第一次递归结果:

    Id       Pid      Name          PName
    -------- -------- ----------   ---------
    1        0        中国          中国
    2        1        江苏省        中国
    5        1        山东省        中国
    

    第二次递归:

    Id       Pid      Name          PName
    -------- -------- ----------   ---------
    1        0        中国          中国
    2        1        江苏省        中国
    5        1        山东省        中国
    6        5        济南市        山东省
    7        5        青岛市        山东省
    8        5        烟台市        山东省
    3        2        苏州市        江苏省
    9        2        南京市        江苏省
    

    第三次递归

    Id       Pid      Name          PName
    -------- -------- ----------   ---------
    1        0        中国          中国
    2        1        江苏省        中国
    5        1        山东省        中国
    6        5        济南市        山东省
    7        5        青岛市        山东省
    8        5        烟台市        山东省
    3        2        苏州市        江苏省
    9        2        南京市        江苏省
    11       9        玄武区        南京市
    4        3        吴中区        苏州市
    

    2.5 父查子-查询某个部门的下级部门

    比如说,这里查询表中所有江苏省以下的行政区域

    with temp as
    (
    	select * from Company where Id=2--江苏省的Id是2,所以递归初始值就是2
    	union all
    	select c.* 
    	from temp ,szmCompany as c where temp.Id =c.Pid
    )
    
    select * from temp
    --option(maxrecursion 1)
    

    查询结果:

    Id     Pid    Name
    ------ ------ ----------
    2      1      江苏省
    3      2      苏州市
    9      2      南京市
    10     9      玄武区
    4      3      吴中区
    

    其实这里,若是我们只需要江苏省的下一级(即:市级),而不需要下下级(即:区县级)
    则可以设置递归的次数为1即可:option(maxrecursion 1)

    结果为:

    Id     Pid    Name
    ------ ------ ----------
    2      1      江苏省
    3      2      苏州市
    9      2      南京市
    消息 530,级别 16,状态 1,第 1 行
    语句被终止。完成执行语句前已用完最大递归 1。
    

    2.6 子查父-查询某个下级部门的上级部门

    通过子部门查询其父部门,比如查询吴中区的上级行政区域

    with temp as
    (
    	select *  from Company where Id=4--吴中区Id
    	union all
    	select c.* 
    	from temp ,Company as c where temp.Pid =c.Id
    )
    
    select * from temp
    --option(maxrecursion 1)
    

    查询结果:

    Id    Pid   Name
    ----- ----- --------
    4     3     吴中区
    3     2     苏州市
    2     1     江苏省
    1     0     中国
    

    若是只需要查询吴中区的直系上级行政区域,则只要限制最大递归次数为1即可

    当然,若是只需要查直系上级,我们可以使用之前的上下级匹配的结果集,筛选特定的记录:

    select * from 
    (
        select a.* ,b.Name as PName from Company as a ,Company as b where a.Pid=b.Id--所有的上下级匹配结果集
    )X
    where X.Id =4--吴中区Id
    

    查询结果:

    Id     Pid     Name       PName
    ------ ------- --------- ----------
    4      3       吴中区      苏州市  
    


    3. 参考

    【待读】

    想要找一本关于存储过程的书籍,一直没有找到,所以都是在网上的一些博文中学习相关的技巧和语法细节

    感觉不系统,隐隐约约感觉自己关于T-SQL以及存储过程的使用还有许多不了解的地方!

  • 相关阅读:
    GenericServlet和HttpServlet有什么区别?
    什么是RMI?
    【WPF学习】第十八章 多点触控输入
    【WPF学习】第十七章 鼠标输入
    【WPF学习】第十六章 键盘输入
    【WPF学习】第十五章 WPF事件
    【WPF学习】第十四章 事件路由
    【WPF学习】第十三章 理解路由事件
    【WPF学习】第十二章 属性验证
    【WPF学习】第十一章 理解依赖项属性
  • 原文地址:https://www.cnblogs.com/shanzhiming/p/12897818.html
Copyright © 2020-2023  润新知