iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0

上一篇使用 EXPLAIN 觀察了 SQL 的 Logical Plan。

Logical Plan 描述的是:

這段查詢想完成什麼?

今天再往下一層,介紹 Physical Plan。Physical Plan 描述的是:

這些工作要如何實際執行?


Planning 與 Execution 是前後相接的階段

DataFusion 執行 SQL 時,可以簡化成以下流程:

SQL
 ↓
SQL Parser
 ↓
Logical Plan
 ↓
Logical Optimizer
 ↓
Physical Plan
 ↓
Execution
 ↓
Query Result

Planning 階段負責產生並最佳化查詢計畫,Execution 階段則依照 Physical Plan 讀取資料、執行運算並產生結果。

所以 Planning 與 Execution 不是兩個互相獨立的區塊,而是前後相接:

Planning:決定要做什麼、如何執行
Execution:按照計畫實際處理資料

Logical Plan 與 Physical Plan 的差異

假設我們有以下 SQL:

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

Logical Plan 可能表示成:

Projection
  Aggregate
    Filter
      TableScan

這棵樹描述了查詢的邏輯步驟:

  1. 讀取 orders
  2. 篩選會員資料
  3. 依照城市分組
  4. 加總訂單金額
  5. 輸出城市與總金額

Logical Plan 不需要決定:

  • 使用哪一種聚合演算法
  • 是否要將資料重新分區
  • 使用多少個執行分割區
  • 每個算子要如何傳遞資料
  • 使用哪一個實際執行元件

這些細節會在 Physical Plan 中決定。

可以用餐廳來比喻:

Logical Plan
客人想要「依城市統計訂單金額」

Physical Plan
廚房決定先分區處理資料,
再進行局部加總,最後合併結果

DataFusion Logical Plan 與 Physical Plan

圖 1:DataFusion 將 Logical Plan 轉換為 Physical Plan,並透過不同的 Execution Operator 執行查詢。

圖中的 Physical Plan 為概念示意,實際輸出會依 DataFusion 版本、資料來源與執行設定而不同。


Physical Plan 是什麼?

Physical Plan 是可以交給 Execution Engine 執行的計畫。

同一個 Logical Plan,可能有不同的 Physical Plan。例如,聚合可以:

  • 在單一分割區中執行
  • 先對各分割區做 Partial Aggregate
  • 再將結果合併成 Final Aggregate
  • 依照群組欄位重新分區
  • 使用不同的執行算子處理資料

因此:

Logical Plan:描述查詢需求
Physical Plan:描述具體執行方式

Physical Plan 的目的,是將抽象的查詢需求轉換成實際可執行的步驟。


TableScan 如何變成資料來源執行算子?

在 Logical Plan 中,資料讀取通常表示為:

TableScan: orders

這代表查詢需要讀取 orders 這張表。

Physical Planner 會根據這張表背後的 TableProvider,建立對應的資料來源執行算子。

不同資料來源可能使用不同的執行元件:

CSV      → CSV 相關的資料來源執行算子
Parquet  → Parquet 相關的資料來源執行算子
Memory   → MemoryExec

這些執行算子在真正執行時會:

  1. 開啟資料來源
  2. 讀取資料
  3. 依照 Schema 解讀欄位
  4. 產生 Arrow RecordBatch
  5. 將資料傳給下一個執行算子

因此,TableScan 本身不是直接讀檔案,而是描述:

需要掃描哪一張表。

真正的資料讀取工作會在 Physical Plan 的資料來源算子中完成。


Filter 如何變成 FilterExec?

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 也可能嘗試把篩選條件交給資料來源處理,減少實際需要讀取的資料量。


Aggregate 如何變成 AggregateExec?

Logical Plan 中的聚合可能表示為:

Aggregate:
  groupBy=[[orders.city]]
  aggr=[[SUM(orders.amount)]]

Physical Plan 中則可能變成:

AggregateExec: mode=Partial

以及:

AggregateExec: mode=Final

這代表 DataFusion 將聚合拆成兩個階段。

Partial Aggregate

每個分割區先獨立計算部分結果:

Partition 1 → Taipei: 1200
Partition 2 → Taipei: 2300

Final Aggregate

再將不同分割區的結果合併:

Taipei: 1200 + 2300 = 3500

這種方式可以讓不同分割區平行處理,最後再合併結果。

簡化流程如下:

RecordBatch
    ↓
Partial Aggregate
    ↓
Repartition
    ↓
Final Aggregate
    ↓
聚合結果

實際是否拆成 Partial 與 Final,會依照查詢內容、資料來源與執行設定而有所不同。


Physical Plan 如何決定資料分區?

大型資料通常會被切成多個 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 如何決定執行順序?

假設 Physical Plan 如下:

ProjectionExec
  AggregateExec
    RepartitionExec
      AggregateExec
        FilterExec
          MemoryExec

通常可以由下往上閱讀:

MemoryExec
    ↓
FilterExec
    ↓
AggregateExec(Partial)
    ↓
RepartitionExec
    ↓
AggregateExec(Final)
    ↓
ProjectionExec

執行流程就是:

  1. MemoryExec 讀取記憶體中的資料
  2. FilterExec 篩選會員資料
  3. Partial AggregateExec 進行局部加總
  4. RepartitionExec 依照城市重新分配資料
  5. Final AggregateExec 合併聚合結果
  6. ProjectionExec 輸出最後需要的欄位

每個執行算子通常會接收上游的 Arrow RecordBatch,處理後再產生新的 RecordBatch


使用 EXPLAIN 觀察轉換結果

可以在 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

實際輸出會受到以下因素影響:

  • DataFusion 版本
  • 使用的資料來源
  • Partition 數量
  • 查詢最佳化設定
  • 資料來源是否支援條件下推或欄位裁剪

所以不同環境看到的算子名稱與排列方式可能不完全相同。


EXPLAIN 與 EXPLAIN ANALYZE

EXPLAIN SELECT ...;

主要用來查看查詢計畫。

EXPLAIN ANALYZE SELECT ...;

則會實際執行查詢,並顯示執行時間、輸出批次與其他統計資訊。

可以這樣區分:

EXPLAIN
查看「準備怎麼執行」

EXPLAIN ANALYZE
實際執行並查看「執行得如何」

如果只是想理解查詢計畫,先使用 EXPLAIN 即可;如果要分析效能,再使用 EXPLAIN ANALYZE


從 SQL 到執行結果

把今天的內容串起來:

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 負責產生最後的輸出欄位
  • Physical Plan 會決定資料的執行順序與平行方式
  • RecordBatch 是執行算子之間傳遞的資料單位

下一篇將完整追蹤一段 SQL 的生命週期,從 SQL 輸入開始,一路觀察它如何經過解析、規劃、最佳化、執行,最後產生查詢結果。

延伸閱讀


上一篇
Day 13|用 EXPLAIN 偷看 SQL 背後的 Query Plan
系列文
深入 SQL 查詢引擎:30 天用 Rust 與 Apache DataFusion 解構資料處理流程14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言