高併發 OLTP 系統中,頻繁需要判斷「某記錄是否存在」 (如主鍵/唯一鍵查詢、業務幂等性校驗、防重複提交)。**目標:最小化邏輯讀、物理讀、CPU、鎖開銷,獲得最優執行計劃。**
-- 測試表:42,667 行,item 列有唯一索引 (UK)
CREATE TABLE item_test AS SELECT * FROM item WHERE rownum <= 42667;
CREATE UNIQUE INDEX uk_item_test_item ON item_test(item);
EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'ITEM_TEST');
-- 目標值:存在
VARIABLE target VARCHAR2(20);
EXEC :target := '1B241371X0021';
SELECT * FROM item_test WHERE item = :target AND ROWNUM < 2;
-- 測試結果:1 row selected in 0.047 seconds
-- 執行計劃關鍵:COUNT STOPKEY -> INDEX UNIQUE SCAN (UK_ITEM_TEST_ITEM)
-- 優化器識別 STOPKEY,索引唯一掃描獲取 1 行即停止,不訪問表塊 (若只選擇索引列)
-- 若 SELECT * 需訪問表,則 INDEX UNIQUE SCAN -> TABLE ACCESS BY INDEX ROWID (1 row)
SELECT COUNT(*) FROM item_test WHERE item = :target;
-- 測試結果:1 row selected in 0.016 seconds
-- 執行計劃:SORT AGGREGATE -> INDEX UNIQUE SCAN (UK_ITEM_TEST_ITEM)
-- 僅掃描索引,不訪問表,**邏輯讀最少** (通常 1-2 consistent gets)
-- 返回數值 0 或 1,應用層判斷 >0 即存在
SELECT COUNT(*) FROM dual WHERE EXISTS (SELECT 1 FROM item_test WHERE item = :target);
-- 測試結果:1 row selected in 0.015 seconds
-- 執行計劃:FILTER -> INDEX UNIQUE SCAN (UK_ITEM_TEST_ITEM) -> FAST DUAL
-- 優化器將 EXISTS 轉為半連接,索引唯一掃描命中即返回,**不聚合、不排序、不訪問表**
-- 邏輯讀同 COUNT(*),但語義更清晰,可擴展為相關子查詢
| 維度 | ROWNUM < 2 | COUNT(*) | EXISTS |
|---|---|---|---|
| 語義 | 返回行數據 (含所有列) | 返回計數 (0/1) | 返回布爾邏輯 (TRUE/FALSE) |
| 優化器轉換 | STOPKEY (COUNT STOPKEY) | 聚合消除 (若唯一索引) / 半連接 | 半連接 (HASH JOIN SEMI / NESTED LOOPS SEMI) |
| 訪問路徑 | INDEX UNIQUE SCAN → TABLE ACCESS (若 SELECT *) | INDEX UNIQUE SCAN (僅索引) | INDEX UNIQUE SCAN (僅索引) |
| 邏輯讀 | 2-3 (索引 + 表) | 1-2 (僅索引) | 1-2 (僅索引) |
| 網絡傳輸 | 行數據 (含所有列) | 單個數值 | 單個數值 (經 dual 包裝) |
| 適用場景 | 需同時取回數據列 | 純存在性判斷,兼容性最好 | 純存在性判斷,語義標準,易擴展 |
| 並發鎖影響 | 若 SELECT FOR UPDATE 則加行鎖 | 無鎖 (一致性讀) | 無鎖 (一致性讀) |
-- 目標:判斷是否存在 status='ACTIVE' 的記錄
-- 非唯一索引 idx_item_test_status
-- COUNT(*) 最優:INDEX RANGE SCAN 統計行數,優化器可提前停止 (FIRST_ROWS 模式)
SELECT COUNT(*) FROM item_test WHERE status = 'ACTIVE';
-- EXISTS 同等:半連接 NESTED LOOPS SEMI,命中第一行即返回
SELECT 1 FROM dual WHERE EXISTS (SELECT 1 FROM item_test WHERE status = 'ACTIVE');
-- ROWNUM < 2 同等:STOPKEY,命中第一行即停止
SELECT * FROM item_test WHERE status = 'ACTIVE' AND ROWNUM < 2;
-- 表大、無索引、需判斷是否存在某條件
-- 並行查詢加速 (Enterprise Edition + Partitioning/Parallel 選項)
SELECT /*+ PARALLEL(4) */ COUNT(*) FROM large_table WHERE condition = :val;
-- EXISTS 同樣受益並行
SELECT 1 FROM dual WHERE EXISTS (SELECT /*+ PARALLEL(4) */ 1 FROM large_table WHERE condition = :val);
-- 1. 靜態 SQL 隱式游標 (最簡潔,編譯期綁定計劃)
DECLARE
l_exists BOOLEAN;
BEGIN
SELECT 1 INTO l_exists FROM dual WHERE EXISTS (SELECT 1 FROM item_test WHERE item = :target);
-- l_exists 為 TRUE/FALSE
IF l_exists THEN ... END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN -- 不存在
NULL;
END;
-- 2. 顯式游標 + LIMIT 1 (可控獲取)
DECLARE
CURSOR c IS SELECT 1 FROM item_test WHERE item = :target;
l_dummy NUMBER;
BEGIN
OPEN c;
FETCH c INTO l_dummy;
IF c%FOUND THEN ... END IF;
CLOSE c;
END;
-- 3. 避免:SELECT COUNT(*) INTO l_cnt ... IF l_cnt > 0 (多一次聚合運算)
-- 分區表按日期分區,查詢特定分區
SELECT 1 FROM dual WHERE EXISTS (
SELECT 1 FROM sales PARTITION (p_2024_01) WHERE cust_id = :cid
);
-- 分區剪枝 + 索引唯一掃描,極致性能