Cursor 游標 (SQL Cursor)

Cursor(游標)讓你可以逐筆處理查詢結果,而不是一次取得所有資料。當需要對每一筆資料執行不同的邏輯處理時,游標就派上用場了。

游標的使用步驟

  1. DECLARE:宣告游標並定義查詢
  2. OPEN:開啟游標,執行查詢
  3. FETCH:逐筆取得資料
  4. CLOSE:關閉游標
  5. DEALLOCATE:釋放游標資源(部分資料庫需要)

SQL Server 語法

-- 宣告變數存放每筆資料
DECLARE @id INT, @name VARCHAR(50), @price DECIMAL(10,2);

-- 宣告游標
DECLARE product_cursor CURSOR FOR
SELECT id, name, price FROM products WHERE price > 100;

-- 開啟游標
OPEN product_cursor;

-- 取得第一筆資料
FETCH NEXT FROM product_cursor INTO @id, @name, @price;

-- 迴圈處理每筆資料
WHILE @@FETCH_STATUS = 0
BEGIN
    -- 處理邏輯
    PRINT 'Product: ' + @name + ', Price: ' + CAST(@price AS VARCHAR);
    
    -- 取得下一筆
    FETCH NEXT FROM product_cursor INTO @id, @name, @price;
END

-- 關閉游標
CLOSE product_cursor;

-- 釋放游標
DEALLOCATE product_cursor;

@@FETCH_STATUS 的值:

  • 0:成功取得資料
  • -1:已無資料
  • -2:資料已被刪除

MySQL 語法

MySQL 的游標只能在預存程序或函數中使用:

DELIMITER //
CREATE PROCEDURE process_products()
BEGIN
    DECLARE done INT DEFAULT FALSE;
    DECLARE v_id INT;
    DECLARE v_name VARCHAR(50);
    DECLARE v_price DECIMAL(10,2);
    
    -- 宣告游標
    DECLARE product_cursor CURSOR FOR
    SELECT id, name, price FROM products WHERE price > 100;
    
    -- 宣告 handler 處理游標結束
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
    
    -- 開啟游標
    OPEN product_cursor;
    
    -- 迴圈讀取
    read_loop: LOOP
        FETCH product_cursor INTO v_id, v_name, v_price;
        IF done THEN
            LEAVE read_loop;
        END IF;
        
        -- 處理邏輯
        SELECT CONCAT('Product: ', v_name, ', Price: ', v_price);
    END LOOP;
    
    -- 關閉游標
    CLOSE product_cursor;
END//
DELIMITER ;

-- 執行
CALL process_products();

PostgreSQL 語法

DO $$
DECLARE
    product_cursor CURSOR FOR SELECT id, name, price FROM products WHERE price > 100;
    v_id INT;
    v_name VARCHAR(50);
    v_price DECIMAL(10,2);
BEGIN
    OPEN product_cursor;
    
    LOOP
        FETCH product_cursor INTO v_id, v_name, v_price;
        EXIT WHEN NOT FOUND;
        
        -- 處理邏輯
        RAISE NOTICE 'Product: %, Price: %', v_name, v_price;
    END LOOP;
    
    CLOSE product_cursor;
END $$;

Oracle 語法

DECLARE
    CURSOR product_cursor IS
    SELECT id, name, price FROM products WHERE price > 100;
    v_id products.id%TYPE;
    v_name products.name%TYPE;
    v_price products.price%TYPE;
BEGIN
    OPEN product_cursor;
    
    LOOP
        FETCH product_cursor INTO v_id, v_name, v_price;
        EXIT WHEN product_cursor%NOTFOUND;
        
        -- 處理邏輯
        DBMS_OUTPUT.PUT_LINE('Product: ' || v_name || ', Price: ' || v_price);
    END LOOP;
    
    CLOSE product_cursor;
END;

Oracle 也支援更簡潔的 FOR 迴圈語法:

BEGIN
    FOR rec IN (SELECT id, name, price FROM products WHERE price > 100)
    LOOP
        DBMS_OUTPUT.PUT_LINE('Product: ' || rec.name || ', Price: ' || rec.price);
    END LOOP;
END;

游標的缺點與替代方案

游標有以下缺點:

  1. 效能較差:逐筆處理比集合操作慢很多
  2. 消耗資源:長時間開啟會占用記憶體和鎖定
  3. 程式碼複雜:相比單純的 SQL 語句更難維護

大多數情況下,應該優先考慮使用集合式操作(set-based operations),例如:

  • 使用 UPDATE 搭配 JOIN 批次更新
  • 使用 CTE 處理複雜邏輯
  • 使用窗口函數進行計算

只有在邏輯複雜到無法用集合式操作完成時,才考慮使用游標。