上一篇使用 EXPLAIN 觀察了 SQL 的 Logical Plan。
Logical Plan 描述的是:
這段查詢想完成什麼?
今天再往下一層,介紹 Physical Plan。Physical Plan 描述的是:
這些工作要如何實際執行?
DataFusion 執行 SQL 時,可以簡化成以下流程:
SQL
↓
SQL Parser
↓
Logical Plan
↓
Logical Optimizer
↓
Physical Plan
↓
Execution
↓
Query Result
Planning 階段負責產生並最佳化查詢計畫,Execution 階段則依照 Physical Plan 讀取資料、執行運算並產生結果。
所以 Planning 與 Execution 不是兩個互相獨立的區塊,而是前後相接:
Planning:決定要做什麼、如何執行
Execution:按照計畫實際處理資料
假設我們有以下 SQL:
SELECT city,
SUM(amount) AS total_amount
FROM orders
WHERE is_member = true
GROUP BY city;
Logical Plan 可能表示成:
Projection
Aggregate
Filter
TableScan
這棵樹描述了查詢的邏輯步驟:
orders
Logical Plan 不需要決定:
這些細節會在 Physical Plan 中決定。
可以用餐廳來比喻:
Logical Plan
客人想要「依城市統計訂單金額」
Physical Plan
廚房決定先分區處理資料,
再進行局部加總,最後合併結果

圖 1:DataFusion 將 Logical Plan 轉換為 Physical Plan,並透過不同的 Execution Operator 執行查詢。
圖中的 Physical Plan 為概念示意,實際輸出會依 DataFusion 版本、資料來源與執行設定而不同。
Physical Plan 是可以交給 Execution Engine 執行的計畫。
同一個 Logical Plan,可能有不同的 Physical Plan。例如,聚合可以:
因此:
Logical Plan:描述查詢需求
Physical Plan:描述具體執行方式
Physical Plan 的目的,是將抽象的查詢需求轉換成實際可執行的步驟。
在 Logical Plan 中,資料讀取通常表示為:
TableScan: orders
這代表查詢需要讀取 orders 這張表。
Physical Planner 會根據這張表背後的 TableProvider,建立對應的資料來源執行算子。
不同資料來源可能使用不同的執行元件:
CSV → CSV 相關的資料來源執行算子
Parquet → Parquet 相關的資料來源執行算子
Memory → MemoryExec
這些執行算子在真正執行時會:
RecordBatch
因此,TableScan 本身不是直接讀檔案,而是描述:
需要掃描哪一張表。
真正的資料讀取工作會在 Physical Plan 的資料來源算子中完成。
Logical Plan 中的篩選條件可能是:
Filter: orders.is_member = Boolean(true)
它對應到 SQL:
WHERE is_member = true
轉換成 Physical Plan 後,通常會看到:
FilterExec: is_member = true
FilterExec 會從上游接收 RecordBatch,逐列判斷條件,只讓符合條件的資料繼續往下游傳遞。
可以簡化成:
輸入 RecordBatch
↓
FilterExec
↓
只保留 is_member = true 的資料
如果資料來源支援條件下推,DataFusion 也可能嘗試把篩選條件交給資料來源處理,減少實際需要讀取的資料量。
Logical Plan 中的聚合可能表示為:
Aggregate:
groupBy=[[orders.city]]
aggr=[[SUM(orders.amount)]]
Physical Plan 中則可能變成:
AggregateExec: mode=Partial
以及:
AggregateExec: mode=Final
這代表 DataFusion 將聚合拆成兩個階段。
每個分割區先獨立計算部分結果:
Partition 1 → Taipei: 1200
Partition 2 → Taipei: 2300
再將不同分割區的結果合併:
Taipei: 1200 + 2300 = 3500
這種方式可以讓不同分割區平行處理,最後再合併結果。
簡化流程如下:
RecordBatch
↓
Partial Aggregate
↓
Repartition
↓
Final Aggregate
↓
聚合結果
實際是否拆成 Partial 與 Final,會依照查詢內容、資料來源與執行設定而有所不同。
大型資料通常會被切成多個 Partition,讓不同執行緒可以平行處理:
Partition 1
Partition 2
Partition 3
Partition 4
Physical Plan 可能會出現:
RepartitionExec
它的工作是依照某個欄位重新分配資料。
例如:
RepartitionExec:
partitioning=Hash([city])
這表示 DataFusion 可能依照 city 的值進行 Hash Partition。
同一個城市的資料會被分配到相同的 Partition,這樣後續的:
GROUP BY city
就能更有效率地執行。
不過,分區數量並不是越多越好:
分區太少
→ CPU 使用率可能不足
分區太多
→ 可能增加排程與資料交換成本
因此,Physical Planner 需要在平行處理與額外成本之間取得平衡。
假設 Physical Plan 如下:
ProjectionExec
AggregateExec
RepartitionExec
AggregateExec
FilterExec
MemoryExec
通常可以由下往上閱讀:
MemoryExec
↓
FilterExec
↓
AggregateExec(Partial)
↓
RepartitionExec
↓
AggregateExec(Final)
↓
ProjectionExec
執行流程就是:
MemoryExec 讀取記憶體中的資料FilterExec 篩選會員資料AggregateExec 進行局部加總RepartitionExec 依照城市重新分配資料AggregateExec 合併聚合結果ProjectionExec 輸出最後需要的欄位每個執行算子通常會接收上游的 Arrow RecordBatch,處理後再產生新的 RecordBatch。
可以在 DataFusion CLI 中執行:
EXPLAIN FORMAT INDENT
SELECT city,
SUM(amount) AS total_amount
FROM orders
WHERE is_member = true
GROUP BY city;
輸出中的 Logical Plan 可能接近:
Projection
Aggregate
Filter
TableScan
Physical Plan 則可能接近:
ProjectionExec
AggregateExec
RepartitionExec
AggregateExec
FilterExec
MemoryExec
實際輸出會受到以下因素影響:
所以不同環境看到的算子名稱與排列方式可能不完全相同。
EXPLAIN SELECT ...;
主要用來查看查詢計畫。
EXPLAIN ANALYZE SELECT ...;
則會實際執行查詢,並顯示執行時間、輸出批次與其他統計資訊。
可以這樣區分:
EXPLAIN
查看「準備怎麼執行」
EXPLAIN ANALYZE
實際執行並查看「執行得如何」
如果只是想理解查詢計畫,先使用 EXPLAIN 即可;如果要分析效能,再使用 EXPLAIN ANALYZE。
把今天的內容串起來:
SQL
↓
Logical Plan
TableScan
Filter
Aggregate
Projection
↓
Logical Optimizer
↓
Physical Plan
MemoryExec / DataSourceExec
FilterExec
RepartitionExec
AggregateExec
ProjectionExec
↓
Arrow RecordBatch Stream
↓
Query Result
這也說明了 DataFusion 為什麼要分成 Logical Plan 與 Physical Plan:
Logical Plan
讓系統理解查詢需求
Physical Plan
讓系統選擇實際執行方式
今天理解了 Physical Plan 的角色:
TableScan 描述需要掃描哪張表Filter 可能轉換成 FilterExec
Aggregate 可能轉換成 Partial 與 Final AggregateExec
RepartitionExec 負責重新分配資料ProjectionExec 負責產生最後的輸出欄位RecordBatch 是執行算子之間傳遞的資料單位下一篇將完整追蹤一段 SQL 的生命週期,從 SQL 輸入開始,一路觀察它如何經過解析、規劃、最佳化、執行,最後產生查詢結果。