SQLServer中的CTE通用表表達式


  開發人員正在研發的許多項目都涉及編寫由基本的 SELECT/FROM/WHERE 類型的語句派生而來的復雜 SQL 語句。其中一種情形是需要編寫在 FROM 子句內使用派生表(也稱為內聯視圖)的 Transact-SQL (T-SQL) 查詢。這一常規使開發人員能獲取一個行集,並立即將該行集加入到 SELECT 語句中的其他表、視圖和用戶定義函數中。另一種方案是使用視圖而不是派生表。這兩種方案都有其各自的優勢和劣勢。
當使用 SQL Server™ 2005 時,我更傾向於第三種方案,就是使用通用表表達式 (CTE)。CTE 能改善代碼的可讀性(以及可維護性),且不會有損其性能。此外,與早期版本的 SQL Server 相比,它們使得用 T-SQL 編寫遞歸代碼簡單了許多。
  首先,我將介紹 CTE 的工作原理以及可用它們來應對的情況。接着我將討論使用 CTE 相對於使用傳統的 T-SQL 構造的優勢,如派生表、視圖和自定義過程。在本期專欄中,我將給出示例並解釋它們的使用方法和適用情況。我還將演示 CTE 是如何處理遞歸邏輯並定義遞歸 CTE 的運行方式的。請注意,我在本專欄中討論的所有代碼都可從《MSDN® 雜志》網站下載獲得,而且它們還使用 SQL Server 2005 附帶的 Northwind 和 AdventureWorks 數據庫。

 

視圖、派生表和 CTE
  如果查詢需要在一組數據中進行選取,而這些數據在數據庫中並不是以表的形式存在,則 CTE 可能非常有用。例如,您可能想要編寫一個針對一組聚合數據的查詢,該聚合數據基於客戶及其訂單來計算值。這些聚合數據可能會將 Customers、Orders 和 Order Details 表聯接在一起,以計算訂單的總和以及平均值。此外,您可能想要查詢聚合的行集。一個方法是創建一個視圖,首先收集聚合數據,然后針對該視圖編寫一個查詢。另一個方法是使用派生表針對聚合數據編寫一個查詢 通過將 SQL 語句移到 FROM 子句中並對其進行查詢,可實現這一點。
視圖通常用來分解大型的查詢,以便用更易讀的方式來查詢它們。例如,一個視圖可以表示一個 SELECT 語句,該語句會將 10 個表聯接起來,選擇許多列,然后根據涉及的一組邏輯來過濾行。接着,可以通過其他 SELECT 語句在整個數據庫中查詢該視圖。此抽象使由該視圖表征的行集更容易訪問,而且無需在臨時表中復制或存儲數據。
  假定權限許可,這個視圖還能在整個數據庫中被重復使用。例如,在圖 1 中,已經創建了一個視圖,並為另一個 T-SQL 語句所使用。然而,當您想要收集數據並且只使用一次的時候,視圖未必是最佳解決方案。由於視圖是存在於數據庫中、適用於所有批處理的數據庫對象,那么創建僅用於單個 T-SQL 批處理的視圖就有些多余。
 1  
 2 CREATE VIEW vwMyView AS
 3 SELECT
 4     EmployeeID, COUNT(*) AS NumOrders,  MAX(OrderDate) AS MaxDate
 5 FROM Orders
 6 GROUP BY EmployeeID
 7 GO
 8 
 9 SELECT 
10     e.EmployeeID, oe.NumOrders, oe.MaxDate, e.ReportsTo AS ManagerID, 
11     om.NumOrders, om.MaxDate
12 FROM 
13     Employees AS e
14     INNER JOIN vwMyView AS oe ON e.EmployeeID = oe.EmployeeID
15     INNER JOIN vwMyView AS om ON e.ReportsTo = om.EmployeeID
View Code
另一種方法是創建派生表(也稱為內聯視圖)。要創建派生表,在由括號包圍的 FROM 子句中移動 SELECT 語句即可。接着就能像表或視圖一樣查詢或者聯接它。圖 2 中的代碼解決的查詢與圖 1 所解決的相同,但使用的是派生表而不是視圖。盡管只能在派生表所在的語句中訪問它們,但是,表通常使查詢變得更難以閱讀和維護。如果想要在同一個批處理中多次使用派生表,此問題會變得更加嚴重,因為隨后必須復制和粘貼派生表才能重復使用它。
 1 SELECT 
 2     e.EmployeeID, oe.NumOrders, oe.MaxDate,  e.ReportsTo AS ManagerID, 
 3     om.NumOrders, om.MaxDate
 4 FROM 
 5     Employees AS e
 6     INNER JOIN 
 7         (SELECT EmployeeID, COUNT(*), MAX(OrderDate)
 8          FROM Orders
 9          GROUP BY EmployeeID) AS oe(EmployeeID, NumOrders, MaxDate)
