[SQL] CTE、Cursor、Table Variable & Temporary Table、Stored Procedure
一般資料表運算式 (Common Table Expressions)
簡單來說,就是建立一個暫存資料表給予該次查詢使用,改查詢使用後,將會自動釋放
或稱通用資料表運算式,另外 CTE 是 SQL 2005 才開始支援
語法:
WITH expression_name [ ( column_name [,...n] ) ]
AS
(
--CTE_query_definition
)
expression_name
CTE 名稱
column_name
在一般資料表運算式中,指定資料行名稱。在單一 CTE 定義內,名稱不能重複。指定的資料行名稱數目必須符合CTE_query_definition 的結果集資料行數目。
CTE_query_definition
不能使用下列子句:
-
COMPUTE 或 COMPUTE BY
-
ORDER BY (除非指定了 TOP 子句)
-
INTO
-
含有查詢提示的 OPTION 子句
-
FOR XML
-
FOR BROWSE
範例
WITH tmpOrders AS
(
SELECT EmployeeID, COUNT(*) as OrderCounts
FROM Orders
GROUP BY EmployeeID
)
SELECT* FROM tmpOrders;
遞迴 CTE
其實我是覺得支援遞迴查詢這部份真的便利許多,雖然還是有些限制
錨點部分為遞迴查詢的初始資料,使用 UNION ALL 和遞迴部分結合
遞迴部分為需要反覆執行的部分
語法:
WITH expression_name AS [(ColName[,...n])]
(
--CTE_query_definition
-- 錨點部分
UNION ALL
-- 遞迴部分
)
使用規定可以參考 MSDN 比較需要注意的是不允許使用的項目
遞迴成員的 CTE_query_definition 不允許使用下列項目:
-
SELECT DISTINCT
-
GROUP BY
-
HAVING
-
純量彙總
-
TOP
-
LEFT、RIGHT、OUTER JOIN (允許 INNER JOIN)
-
子查詢
-
適用於 CTE_query_definition 內 CTE 之遞迴參考的提示。
若遞迴成員查詢定義,對父資料行與子資料行傳回相同的值,就會建立無限迴圈。
若要防止無限迴圈,可以透過 OPTION 子句中
設定 MAXRECURSION 屬性( 0 和 32767 之間的值),來限制遞迴層級數目。
資料庫的遞迴預設值是 100。 指定 0 時,不限制遞迴層級。
此外 WITH 這個關鍵字用在許多表達式中,所以若一個 CTE 表達式的第一行不是由 WITH 開始,就必須加上分號(semicolon)來斷行。
範例
-- 遞迴 Recursive CTE's
;WITH EmpCTE(EmployeeID, EmployeeFirstName, EmployeeLastName, MgrID, SalesLevel)
AS
(
-- 錨點部分 Anchor Member (AM)
SELECT EmployeeID, FirstName, LastName, ReportsTo, 0
FROM Employees
WHERE EmployeeID = 5
UNION ALL
-- 遞迴部分 Recursive Member (RM)
SELECT
e.EmployeeID, e.FirstName, e.LastName, e.ReportsTo, m.SalesLevel+1
FROM
Employees AS e
INNER JOIN EmpCTE m ON e.ReportsTo = m.EmployeeID
)
-- 設定 MAXRECURSION 遞迴層次為 5
SELECT * FROM EmpCTE OPTION (MAXRECURSION 5)
參考資料
MSDN_WITH common_table_expression
TechNet_Common Table Expressions
VITO の SQL 學習筆記 介紹的蠻詳細,大力推薦
Cursor
詳細說明參照德瑞克大,這邊主要紀錄一下 DECLARE CURSOR 的使用
不過盡量避免使用 Cursor,原因的話RiCo技術農場 與德瑞克大都有測試與說明
範例
-- 建立一個暫存表 等下查看
CREATE Table #tmp
(
id int IDENTITY(1,1) NOT NULL,
EpTitle nvarchar(30)
);
-- 建立Cursor 並啟用了效能最佳化的 FORWARD_ONLY、READ_ONLY 資料指標
DECLARE tmpCursor CURSOR FAST_FORWARD FOR
(select EmployeeID,Title
from Employees);
-- 宣告 Cursor 等下使用的變數
DECLARE @eid int,@title nvarchar(30);
OPEN tmpCursor--開啟Cursor
--將第一筆資料填入變數
FETCH NEXT FROM tmpCursor INTO @eid,@title
-- @@FETCH_STATUS 傳回值
-- 0 FETCH 陳述式成功。
-- -1 FETCH 陳述式失敗,或資料列已超出結果集。
-- -2 遺漏提取的資料列。
WHILE @@FETCH_STATUS = 0
BEGIN
-- 進行想要處理的動作
INSERT INTO #tmp (EpTitle) VALUES(@title);
-- 將下一筆資料填入變數
FETCH NEXT FROM tmpCursor INTO @eid,@title
END
--關閉Cursor
CLOSE tmpCursor
--釋放Cursor
DEALLOCATE tmpCursor
-- 看一下剛剛處理後的結果
select * from #tmp
--DROP TABLE #tmp
輸出結果
參考資料
德瑞克_初探Cursors(資料指標) 與資料列集(Rowsets)
Table Variable & Temporary Table
暫存表(Temporary Tables)
CREATE TABLE #TableName
(
columnName bigint
)
暫存表名稱使用 # 開頭,後面接上自訂命名,當連線關閉或中斷後,此暫存表會刪除。
暫存表建立後會存在於 tempdb 資料庫
除了 Create 暫存表外,還可以直接使用 INTO 至暫存表,
資料庫會自動按照資料來源 Table 的欄位建立至暫存表。
例如:
SELECT * INTO #temp FROM Northwind.dbo.Employees
全域暫存表(Global Temporary Tables)
暫存表名稱使用 ## 開頭,表示這是全域暫存表,可以被所有連線使用者使用
例如:
-- 全域暫存表 GTT
SELECT * INTO ##temp FROM Northwind.dbo.Employees
-- 暫存表 TT
SELECT * INTO #temp FROM Northwind.dbo.Employees
產生兩個暫存資料表
在同一連線下 SELECT 資料,暫存表皆可讀取
開一個新連線,一般的暫存表僅供建立該表的連線使用
在新連線中,GTT 可以使用
至於全域暫存表的釋放與暫存表相同,是當建立此全域暫存表的連線中斷後,才會釋放
表格變數(Table Variables)
語法
-- 表格變數 Tabel Variable
-- 變數名採用 @ 開頭
DECLARE @TableName TABLE
(
-- 欄位名稱 資料格式
columnName bigint
)
引用黑大分析的優缺點
優點
-
用於 Stored Procedure 時,不需要每次Recompile,速度較快
-
比照 Local Variable,Scope 定義明確,在不需要時就立刻會被清除
-
Transaction Lock 存在時間短,也不影響實體資料庫,資料的更新操作更有效率
缺點:
-
只支援 PK 及 UNIQUE KEY,不能建立 Non-Cluster Index,也沒有資料分佈統計機制,不利於大量資料或複雜的查詢。
-
基於 Local Variable 的限制,使用 sp_executesql 時無法存取
-
不支援 SELECT INTO、INSERT EXEC
結論
當暫存的資料筆數較小時,可使用表格變數,如果資料筆數多可使用暫存表
參考資料
黑大的KB-SQL 2000的資料庫變數(Table Variable)
亂馬客_Table-Variable in Transaction
SQL SERVER – Difference TempTable and Table Variable – TempTable in Memory a Myth
Yet Another Temp Tables Vs Table Variables Article
Temporary Tables vs. Table Variables and Their Effect on SQL Server Performance
Stored Procedure
或稱預存程序,當遇到一些冗長的查詢語句,建議可以使用預存程序來處理,
降低網路的傳輸量。
預存程序資料傳回方式:
-
使用 SELECT 以表格方式傳回。
-
設定 Output Parameter 以參數方式傳回。
-
使用 RETURN 傳回 1 個整數型別的資料。
列出 WHILE 迴圈的範例
此方式是以資料筆數的 Count 來進行迴圈設計
--CREATE PROCEDURE dbo.SPRowIndexSample
--AS
-- 宣告 table 變數,此 @temp 資料為迴圈逐筆進行之資料
DECLARE @temp TABLE
(
ID INT , --ROW序號
OrderID INT,
Employee NVARCHAR(35)
);
-- @OutTable 為輸出暫存表
DECLARE @OutTable Table
(
OrderID INT,
Employee NVARCHAR(35)
);
-- 把資料塞到 @temp 變數裡面
INSERT INTO @temp (ID
, OrderID
, Employee)
(SELECT
ROW_NUMBER() OVER (ORDER BY OrderID) AS ID,
OrderID,
e.FirstName + '-' + e.LastName AS 'Employee'
FROM Orders o
INNER JOIN Employees e
ON o.EmployeeID = e.EmployeeID
WHERE o.OrderDate > '1998/5/1'
)
-- 宣告變數
DECLARE
-- @tmep 資料總筆數
@tempCount INT = ( SELECT
COUNT(ID)
FROM @temp)
-- while 迴圈的逐筆資料 index
, @rowIndex int = 1
, @OrderId int
, @Employee nvarchar(35);
WHILE @rowIndex <= @tempCount
BEGIN
--透過 @tempCount ID 取得欲使用的欄位值
SELECT
@OrderId = OrderID,
@Employee = Employee
FROM @temp
WHERE ID = @rowIndex
-- 想要處理的動作 Start --
INSERT INTO @OutTable (OrderID
, Employee)
VALUES (@OrderId, @Employee)
-- 想要處理的動作 End --
-- 設定 while 條件,跑下一筆
SET @rowIndex = @rowIndex + 1
END
-- 取出
SELECT
OrderID,
Employee
FROM @OutTable
--GO
另一方式是使用字串分割方式進行迴圈設計
--CREATE PROCEDURE dbo.SPStrSplitSample
--AS
-- 宣告字串變數,@strID 為資料來源的 OrderID 組合字串
-- @CurId 為迴圈進行的當下 OrderID
DECLARE @strOID NVARCHAR(2000), @CurOId NVARCHAR(10);
SET @strOID = '';
SELECT @strOID = @strOID + convert(VARCHAR, OrderID) + ','
FROM
Orders
WHERE
OrderDate > '1998/5/1';
-- @OutTable 為輸出暫存表
DECLARE @OutTable Table
(
OrderID INT,
Employee NVARCHAR(35)
);
-- 透過字串長度進行迴圈
WHILE LEN(@strOID) > 0
BEGIN
-- 擷取 OrderID
SET @CurOId = substring(@strOID, 1, charindex(',', @strOID, 1) - 1);
-- 想要處理的動作 Start --
INSERT INTO @OutTable (OrderID
, Employee)
SELECT OrderID
, e.FirstName + '-' + e.LastName AS 'Employee'
FROM
Orders o
INNER JOIN Employees e
ON o.EmployeeID = e.EmployeeID
WHERE
OrderID = @CurOId;
-- 想要處理的動作 End --
-- 剃除完成的 OrederID,讓迴圈跑下一筆
SET @strOID = right(@strOID, len(@strOID) - charindex(',', @strOID, 1));
END
-- 取出
SELECT OrderID
, Employee
FROM
@OutTable
--GO
不過這方式在字串變數長度小於要進迴圈的資料長度時,很容易會因為資料截斷產生誤差
接收 SP 的回傳值與回傳參數,如果 SP 使用表格方式輸出結果,那就要用表格參數或是暫存表去接
-- 接收預存程序回傳值
CREATE PROCEDURE SPDemo
(
@Param1 int
,@Param2 varchar(100) OUTPUT -- 回傳參數設定 OUTPUT
)
AS
IF ISNULL(@Param1,0)> 5
BEGIN
SET @Param2 = 'the value is greather then 5 '
END
ELSE
BEGIN
SET @Param2 = 'the value is less then or equal to 5 '
END
-- 回傳值
RETURN @Param1
GO
--==============================================
-- @OutputParameter 回傳參數
-- @ReturnValue 回傳值(僅能整數型別)
DECLARE @OutputParameter varchar(100)
,@ReturnValue int
EXEC @ReturnValue = SPDemo 8, @OutputParameter OUTPUT
PRINT @ReturnValue
PRINT @OutputParameter
參考資料
TechNet_Stored Procedures 事件類別目錄
walter 心得筆記_接收 Store Procedure 的傳回值
RiCo技術農場_撰寫Stored Procedure小細節
備註
- 範例皆使用 SQL SERVER 2008 R2 做為測試
-
範例資料庫為北風資料庫