游標 是資料庫引擎在記憶體中建立的**結果集指標**,允許逐列處理查詢結果。SQL 本質為**集合導向語言** (Set-based),單一陳述式處理整個集合;游標將集合操作**序列化**為逐列迴圈,違背關聯模型設計初衷。
| 維度 | 類型 | 特性 |
|---|---|---|
| 游標範圍 | SELECT 游標 | 最常用,對應 DECLARE CURSOR FOR SELECT ... |
| INSERT 游標 | 對應 DECLARE CURSOR FOR INSERT ... (ESQL/C 等嵌入式 SQL),T-SQL 不直接支援。 | |
| UPDATE/DELETE 游標 | 定位更新/刪除:WHERE CURRENT OF cursor_name,需 FOR UPDATE 宣告。 | |
| 資料庫游標 vs 語言游標 | 資料庫游標 (Server-side,T-SQL DECLARE CURSOR) vs 語言游標 (Client-side,ODBC/ADO/ADO.NET SqlDataReader 等)。 | |
| 捲動能力 | FORWARD_ONLY (預設) | 僅能 FETCH NEXT 向前,效能最好、資源最少。 |
| SCROLL | 支援 FETCH FIRST/LAST/PRIOR/NEXT/ABSOLUTE n/RELATIVE n,需臨時表儲存結果集。 | |
| STATIC | 建立時複製結果集至 tempdb,隔離基礎資料變更,唯讀。 | |
| KEYSET | 固定鍵值集合,非鍵值欄位變更可見,鍵值變更不可見 (跳過)。 | |
| DYNAMIC | 完全反映基礎資料所有變更,開銷最大。 | |
| 並行控制 | READ_ONLY | 不可透過游標更新,允許最佳化器選擇較輕量計畫。 |
| OPTIMISTIC | 更新時檢查時間戳/版本,衝突則報錯。 | |
| SCROLL_LOCKS (FOR UPDATE) | 讀取時加 U/X 鎖,保證更新成功,並發度最低。 | |
| 區域性 | LOCAL (預設) / GLOBAL | LOCAL 僅批次/程序內可見,GLOBAL 連線層級可見 (慎用)。 |
DECLARE cursor_name CURSOR
[ LOCAL | GLOBAL ]
[ FORWARD_ONLY | SCROLL ]
[ STATIC | KEYSET | DYNAMIC | FAST_FORWARD ] -- FAST_FORWARD = FORWARD_ONLY + READ_ONLY + 效能優化
[ READ_ONLY | SCROLL_LOCKS | OPTIMISTIC ]
[ TYPE_WARNING ]
FOR select_statement
[ FOR UPDATE [ OF column_list ] ]
OPEN cursor_name
FETCH [ NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n ]
FROM cursor_name
INTO @Var1, @Var2, ...
WHILE @@FETCH_STATUS = 0
BEGIN
-- 逐列處理邏輯
FETCH NEXT FROM cursor_name INTO @Var1, @Var2, ...
END
CLOSE cursor_name
DEALLOCATE cursor_name
SCROLL_LOCKS 長時間持有 U/X 鎖;STATIC 複製至 tempdb 消耗 I/O。
-- ❌ 游標寫法:為每位客戶更新最新訂單日期 (假設 100 萬客戶)
DECLARE c CURSOR LOCAL FAST_FORWARD FOR SELECT CustomerID FROM Customers;
OPEN c; FETCH NEXT FROM c INTO @CustID;
WHILE @@FETCH_STATUS = 0
BEGIN
UPDATE Customers SET LastOrderDate = (SELECT MAX(OrderDate) FROM Orders WHERE CustomerID = @CustID)
WHERE CustomerID = @CustID;
FETCH NEXT FROM c INTO @CustID;
END
CLOSE c; DEALLOCATE c;
-- 耗時:~45 分鐘 (含鎖爭用、計畫編譯、交易記錄)
-- ✅ 集合寫法:單一 UPDATE + JOIN/子查詢
UPDATE c
SET LastOrderDate = o.MaxDate
FROM Customers c
JOIN (SELECT CustomerID, MAX(OrderDate) AS MaxDate FROM Orders GROUP BY CustomerID) o
ON c.CustomerID = o.CustomerID;
-- 耗時:~3 秒 (平行掃描、單一計畫、最小記錄)
-- ✅ 替代方案:MERGE (同步來源/目標)
MERGE INTO Customers c
USING (SELECT CustomerID, MAX(OrderDate) AS MaxDate FROM Orders GROUP BY CustomerID) o
ON c.CustomerID = o.CustomerID
WHEN MATCHED THEN UPDATE SET LastOrderDate = o.MaxDate;
EXEC sp_executesql (如依表名動態截斷/重建索引)。即使必須用游標:**盡量用FAST_FORWARD(唯讀、僅向前)、READ_ONLY、避免SCROLL_LOCKS、批次大小控制、在應用層 (C#/PowerShell) 用SqlDataReader替代伺服器端游標。**
| 場景 | 集合/現代替代 | 關鍵語法/工具 |
|---|---|---|
| 逐列更新/刪除 | 單一 UPDATE/DELETE + JOIN/CTE/APPLY | CROSS APPLY, OUTER APPLY, MERGE |
| 逐列插入 | INSERT ... SELECT / 表值建構子 | VALUES (...), (...), ..., OPENJSON 解析陣列 |
| 逐列計算彙總 | 窗格函數 | SUM() OVER (PARTITION BY ...), ROW_NUMBER(), LAG/LEAD |
| 逐列動態 SQL | 參數化批次 / CLR / 應用層平行 | sp_executesql + 表值參數 (TVP), SqlBulkCopy, Parallel.ForEach |
| ETL 轉換 | SSIS / Azure Data Factory / PolyBase | 資料流元件、對應資料流、T-SQL 推算下推 |
**游標是「最後手段」,而非「首選工具」。** 具備集合思維、熟練窗格函數、APPLY、MERGE、TVP、SSIS,99% 游標場景可重構為高效能集合操作。下次寫 DECLARE CURSOR 前,問自己:**「這能不能用單一 SELECT/UPDATE/DELETE/INSERT 解決?」** 答案幾乎總是肯定的。