第五章

表表达式是一种命名的查询表达式,代表一个有效的关系表。SQLServer支持4种类型的表表达式:
派生表(derived table)、公用表表达式(CTE,common table expression)、视图,以及内联表值函数(inline TVF,inline table-valued function)

表表达式并不是物理上真实存在的什么对象,它是虚拟的。对于表表达式的查询在数据库引擎内部都将转换为对底层对象的查询。使用表表达式的好处通常体现在代码的逻辑方面,而不是性能方面。

派生表是在外部查询的from子句中定义的。派生表的存在范围为定义它的外部查询,只要外部查询一结束,派生表也就不存在了。

use TSQLFundamentals2008;
select *
from (select custid, companyname
        from Sales.Customers
        where country=N'USA') as USACusts;

要有效地定义任何类型的表表查询,查询语句必须满足三个要求:
1、不保证有一定的顺序
2、所有的列必须有名称
3、所有的列名必须是唯一的

-- 分配列别名
select orderyear, count(distinct custid) as numcusts
from (select YEAR(orderdate) as orderyear, custid
        from Sales.Orders) as d
group by orderyear;
--
select orderyear, count(distinct custid) as numcusts
from (select YEAR(orderdate), custid
        from Sales.Orders) as d(orderyear, custid)
group by orderyear;

-- 使用参数
declare @empid as int = 3;

select orderyear, count(distinct custid) as numcusts
from (select year(orderdate) as orderyear, custid
        from Sales.Orders
        where empid = @empid) as d
group by orderyear;

嵌套:如果须要用一个本身就引用了某个派生表的查询去定义另一个派生表,最终得到的就是嵌套派生表。派生表之所以会嵌套,是因为在外部查询的from子句中定义了派生表,而不是单独定义的。嵌套一般是编程过程中容易产生问题的一个方面,因为它趋于让代码变得复杂,降低代码的可读性。

select orderyear, numcusts
from (select orderyear, count(distinct custid) as numcusts
        from (select YEAR(orderdate) as orderyear, custid
                from sales.orders) as d1
        group by orderyear) as d2
where numcusts > 70;

派生表的多引用:派生表另一个存在问题的方面源于派生表是在外部查询的from子句中定义的,其逻辑顺序并不优先于外部查询。当对外部查询的from子句进行处理时,派生表其实并不存在。因此,如果需要引用派生表的多个实例,这是还不能这样做。相反,必须基于同一查询去定义多个派生表。

select
    cur.orderyear
    , cur.numcusts as curnumcusts
    , prv.numcusts as prvnumcusts
    , cur.numcusts - prv.numcusts as growth
from (select 
        year(orderdate) as orderyear
        , count(distinct custid) as numcusts
        from sales.Orders
        group by year(orderdate)) as cur
    left outer join
        (select 
            year(orderdate) as orderyear
            , count(distinct custid) as numcusts
        from sales.orders
        group by year(orderdate)) as prv
    on cur.orderyear = prv.orderyear + 1;

公用表表达式(CTE)是和派生表很相似的另一种形式的表表达式,而且具有一些重要优势。
CTE使用with子句定义的,它的一般格式为:
WITH <CTE_NAME>[(<TARGET_COLUMN_LIST>)]
AS
(
       <INNER_QUERY_DEFINING_CTE>
)
<OUTER_QUERY_AGAINST_CTE>;
前面提到的为了有效定义表表达式而须要遵守的所有规则,对定义CTE的内部查询也同样适用。

with USACusts as
(
    select custid, companyname
    from sales.Customers
    where country = N'USA'
)
select * from USACusts;

和派生表一样,一旦外部查询完成,CTE的生命期也结束了
CTE也支持两种格式的列别名命名方式--内联格式和外部格式。对于内联格式,要指定<expression> AS <column_alias>;对于外部格式,在CTE名称后面的一对圆括号中目标列的列表。

with C as
( 
    select year(orderdate) as orderyear, custid
    from Sales.Orders
)
select orderyear, COUNT(distinct custid) as numcusts
from c
group by orderyear;

with C(orderyear, custid) as
(
    select year(orderdate), custid
    from Sales.Orders
)
select orderyear, count(distinct custid) as numcusts
from c
group by orderyear;

使用参数

declare @empid as int = 3;
with c as
(
    select year(orderdate) as orderyear, custid
    from Sales.Orders
    where empid = @empid
)
select orderyear, count(distinct custid) as numcusts
from c
group by orderyear;

定义多个CTE