10         ON e.EmployeeID = oe.EmployeeID
11     LEFT JOIN 
12         (SELECT EmployeeID, COUNT(*), MAX(OrderDate)
13          FROM Orders
14          GROUP BY EmployeeID) AS om(EmployeeID, NumOrders, MaxDate)
15         ON e.ReportsTo = om.EmployeeID
View Code

  

  CTE 非常適用於此類情形,因為它提升了 T-SQL 的可讀性(就像視圖一樣),而且能在同一個批處理后緊跟的查詢中多次使用。當然,超出該范圍它就不適用了。另外,CTE 是語言級別的構造,也就是說 SQL Server 不會在內部創建臨時表或虛擬表。每次在緊隨其后的查詢中引用 CTE 的底層查詢時都會調用它。

因此,同樣的情形也能用 CTE 來編寫,如圖 3 所示。EmpOrdersCTE 收集聚合數據,然后在緊隨 CTE 之后的查詢中使用該數據。使用 CTE 之后,圖 3 中的代碼令查詢變得非常易讀(就像視圖一樣),而且並沒有創建系統對象來存儲元數據。
 1 ;WITH EmpOrdersCTE (EmployeeID, NumOrders, MaxDate) AS
 2 (
 3   SELECT EmployeeID, COUNT(*), MAX(OrderDate)
 4   FROM Orders
 5   GROUP BY EmployeeID
 6 )
 7 
 8 SELECT 
 9     e.EmployeeID,  oe.NumOrders, oe.MaxDate,
10     e.ReportsTo AS ManagerID,  om.NumOrders, om.MaxDate
11 FROM 
12     Employees AS e
13     INNER JOIN EmpOrdersCTE oe ON e.EmployeeID = oe.EmployeeID
14     LEFT JOIN EmpOrdersCTE om ON e.ReportsTo = om.EmployeeID
View Code
 
CTE 的結構
  現在我將用一個簡單的 CTE 為例來演示如何構造 CTE。CTE 以 WITH 關鍵字開始。然而,如果 CTE 不是批處理中的第一個語句,則必須在 WITH 關鍵字前添加一個分號。作為最佳做法,我傾向於在所有的 CTE 之前都加上一個分號作為前綴,我發現這種一致的方式比起必須牢記是否需要添加分號來,要容易得多。
WITH 關鍵字后面是 CTE 的名稱,接着是一個列別名的可選列表。列別名對應於 CTE 內的 SELECT 語句返回的列。可選列別名的后面是 AS 關鍵字,這是必需的。AS 關鍵字后面是用括號括起來、定義 CTE 的查詢表達式。
請看這個示例:
1 ;WITH myCTE (CustID, Co) AS
2 (
3   SELECT CustomerID, CompanyName FROM Customers
4 )
5 SELECT CustID, Co FROM myCTE
View Code
CustomerID 和 CompanyName 列的別名為 CustID 和 Co。接着跟隨 CTE 其后的是通過列別名引用 CTE 的 SELECT 語句。
 
理解 CTE
  在設計 CTE 之前,必須理解它的工作原理和遵循的規則。本章節描述了 CTE 的適用情況,以及在 CTE 內什么是可以使用的,什么是不可以使用的。對於初學者來說,可以在 T-SQL 批處理、用戶自定義函數、存儲過程、觸發器或視圖中創建並使用 CTE。
