iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0

前幾天我們看到,一段 SQL 會先被轉換成 Logical Plan,再交給 Physical Planner 產生實際執行計畫。

但在這兩個階段之間,還有一個非常重要的角色:

Query Optimizer

它的工作不是改變查詢結果,而是:

在維持結果正確的前提下,找出成本較低的執行方式。


為什麼需要 Query Optimizer?

假設我們有以下查詢:

SELECT city,
       SUM(amount) AS total_amount
FROM orders
WHERE is_member = true
GROUP BY city;

最直接的做法可能是:

讀取所有欄位
    ↓
讀取所有資料列
    ↓
進行篩選
    ↓
分組與加總

但這種方式可能讀取了許多不必要的資料。

實際上,這段查詢只需要:

city
amount
is_member

而且應該盡早套用:

WHERE is_member = true

讓不符合條件的資料不要繼續往下傳遞。

因此,Optimizer 可能將計畫調整成:

只讀取需要的欄位
    ↓
先篩選會員資料
    ↓
再進行分組與加總

結果相同,但需要處理的資料量可能更少。


Optimizer 的核心原則

Query Optimizer 通常需要同時滿足兩個條件。

結果必須正確

最佳化前後的查詢,應該產生相同結果:

Original Plan
      ↓
Optimizer
      ↓
Optimized Plan

最佳化不能改變 SQL 的語意。

例如:

WHERE amount > 100 AND is_member = true

不能被錯誤改寫成:

WHERE amount > 100 OR is_member = true

因為兩者會產生不同結果。

執行成本要更低

在結果不變的前提下,Optimizer 會嘗試降低:

  • 需要讀取的資料量
  • 需要處理的欄位數
  • 記憶體使用量
  • 網路或資料交換量
  • CPU 運算量
  • Join 與 Aggregate 的成本

所以,Optimizer 不是單純把 SQL 改寫得更漂亮,而是在尋找更有效率的資料處理方式。


DataFusion 的 Optimizer 在哪裡?

整體流程可以表示成:

SQL
 ↓
Logical Plan
 ↓
Logical Optimizer
 ↓
Optimized Logical Plan
 ↓
Physical Planner
 ↓
Initial Physical Plan
 ↓
Physical Optimizer
 ↓
Execution

DataFusion Query Optimization Flow

圖 1:DataFusion 從 Logical Plan 開始,經過 Logical Optimizer、Physical Planner 與 Physical Optimizer,最後交給 Execution Engine 執行。

DataFusion 的最佳化大致可以分成兩個階段:

Logical Optimizer
最佳化邏輯計畫

Physical Optimizer
最佳化實際執行計畫

Logical Optimizer 主要處理查詢結構與運算式;Physical Optimizer 則會處理分區、批次合併與執行算子等細節。


Logical Optimizer 與 Physical Optimizer

Logical Optimizer

Logical Optimizer 會在 Logical Plan 層級進行改寫,例如:

Projection Pushdown
Filter Pushdown
Expression Simplification
移除不必要的運算

這些改寫不會指定實際使用哪個執行算子,而是先讓查詢邏輯變得更精簡。

Physical Optimizer

Physical Planner 產生 Initial Physical Plan 後,Physical Optimizer 還可以再進一步調整:

Initial Physical Plan
        ↓
Physical Optimizer
        ↓
較適合執行的 Physical Plan

例如:

  • 合併過小的資料批次
  • 調整資料分區方式
  • 選擇較適合的執行策略
  • 加入或調整執行階段算子

因此,Physical Optimizer 處理的是「實際要如何跑」的問題。


常見最佳化一:Projection Pushdown

Projection 指的是查詢需要輸出的欄位。

例如:

SELECT city, amount
FROM orders;

如果 orders 還有其他欄位:

order_id
city
amount
is_member
created_at

查詢其實不需要讀取:

order_id
is_member
created_at

Optimizer 可以將需要的欄位往資料來源方向傳遞:

原始資料表
  order_id, city, amount, is_member, created_at
        ↓
Projection Pushdown
        ↓
只讀取 city, amount

這能減少資料讀取與後續處理成本。

對於 Parquet 這類欄式格式,Projection Pushdown 特別重要,因為資料來源通常可以只讀取指定欄位,而不必載入整個檔案。


常見最佳化二:Filter Pushdown

Filter Pushdown 是將篩選條件盡量往資料來源方向移動。

例如:

SELECT city, amount
FROM orders
WHERE is_member = true;

未最佳化時,可能是:

TableScan
    ↓
Projection
    ↓
Filter

最佳化後可能變成:

TableScan + Filter
    ↓
Projection

也就是在讀取資料時,就先套用條件:

只讀取 is_member = true 的資料

這樣可以減少:

  • 傳遞到下游的資料列
  • Filter 後續需要處理的資料量
  • 記憶體使用量
  • 網路傳輸量

不過,Filter Pushdown 是否能真正發生,取決於資料來源是否支援這項能力。


常見最佳化三:Expression Simplification

Optimizer 也可能簡化運算式。

例如:

SELECT 1 + 2;

原本可以理解成:

執行時再計算 1 + 2

Optimizer 可能在規劃階段直接將它改寫成:

SELECT 3;

又例如:

WHERE true AND is_member = true

可以簡化成:

WHERE is_member = true

這類最佳化可以讓執行階段少做一些不必要的運算。