with c1 as
(
    select year(orderdate) as orderyear, custid
    from Sales.Orders
),
c2 as 
(
    select orderyear, count(distinct custid) as numcusts
    from c1
    group by orderyear
)
select orderyear, numcusts
from c2
where numcusts > 70;

CTE的多引用

with yearlycount as
(    
    select 
        year(orderdate) as orderyear
        , COUNT(distinct custid) as numcusts
    from 
        sales.orders
    group by
        YEAR(orderdate)
)
select 
    cur.orderyear
    , cur.numcusts as curnumcusts
    , prv.numcusts as prvnumcusts
    , cur.numcusts - prv.numcusts as growth
from 
    yearlycount as cur
    left outer join
    yearlycount as prv
        on cur.orderyear = prv.orderyear;

递归CTE
CTE之所以与其他表表达式不同,是因为它支持递归查询。定义一个递归CTE至少需要两个(可能需要更多)
查询:第一个查询称为定位点成员(anchor member),第二个查询称为递归成员(recursive member)。
基本格式为
with <CTE_NAME>[(<target_column_list>)]
as
(    
    <anchor_member>
    union all
    <recursive_member>
)
<outer_query_against_CTE>;
 
递归成员是一个引用了CTE名称的查询。对CTE名称的引用代表的是一个在一个执行序列中逻辑上的“前一个结果集”。第一次调用递归成员时,“前一个结果集”代表由定位点成员返回的任何结果集。之后每次调用递归成员时,对CTE名称的引用代表对递归成员的前一次调用所返回的结果集。递归成员没有显式的递归终止检查(终止检查是隐式的)。递归成员会一直被重复调用,直到返回空的结果集或超出了某种限制条件。在查询返回的结果上,两个成员查询必须在列的个数和相应列的数据类型上保持兼容。外部查询中的CTE名称引用代表对定位点成员调用和所有对递归成员调用的联合结果集。

with empsCte as
 (
    select empid, mgrid, firstname, lastname
    from hr.Employees
    where empid = 2

    union all

    select c.empid, c.mgrid, c.firstname, c.lastname
    from empsCte as p
        join hr.Employees as c
            on c.mgrid = p.empid
)
select empid, mgrid, firstname, lastname
from empsCte;

如果递归成员的联接谓词中存在逻辑错误,或是循环中的数据结果出了问题,都可能导致递归成员被调用无限多次。为了安全起见,SQLServer默认把递归成员最多可以调用的次数限制为100次,递归成员的调用次数达到101次时,代码将会因递归失败而终止运行。为了修改默认的最大递归次数,可以在外部查询的最后指定option(maxrecursion n)。这里的n是一个范围在0到32767之间的整数,代表想要设定的最大递归调用次数限制。如果想去掉对递归调用次数的限制,可以将maxrecursion设为0。注意,SQLServer把定位点成员和递归成员返回的临时结果集先保存在tempdb数据库的工作表中。如果去掉对递归次数的限制,万一查询失控,工作表的体积将很快变得非常大。当tempdb数据库的体积不能再继续增长时,查询便会失败。

视图
视图和内联表值函数(inline TVF)是两种可重用的表表达式,它们的定义存储在一个数据库对象中。一旦创建,这些对象就是数据库的永久部分:只有用删除语句显式删除,他们才会从数据库中移除。在其他很多方面,视图和内联表值函数的处理方式都类似于派生表和CTE。例如,当查询视图和内联TVF时,SQLServer会先扩展表表达式的定义,再直接查询底层对象,这与派生表和cte的处理方式是一样的。

use TSQLFundamentals2008;
if    OBJECT_ID('sales.usacusts') is not null
    drop view Sales.usacusts;
go
create view sales.usacusts
as
select
    custid, companyname, contactname, contacttitle, address,
    city, region, postalcode, country, phone, fax
from Sales.Customers
where country = N'USA';
go

select custid, companyname
from Sales.usacusts;

因为视图是数据库中的一个对象,所以可以用权限来控制对视图的访问,就像其他查询的数据库对象一样。
注意,一般推荐在和视图有关的应用上下文应该避免使用select * 语句。列是在编译视图时进行枚举的,新加的列可能不会自动加到视图中。
用一个名为sp_refreshview的存储过程可以刷新视图的元数据,但为避免混淆,最好的开发实践就是在视图的定义中显式的列出需要的列名。如果在底层表中添加了列,而且在视图中需要这些新加的列,则可以使用alter view语句对视图定义进行相应的修改。

