前幾天我們看到,一段 SQL 會先被轉換成 Logical Plan,再交給 Physical Planner 產生實際執行計畫。
但在這兩個階段之間,還有一個非常重要的角色:
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 可能將計畫調整成:
只讀取需要的欄位
↓
先篩選會員資料
↓
再進行分組與加總
結果相同,但需要處理的資料量可能更少。
Query Optimizer 通常需要同時滿足兩個條件。
最佳化前後的查詢,應該產生相同結果:
Original Plan
↓
Optimizer
↓
Optimized Plan
最佳化不能改變 SQL 的語意。
例如:
WHERE amount > 100 AND is_member = true
不能被錯誤改寫成:
WHERE amount > 100 OR is_member = true
因為兩者會產生不同結果。
在結果不變的前提下,Optimizer 會嘗試降低:
所以,Optimizer 不是單純把 SQL 改寫得更漂亮,而是在尋找更有效率的資料處理方式。
整體流程可以表示成:
SQL
↓
Logical Plan
↓
Logical Optimizer
↓
Optimized Logical Plan
↓
Physical Planner
↓
Initial Physical Plan
↓
Physical Optimizer
↓
Execution
圖 1:DataFusion 從 Logical Plan 開始,經過 Logical Optimizer、Physical Planner 與 Physical Optimizer,最後交給 Execution Engine 執行。
DataFusion 的最佳化大致可以分成兩個階段:
Logical Optimizer
最佳化邏輯計畫
Physical Optimizer
最佳化實際執行計畫
Logical Optimizer 主要處理查詢結構與運算式;Physical Optimizer 則會處理分區、批次合併與執行算子等細節。
Logical Optimizer 會在 Logical Plan 層級進行改寫,例如:
Projection Pushdown
Filter Pushdown
Expression Simplification
移除不必要的運算
這些改寫不會指定實際使用哪個執行算子,而是先讓查詢邏輯變得更精簡。
Physical Planner 產生 Initial Physical Plan 後,Physical Optimizer 還可以再進一步調整:
Initial Physical Plan
↓
Physical Optimizer
↓
較適合執行的 Physical Plan
例如:
因此,Physical Optimizer 處理的是「實際要如何跑」的問題。
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 是將篩選條件盡量往資料來源方向移動。
例如:
SELECT city, amount
FROM orders
WHERE is_member = true;
未最佳化時,可能是:
TableScan
↓
Projection
↓
Filter
最佳化後可能變成:
TableScan + Filter
↓
Projection
也就是在讀取資料時,就先套用條件:
只讀取 is_member = true 的資料
這樣可以減少:
不過,Filter Pushdown 是否能真正發生,取決於資料來源是否支援這項能力。
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 也可以將其排除。
這些規則通常不會改變查詢結果,但可以讓計畫更簡單。
當查詢包含多張表或聚合時,執行成本可能大幅增加。
例如:
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 需要考慮:
同一段 SQL 可以有不同的執行計畫:
Plan A:
先 Join,再 Filter,再 Aggregate
Plan B:
先 Filter,再 Join,再 Aggregate
如果 Filter 能大幅減少資料量,Plan B 通常可能更有效率。
DataFusion 可以利用資料表與欄位統計資訊,估算不同計畫的成本。
常見統計資訊包括:
資料列數量
欄位最小值
欄位最大值
NULL 數量
不同值的數量
例如,Optimizer 需要判斷以下條件的選擇性:
WHERE amount > 10000
如果統計資訊顯示大部分資料的 amount 都小於 10000,這個條件可能會排除大量資料。
如果只有少數資料符合條件,就可以預期 Filter 會大幅降低後續處理量。
這些估算可以協助系統選擇更合適的 Join、Filter 或 Aggregate 執行方式。
不過,統計資訊可能是精確值,也可能只是估計值,因此 Optimizer 的選擇不一定永遠是最佳解。
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 可以保留原本的計畫。
重要的是,每次改寫都必須維持相同的查詢語意。
可以在 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 並不代表每次查詢都一定會變快。
原因包括:
因此,實務上通常會搭配:
EXPLAIN
觀察計畫結構,再搭配:
EXPLAIN ANALYZE
查看實際執行時間與執行統計。
今天介紹了 Query Optimizer 的主要目的:
在不改變查詢結果的前提下,降低資料處理成本。
我們學到:
EXPLAIN VERBOSE 可以觀察各條 Optimizer Rule 的效果可以用以下流程總結:
原始 Logical Plan
↓
套用 Optimizer Rules
↓
Optimized Logical Plan
↓
Physical Planning
↓
Physical Optimization
↓
實際執行
下一篇將深入介紹 Projection Pushdown,觀察 DataFusion 如何判斷查詢真正需要哪些欄位,以及欄位裁剪如何降低資料讀取成本。
iThome鐵人賽