常見最佳化四:移除不必要的運算

某些查詢可能包含不會影響結果的操作。

例如:

SELECT city
FROM orders
WHERE true;

WHERE true 不會過濾任何資料,因此 Optimizer 可以將它移除。

如果某個中間欄位只在計畫早期使用,後續不再需要,Optimizer 也可以將其排除。

這些規則通常不會改變查詢結果,但可以讓計畫更簡單。


常見最佳化五:Join 與 Aggregate

當查詢包含多張表或聚合時,執行成本可能大幅增加。

例如:

SELECT c.name,
       SUM(o.amount) AS total_amount
FROM customers c
JOIN orders o
  ON c.id = o.customer_id
GROUP BY c.name;

Optimizer 或 Physical Planner 需要考慮:

  • 先 Join 還是先 Filter
  • 哪張表先讀取
  • 是否能先進行局部聚合
  • 使用哪一種 Join 演算法
  • 是否需要重新分區
  • 是否能平行處理不同 Partition

同一段 SQL 可以有不同的執行計畫:

Plan A:
先 Join,再 Filter,再 Aggregate

Plan B:
先 Filter,再 Join,再 Aggregate

如果 Filter 能大幅減少資料量,Plan B 通常可能更有效率。


Optimizer 如何估算成本?

DataFusion 可以利用資料表與欄位統計資訊,估算不同計畫的成本。

常見統計資訊包括:

資料列數量
欄位最小值
欄位最大值
NULL 數量
不同值的數量

例如,Optimizer 需要判斷以下條件的選擇性:

WHERE amount > 10000

如果統計資訊顯示大部分資料的 amount 都小於 10000,這個條件可能會排除大量資料。

如果只有少數資料符合條件,就可以預期 Filter 會大幅降低後續處理量。

這些估算可以協助系統選擇更合適的 Join、Filter 或 Aggregate 執行方式。

不過,統計資訊可能是精確值,也可能只是估計值,因此 Optimizer 的選擇不一定永遠是最佳解。


Optimizer Rule 如何運作?

DataFusion 使用一組 Optimizer Rules。

每個 Rule 通常負責一類特定的轉換,例如:

projection_push_down
filter_push_down
simplify_expressions
reduce_cross_join
eliminate_filter

每條規則會讀取目前的 Logical Plan,判斷是否能進行安全轉換:

原始 Logical Plan
        ↓
套用一條 Rule
        ↓
新的 Logical Plan

如果沒有適合的轉換,Rule 可以保留原本的計畫。

重要的是,每次改寫都必須維持相同的查詢語意。


用 EXPLAIN 觀察最佳化前後的差異

可以在 DataFusion CLI 執行:

EXPLAIN FORMAT INDENT
SELECT city,
       SUM(amount) AS total_amount
FROM orders
WHERE is_member = true
GROUP BY city;

若想觀察每一條最佳化規則套用後的計畫,可以使用:

EXPLAIN VERBOSE
SELECT city,
       SUM(amount) AS total_amount
FROM orders
WHERE is_member = true
GROUP BY city;

輸出可能包含:

initial_logical_plan
logical_plan after projection_push_down
logical_plan after filter_push_down
logical_plan after simplify_expressions
logical_plan
initial_physical_plan
physical_plan after repartition
physical_plan

這能幫助我們觀察:

原始計畫
    ↓
套用 projection pushdown
    ↓
套用 filter pushdown
    ↓
套用其他規則
    ↓
產生最佳化後的計畫

實際顯示的規則名稱與順序,會依 DataFusion 版本與查詢內容而有所不同。


Optimizer 不代表查詢一定更快

看到 Optimizer 並不代表每次查詢都一定會變快。

原因包括:

  • 統計資訊可能不完整
  • 資料來源不支援某些 Pushdown
  • 最佳化本身也有計算成本
  • 查詢計畫受資料量與資料分布影響
  • 不同 DataFusion 版本可能使用不同規則

因此,實務上通常會搭配:

EXPLAIN

觀察計畫結構,再搭配:

EXPLAIN ANALYZE

查看實際執行時間與執行統計。


今日小結

今天介紹了 Query Optimizer 的主要目的:

在不改變查詢結果的前提下,降低資料處理成本。

我們學到:

  • Logical Optimizer 會改寫 Logical Plan
  • Physical Optimizer 會調整實際執行計畫
  • Projection Pushdown 可以減少讀取欄位
  • Filter Pushdown 可以提早篩選資料
  • Expression Simplification 可以移除不必要的運算
  • Join 與 Aggregate 可能有多種執行方式
  • 統計資訊可以協助估算查詢成本
  • EXPLAIN VERBOSE 可以觀察各條 Optimizer Rule 的效果
  • 最佳化必須維持查詢語意不變

可以用以下流程總結:

原始 Logical Plan
        ↓
套用 Optimizer Rules
        ↓
Optimized Logical Plan
        ↓
Physical Planning
        ↓
Physical Optimization
        ↓
實際執行

下一篇將深入介紹 Projection Pushdown,觀察 DataFusion 如何判斷查詢真正需要哪些欄位,以及欄位裁剪如何降低資料讀取成本。

延伸閱讀


上一篇
Day 15|完整追蹤一段 SQL 的生命週期
系列文
深入 SQL 查詢引擎:30 天用 Rust 與 Apache DataFusion 解構資料處理流程16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言