视图和order by子句
用于定义视图的查询语句,必须满足之前在介绍派生表时对表表达式提到的所有要求。虽然视图不用保证数据行的任何顺序,但视图的所有列都必须有名称,而且所有列名必须是唯一的。
记住,在定义表表达式的查询语句中不允许出现order by子句,因为关系表的行之间没有顺序。视图创建一个有序视图的想法也不合理,因为这违反了关系模型定义的关系的基本属性。如果为了数据展示的目的,确实需要从视图中返回有序的数据行,这时也不应该让视图做违反规则的事情。相反,应该在使用视图的外部查询中指定一个数据展示用的order by子句。

select custid, companyname, region
from sales.usacusts
order by region;

输出中行的任何顺序都可以认为是有效的,不会保证有什么特定的顺序。因此,当对表表达式进行查询时,除非在外部查询中指定了order by子句,否则不应该假定输出具有任何顺序。
不要把用于定义表表达式的查询和其他用途的查询混为一谈。对于包含top和order by的查询,只有在表表达式的上下文中,它才不保证输出具有特定的顺序。而对于不是用于定义表表达式的查询,order by子句即用于为top选项提供逻辑筛选服务,也用于控制输出结果的排列顺序。

视图选项
当创建或修改视图时,可以在视图定义中指定视图的属性和选项。在视图定义的头部,可以用with子句来指定诸如encryption和schemabinding这样的属性;在视图查询的末尾,还可以指定with check option。

encryption选项
在创建和修改视图、存储过程、触发器及用户定义函数(UDF)时,都可以使用encryption选项。如果指定encryption选项,SQLServer在内部会对定义对象的文本信息进行混淆(obfuscated)处理。普通用户通过任何目录对象都无法直接看到这种经过混淆处理的文本,只有特权用户通过特殊手段才能反问创建对象的文本。

alter view sales.usacusts
as
select
    custid, companyname, contactname, contacttitle, address
    , city, region, postalcode, country, phone, fax
from sales.Customers
where country = N'USA';
go
-- 得到视图定义
select OBJECT_DEFINITION(object_id('sales.usacusts'));
-- 修改视图定义,这一次要包含encryption选项
alter view sales.usacusts with encryption
as
select custid, companyname, contactname, contacttitle, Address
        ,city, region, postalcode, country, phone, fax
from sales.Customers
where country = N'USA';
-- 再一次获取视图定义的文本,得到结果为NULL
select OBJECT_DEFINITION(object_id('sales.usacusts'));
-- 除了object_definition函数,还可以使用存储过程sp_helptext来获取对象的定义。
exec sp_helptext 'sales.usacusts';

SCHEMABINDING选项
视图和UDF支持SCHEMABINDING选项,该选项可以将对象和列的架构绑定到引用其对象的架构。也就是说,一旦指定了这个选项,被引用的对象就不能被删除,被引用的列也不能删除或修改。

alter view sales.usacusts with schemabinding
as
select custid, companyname, contactname, contacttitle, address,
        city, region, postalcode, country, phone, fax
from sales.Customers
where country = N'USA';
-- 试图删除customers表中的address列时会提示错误信息
alter table sales.customers drop column address;

对象定义必须满足两个技术要求,才能支持SCHEMABINDING选项。不允许在查询的select子句中使用星号*, 必须显式的列出列名。此外,在引用对象时,必须使用带有架构名称修饰的完整对象名称。这两个要求都是平常值得遵守的最佳实践原则。可以想象,在创建对象时指定SCHEMABINDING选项,也是一种好的实践方法。

CHECK OPTION选项
check option选项的目的是为了防止通过视图执行的数据修改与视图中设置的过滤条件发生冲突。

-- 例如,插入一个英国客户
insert into sales.usacusts(companyname, contactname, contacttitle, address,
                            city, region, postalcode, country, phone, fax)
values(N'Customers ABCDE', N'Contact ABCDE', N'Title ABCDE', N'Address ABCDE',
        N'London', Null, N'12345', N'UK', N'012-3456789', N'012-3456789');
-- 查找这个客户将的到一个空的结果集
select custid, companyname, country
from sales.usacusts
where companyname=N'customers ABCDE';
-- 为了查找这个新客户,可以直接查询customers表
select custid, companyname, country
from sales.Customers
where companyname = N'Customers ABCDE';
-- 如果想防止这种与视图的查询过滤条件相冲突的修改,只需要在定义视图的查询语句末尾加上
-- with check option即可
alter view sales.usacusts with schemabinding
as
select custid, companyname, contactname, contacttitle, address,
        city, region, postalcode, country, phone, fax
