第五章

表表達式是一種命名的查詢表達式,表明一個有效的關係表。SQLServer支持4種類型的表表達式:
派生表(derived table)、公用表表達式(CTE,common table expression)、視圖,以及內聯表值函數(inline TVF,inline table-valued function)sql

表表達式並非物理上真實存在的什麼對象,它是虛擬的。對於表表達式的查詢在數據庫引擎內部都將轉換爲對底層對象的查詢。使用表表達式的好處一般體如今代碼的邏輯方面,而不是性能方面。數據庫

派生表是在外部查詢的from子句中定義的。派生表的存在範圍爲定義它的外部查詢,只要外部查詢一結束,派生表也就不存在了。express

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

要有效地定義任何類型的表表查詢,查詢語句必須知足三個要求:
一、不保證有必定的順序
二、全部的列必須有名稱
三、全部的列名必須是惟一的編程

-- 分配列別名
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的內部查詢也一樣適用。app

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;

使用參數post

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;
相關文章
相關標籤/搜索