CTE 僅能被緊隨其后的語句所引用。這意味着如果要使用 CTE,則必須緊隨 T-SQL 批處理中的 CTE 之后編寫引用 CTE 的查詢。例如,以下批處理會產生錯誤:
1 ;WITH myCTE (CustID, Co) AS
2 (
3   SELECT CustomerID, CompanyName FROM Customers
4 )
5 SELECT CompanyName FROM Customers WHERE CustomerID = 'ALFKI'
6 SELECT CustID, Co FROM myCTE
View Code
在本段代碼中,myCTE 僅供緊隨其后的第一個查詢使用。當第二個查詢引用 myCTE 時,CTE 不在范圍之內,並且引發異常(對象名“myCTE”無效)。
另請注意,因為 CTE 預期由另一個可能隨之肯定要重新處理數據的查詢引用,所以 CTE 的查詢不能含有 ORDER 和 COMPUTE 之類的語句。然而,諸如 FOR XML 的復雜語句仍可用來定義和運行 CTE。例如,您可以使用 FOR XML 子句來查詢 CTE 並返回其結果,如下所示。 
1 ;WITH myCTE AS
2 (
3   SELECT c.CustomerID, c.CompanyName, o.OrderID, o.OrderDate
4   FROM Customers c 
5   INNER JOIN Orders o ON c.CustomerID = o.CustomerID
6 )
7 
8 SELECT CustomerID, CompanyName, OrderID, OrderDate 
9 FROM myCTE FOR XML AUTO
View Code

一旦定義了 CTE,跟隨其后的首個查詢便能多次引用它。這一功能在某個查詢需要多次引用 CTE 時尤為有用。圖 3 中的代碼示例演示了查詢如何引用 EmpOrdersCTE 兩次,以便能獲取員工和主管的信息。當需要多次引用同一行集時,這非常有用;引用 CTE 兩次比復制該查詢要簡單得多。

  CTE 並不一定由 SELECT 語句使用;任何引用 CTE 所生成行集的語句都可使用它。這意味着 CTE 后面可以跟隨使用 CTE 的 SELECT、INSERT、UPDATE 或 DELETE 語句。您也可以在使用 CTE 的查詢中使用只進和快照光標。
此外,CTE 后面也可以跟隨另一個 CTE。在想要把中間結果聚集到行集時,可使用這種技術從其他 CTE 構建 CTE。當創建從其他 CTE 構建的 CTE 時,請用逗號分隔 CTE 的定義。
圖 4 所示的示例定義了 EmpOrdersCTE,它收集了一個員工列表和每個員工的訂單總數。第二個 CTE 的名稱是 MinMaxOrdersCTE,它查詢第一個 EmpOrdersCTE 並在行集上執行聚合函數來確定員工訂單的平均數、最小數和最大數。
;WITH 
EmpOrdersCTE (EmployeeID, NumOrders)
AS
(
  SELECT EmployeeID, COUNT(*)
  FROM Orders
  GROUP BY EmployeeID
),
MinMaxOrdersCTE (Mn, Mx, Diff)
AS
(
  SELECT MIN(NumOrders), MAX(NumOrders), AVG(NumOrders)
  FROM EmpOrdersCTE
)
SELECT Mn, Mx,    Diff
FROM MinMaxOrdersCTE
View Code
通過用逗號來分隔,在 WITH 關鍵字后面可以定義多個 CTE。每個 CTE 都可以被緊隨其后的 CTE 所引用,形成層接的構建關系。CTE 定義后面的數據操作語言 (DML) 語句也可引用 WITH 子句中定義的任何 CTE。

 

遞歸規則
  CTE 還可用於實現遞歸算法。在需要編寫調用其本身的算法時,遞歸邏輯很有用——這通常用來遍歷一組嵌套的數據。編寫遞歸邏輯可能很復雜,特別是使用 T-SQL 之類的語言的時候。然而,這正是 CTE 旨在解決的特別問題之一。創建遞歸 CTE 的基本公式如下所示:
  1. 創建一個返回頂層(這是定位點成員)的查詢。
  2. 編寫一個遞歸查詢(這是遞歸成員)。
  3. 通過 UNION 將第一個查詢與遞歸查詢結合起來。
  4. 確保存在沒有行會被返回的情況(這是終止檢查)。
您的遞歸 CTE 如下所示:
1 ;WITH myRecursiveCTE(col1, col2, ... coln) AS
2 ( 
3   -- 定位點成員 查詢
4   UNION ALL
5   -- 遞歸成員 查詢來自CTE的自身數據
6 )
View Code
  當編寫不涉及 CTE 的自定義遞歸過程時,必須包含一個顯式終止子句這個終止子句負責確保遞歸算法最后將終止,並彈出了遞歸調用堆棧。若無此子句,您的代碼最終將無限循環下去。
  CTE 可從兩個方面幫助處理終止子句。首先是一個隱式終止子句,當遞歸成員返回零記錄時出現。此時,遞歸成員查詢不會遞歸調用 CTE,取而代之的彈出了遞歸調用堆棧。其次是能顯式設置 MAXRECURSION 層