from sales.Customers
where country = N'USA'
with check option;
go
-- 这样,在试图插入数据时会报错

内联表值函数是一种可重用的表表达式,能够支持输入参数。除了支持输入参数以外,内联表值函数在其他方面都与视图相似。正因为如此,内联表值函数可以看作是一种参数化视图,尽管并没有这种正式的说法。

use tsqlfundamentals2008;
if OBJECT_ID('dbo.fn_getcustorders') is not null
    drop function dbo.fun_getcustorders;
go
create function dbo.fn_getcustorders
    (@cid as int) returns table
as
    return
        select orderid, custid, empid, orderdate, requireddate,
            shippeddate, shipperid, freight, shipname, shipaddress,
            shipcity, shipregion, shippostalcode, shipcountry
        from Sales.Orders
        where custid = @cid;
go

这个内联表值函数接受一个代表客户id的输入参数@cid,返回由输入客户下的所有订单。对内联表值函数的查询和用DML(数据操作语言)对其它表进行的查询一样。如果函数接受输入参数,则可以在函数名称后面的圆括号内列出所有参数。此外,应该确保为表表达式提供别名。并不总是必须为表表达式提供别名,但这的确是一个很好的时间方法,因为它可以增强代码的可读性,减少出错的机会。

select orderid, custid
from dbo.fn_getcustorders(1) as co;

select co.orderid, co.custid, od.productid, od.qty
from dbo.fn_getcustorders(1) as co
    join sales.OrderDetails as od
        on co.orderid = od.orderid;

APPLY运算符也是在SQLServer2005中引入的一个非标准表运算符。和其他表运算符一样,这个运算符也是在查询的FROM子句中使用。APPLY运算符支持两种形式:CROSS APPLY和OUTER APPLY。

CROSS APPLY只实现了一个逻辑查询步骤,而OUTER APPLY实现了两个步骤。APPLY 运算符对两个输入表进行操作,其中第二个可以是一个表表达式,而我们将它们分别称为左表和右表。右表通常是一个派生表或内联表值函数。CROSS APPLY运算符实现了一个逻辑查询处理逻辑:把右表表达式应用到左表中的每一行,再把结果集组合起来,生成一个统一的结果表。就目前来看,CROSS APPLY运算符与交叉连接非常类似,从某种意义上讲也确实如此。

select s.shipperid, e.empid
from sales.shippers as s
    cross join hr.Employees as e;

select s.shipperid, e.empid
from sales.Shippers as s
    cross apply hr.Employees as e;

与联接不同的是,当使用cross apply操作符时, 右表表达式可能代表不同的数据行集合。为此,可以在右边使用一个派生表,在派生表的查询中去引用左表列;也可以使用内联表值函数,把左表中的列作为输入参数进行传递。

select c.custid, a.orderid, a.orderdate
from Sales.Customers as c
    cross apply
        (select top (3) orderid, orderdate, requireddate
        from Sales.Orders as o
        where o.custid = c.custid
        order by orderdate desc, orderid desc) as a;

可以把上面查询中的表表达式A看作是一个相关子查询。就逻辑查询处理来说,右表表达式要应用于customers表的每一行。如果右表表达式返回的是一个空集,cross apply运算符则不会返回相应左边的数据行。如果要右表表达式返回空集时也照样返回相应左表中的行,则可以用outer apply运算符代替cross apply。outer apply运算符增加了另一个逻辑处理阶段:标识出让右表表达式返回空集的坐标中的数据行,并把这些行作为外部行添加到结果集中,来自右表表达式的列用null作为占位符。从某种意义上讲,这个处理步骤类似于左外联接中增加外部行的那一步。

select c.custid, a.orderid, a.orderdate
from Sales.Customers as c
    outer apply 
    (select top(3) orderid, empid, orderdate, requireddate
    from sales.Orders as o
    where o.custid = c.custid
    order by orderdate desc, orderid desc) as a;

以下代码创建了一个内联表值函数fn_toporders

if OBJECT_ID('dbo.fn_toporders') is not null
    drop function dbo.fn_toporders;
go
create function dbo.fu_toporders
    (@custid as int, @n as int)
    returns table
as
return
    select top(@n) orderid, empid, orderdate, requireddate
    from sales.Orders
    where custid = @custid
    order by orderdate desc, orderid desc;
go

select
    c.custid, c.companyname,
    a.orderid, a.empid, a.orderdate, a.requireddate
from sales.Customers as c
    cross apply dbo.fu_toporders(c.custid, 3) as a;
原文地址:https://www.cnblogs.com/panshu/p/3280032.html