日誌表 LOG 約 300 萬筆,主鍵 LOGTIME。需求:統計「近 2 小時內使用過系統的使用者,在近 3 天內的日誌筆數」。
WITH result1 AS (
SELECT DISTINCT username
FROM log
WHERE logtime >= SYSDATE - 2/24 -- 近 2 小時
)
SELECT log.username, COUNT(1) AS times
FROM log
INNER JOIN result1 ON log.username = result1.username
WHERE logtime >= SYSDATE - 3 -- 近 3 天
GROUP BY log.username;
問題:優化器選擇 NESTED LOOPS,以小結果集 result1 驅動大表 LOG 全表掃描 300 萬 × 使用者數,極慢。
SELECT /*+ USE_HASH(log) */ log.username, COUNT(1) AS times
FROM log
INNER JOIN result1 ON log.username = result1.username
WHERE logtime >= SYSDATE - 3
GROUP BY log.username;
HASH JOIN 減少隨機 I/O,但仍需掃描 3 天完整大表再聯接。
觀察發現:直接查詢 3 天聚合結果極快 (全表掃描一次即得)。瓶頸在於「先找 2 小時使用者,再回大表聯接」。反向思維:先對大表聚合成小表,再與小結果集聯接。
WITH
result1 AS ( -- 近 2 小時活躍使用者 (小)
SELECT DISTINCT username
FROM log
WHERE logtime >= SYSDATE - 2/24
),
result2 AS ( -- 近 3 天各使用者筆數 (小,聚合後)
SELECT username, COUNT(1) AS times
FROM log
WHERE logtime >= SYSDATE - 3
GROUP BY username
)
SELECT result2.username, result2.times
FROM result1
INNER JOIN result2 ON result1.username = result2.username;
SELECT /*+ USE_HASH(result1) */ result2.username, result2.times
FROM result1
INNER JOIN result2 ON result1.username = result2.username;
讓較小的 result1 當 Build Input,減少記憶體佔用。
| 版本 | 邏輯 | 耗時 | 關鍵操作 |
|---|---|---|---|
| 初版 | CTE 過濾 → NESTED LOOPS 聯接大表 | > 3600 秒 | 大表隨機存取 |
| HASH JOIN 強制 | CTE 過濾 → HASH JOIN 大表 | ~1800 秒 | 大表全掃描 + 雜湊 |
| 雙 CTE 先聚合 | 大表聚合成小表 → 小表聯接 | 112 秒 | 兩次大表循序掃描 + 小表聯接 |
| 指定 HASH Build | 同上 + 優化雜湊建表端 | ~100 秒 | 記憶體效率最佳化 |
「大表變小表,小表再聯接」是大數據量查詢優化的核心模式。透過 CTE/暫存表/物化視圖將大表預先縮減,再進行精準聯接,往往能獲得數量級效能提升。本案從 1 小時降至 112 秒,證明了重構查詢邏輯優於單純 Hint 強制的價值。