MAXRECURSION 層可以在含有 CTE 的批處理中或通過服務器端設置(服務器范圍的設置默認值為 100,除非您更改它)顯式設置。這個設置限制了 CTE 可遞歸調用其本身的次數。如果達到限制次數,則將引發異常。設置 MAXRECURSION 層的語法是在 SELECT 語句中的 CTE 后面使用 OPTION 子句,如下所示:
-- DEFINE YOUR CTE HERE
SELECT * FROM EmpCTE
OPTION (MAXRECURSION 7) 
在設計遞歸 CTE 時,還有一些其他規則也得銘記在心。遞歸 CTE 必須包含定位點成員和遞歸成員。這兩種成員必須擁有相同數量的列,而且同屬於這兩種成員的列必須具有匹配的數據類型。遞歸成員只能引用 CTE 一次,並且成員不能使用下列子句或關鍵字:
  • SELECT DISTINCT
  • GROUP BY
  • HAVING
  • TOP
  • LEFT/RIGHT OUTER JOIN

 

遞歸偏移
  在數據和行集方面,遞歸用於解決您需要在不同情況下針對同一組數據重復執行同一邏輯時所遇到的問題。例如,假設您需要找出所有銷售人員以及他們的上級,然后以分層順序返回數據。圖 5 演示了一個使用 CTE 的解決方案,該方案通過遞歸來收集銷售副總裁下屬的員工的列表。
 1 ;WITH EmpCTE(EmployeeID, EmployeeFirstName,
 2  EmployeeLastName, MgrID, SalesLevel)
 3 AS
 4 ( 
 5   -- 定位點成員
 6   SELECT EmployeeID, FirstName, LastName, ReportsTo, 0
 7   FROM Employees
 8   WHERE EmployeeID = 2 -- Start with the VP of Sales
 9 
10   UNION ALL
11   
12   -- 遞歸成員
13   SELECT 
14     e.EmployeeID, e.FirstName, e.LastName, e.ReportsTo, m.SalesLevel+1
15   FROM 
16     Employees AS e
17     INNER JOIN EmpCTE m ON e.ReportsTo = m.EmployeeID
18 )
19 
20  -- 使用CTE
21 SELECT EmployeeID, EmployeeFirstName, EmployeeLastName, 
22        MgrID, SalesLevel  
23 FROM EmpCTE
View Code
除了少數其他方面,圖 5 中所示的遞歸 CTE 看起來很像標准 CTE。其中標准 CTE 包含一個定義行集的查詢,遞歸 CTE 定義兩個查詢定義。第一個查詢定義(即定位點成員)定義了將在調用 CTE 時執行的查詢。第二個查詢定義,即遞歸成員,定義了一個返回與定位點成員相同的列和數據類型的查詢。遞歸成員還檢索接下來將被用於遞歸回調到 CTE 的值。查詢的結果通過 UNION 語句結合在一起。
圖 5 中的 EmpCTE 顯示了收集銷售副總裁的員工記錄的定位點成員 (EmployeeID = 2)。定位點成員查詢的最后一列返回 0 值,這表示分層順序的第 0 層,也就是最頂層。遞歸成員查詢收集向其各自上級匯報的員工的列表。這通過聯接 Employees 表和 EmpCTE 來實現。
從遞歸成員中可以檢索相同的列,但是 SalesLevel 列的計算方式是:取當前員工的主管,收集主管的 SalesLevel,然后在其基礎上增加 1。表達式 m.SalesLevel+1 為所有直接向銷售副總裁匯報的員工(從定位點成員中檢索到)的 SalesLevel 賦值 1。然后,所有向那些員工匯報的員工的 SalesLevel 值變為 2。對隨后銷售機構層次結構中的每一后續層次,其 SalesLevel 都以此方式遞增。
 
補充: 目前僅有Sql Server 2005、2008、2012、Oracle11g、IBM DB2和PostegreSQL8.4支持CTE;
           MySQL、SQLite和Infomix暫時不支持。
 
結束語
  比起那些在查詢中使用復雜的派生表或引用那些在 T-SQL 批處理外部定義的視圖的方案,CTE 使得編寫 T-SQL 更具可讀性。此外,CTE 還為解決使用遞歸算法的過程中遇到的難題提供了一個更先進的工具。不管您使用的是非遞歸 CTE 還是遞歸 CTE,您都會發現 CTE 可以幫您應對許多常見開發情況,並且可在不損害性能的情況下提升可讀性。
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM