iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0

Logical Plan 是 SQL 的第一張執行藍圖。

它不會立刻讀取檔案,也不會直接產生查詢結果,而是先把 SQL 拆成一棵由多個 Logical Operator 組成的樹,描述:

要讀取什麼資料?
要篩選哪些資料?
要保留哪些欄位?
要如何分組與聚合?

例如:

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

可以表示成:

Projection
├── city
└── total_amount
        ↓
Aggregate
├── GROUP BY city
└── SUM(amount)
        ↓
Filter
└── is_member = true
        ↓
TableScan
└── orders

這棵樹由下往上讀:

TableScan
→ Filter
→ Aggregate
→ Projection

今天會認識幾個常見的 Logical Operator:

TableScan
Filter
Projection
Aggregate
Join

Logical Plan 是什麼?

Logical Plan 是描述查詢邏輯的中間表示。

它關心的是:

查詢想完成什麼工作?

而不是:

要使用哪種 Join 演算法?
要分成幾個 Partition?
要如何平行讀取 Parquet?

這些細節會在 Physical Plan 階段才決定。

因此可以先這樣區分:

Plan 主要回答的問題
Logical Plan 要做什麼?
Physical Plan 實際怎麼做?

DataFusion 的 Logical Plan 是一棵由關聯式運算元組成的樹,例如 TableScanFilterProjectionAggregateJoinDataFusion Building Logical Plans


把一段 SQL 組成完整 Logical Plan

先用一張圖看整體結構:

DataFusion Logical Plan:SQL 的執行藍圖

圖 1:一段 SQL 對應的 DataFusion Logical Plan。Logical Plan 描述查詢要做什麼,經過最佳化後才會轉換為 Physical Plan。圖由本文整理。

這棵樹要從最下層開始理解:

TableScan
→ 取得資料
→ Filter 篩選資料
→ Aggregate 分組與聚合
→ Projection 決定輸出欄位

SQL 是給人撰寫的查詢語言;Logical Plan 則是讓 Query Engine 能夠分析與最佳化的資料結構。


TableScan:從哪裡取得資料?

TableScan 是 Logical Plan 的起點,代表:

我要讀取哪一張資料表?

例如:

FROM orders

可以表示成:

TableScan
└── table: orders

這張表可能來自:

CSV
JSON
Parquet
記憶體資料
資料庫
自訂 Data Source

TableScan 也會知道資料表的 Schema:

orders
├── order_id  : Int64
├── city      : Utf8
├── amount    : Float64
└── is_member : Boolean

但要注意,Logical Plan 中的 TableScan 只是描述「要掃描哪張表」,不代表此時已經把所有資料讀進記憶體。

實際讀取資料會在後面的 Physical Plan 與 Execution 階段發生。


Filter:只保留符合條件的資料

Filter 對應 SQL 中的 WHERE

WHERE is_member = true

概念上會形成:

Filter
└── predicate: is_member = true
        ↓
TableScan
└── orders

假設原始資料是:

order_id | city | amount | is_member
1        | 台北 | 1200   | true
2        | 台中 | 850    | false
3        | 台北 | 2300   | true

套用條件:

is_member = true

後,留下:

order_id | city | amount | is_member
1        | 台北 | 1200   | true
3        | 台北 | 2300   | true

Filter 通常不會改變欄位名稱與型別,只會讓資料列變少:

輸入 Schema
order_id, city, amount, is_member

輸出 Schema
order_id, city, amount, is_member

在 Logical Plan 中,Filter 只描述篩選條件;真正如何有效率地讀取與篩選資料,可能會由 Optimizer 和 Physical Plan 決定。


Projection:選擇或計算欄位

Projection 對應 SQL 中的 SELECT 欄位清單:

SELECT city, amount
FROM orders;

可以表示成:

Projection
├── city
└── amount
        ↓
TableScan
└── orders

Projection 不只可以選擇欄位,也可以計算衍生欄位:

SELECT
    city,
    amount * 1.05 AS amount_with_tax
FROM orders;

概念上的輸出是:

Projection
├── city
└── amount * 1.05 AS amount_with_tax
        ↓
TableScan

此時輸出 Schema 會從:

order_id, city, amount, is_member

變成:

city, amount_with_tax

Projection 可以負責:

  • 選擇欄位
  • 改變欄位順序
  • 重新命名欄位
  • 計算衍生欄位
  • 執行型別轉換
  • 移除不需要的欄位

因此,Projection 主要決定查詢結果的欄位結構。


Aggregate:分組與聚合

Aggregate 對應 SQL 中的 GROUP BY 與聚合函式:

SELECT city, SUM(amount) AS total_amount
FROM orders
GROUP BY city;

可以表示成:

Aggregate
├── group_by: city
└── aggregate: SUM(amount)
        ↓
TableScan
└── orders

輸入資料:

city | amount
-----|-------
台北 | 1200
台中 | 850
台北 | 2300

依照 city 分組後:

台北
├── 1200
└── 2300

台中
└── 850

最後得到:

city | total_amount
-----|-------------
台北 | 3500
台中 | 850

Aggregate 通常會改變:

資料列數
輸出欄位
輸出 Schema

常見的聚合函式包括:

SUM(amount)
COUNT(*)
AVG(amount)
MIN(amount)
MAX(amount)

Join:合併不同資料來源

Join 用來合併兩個資料來源:

SELECT orders.order_id, customers.name
FROM orders
JOIN customers
  ON orders.customer_id = customers.id;

Logical Plan 會有兩個輸入子樹:

Join
├── condition: orders.customer_id = customers.id
├── TableScan: orders
└── TableScan: customers

和 Filter、Projection 相比,Join 的特別之處是:

Filter
→ 通常只有一個輸入

Projection
→ 通常只有一個輸入

Join
→ 同時需要兩個輸入

Join 的輸出 Schema 通常會包含兩張表的欄位:

orders.order_id
orders.customer_id
customers.id
customers.name

至於實際執行時採用 Hash Join、Sort-Merge Join 或其他演算法,則屬於 Physical Plan 的工作。

Logical Plan 只需先表達:

哪兩個資料來源要合併?
使用什麼條件合併?

Logical Operator 會攜帶 Schema

每個 Logical Operator 不只描述運算,也會推導自己的輸出 Schema。

例如:

TableScan
輸出:order_id, city, amount, is_member
Filter
輸出:order_id, city, amount, is_member
Aggregate
輸出:city, total_amount
Projection
輸出:city, total_amount

因此 DataFusion 在尚未執行查詢前,就能先知道:

每個節點會輸出哪些欄位?
每個欄位的型別是什麼?
下一個節點能否使用這些欄位?

這也是為什麼許多錯誤可以在執行前被發現:

欄位不存在
型別不相容
聚合函式使用錯誤
GROUP BY 欄位不符合規則

Logical Plan 不是執行結果

看到 Logical Plan,不代表資料已經讀取完成。

Logical Plan
→ 描述查詢要做什麼

Physical Plan
→ 決定實際怎麼做

Execution
→ 真正讀取資料並產生 RecordBatch

完整流程可以表示成:

SQL
↓
Parser
↓
AST
↓
Logical Plan
↓
Logical Optimizer
↓
Physical Plan
↓
Execution
↓
Arrow RecordBatch Stream
↓
Query Result

因此:

Logical Plan 是藍圖。
Physical Plan 是施工方法。
Execution 是實際施工。

使用 EXPLAIN 觀察 Logical Plan

DataFusion 提供 EXPLAIN 指令查看查詢計畫:

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

輸出格式會依版本與設定不同,但通常可以看到類似結構:

Projection
  Aggregate
    Filter
      TableScan

閱讀時可以從最下層開始:

TableScan
→ 讀取哪張表?

Filter
→ 使用什麼條件篩選?

Aggregate
→ 如何分組與聚合?

Projection
→ 最後輸出哪些欄位?

EXPLAIN 很適合用來檢查:

查詢是否讀取不需要的欄位?
Filter 是否被推近資料來源?
Logical Plan 是否符合預期?

下一篇會進一步使用 EXPLAIN,比較最佳化前後的 Query Plan。


今日小結

今天認識了幾個常見的 Logical Operator:

TableScan
→ 從資料來源取得資料

Filter
→ 依條件篩選資料列

Projection
→ 選擇、重新命名或計算欄位

Aggregate
→ 分組並執行 SUM、COUNT、AVG 等聚合

Join
→ 合併兩個資料來源

一段 SQL 會被轉換成一棵 Logical Plan 樹:

Projection
        ↓
Aggregate
        ↓
Filter
        ↓
TableScan

這棵樹描述的是:

查詢要做什麼

但還沒有決定:

查詢實際要怎麼執行

可以用一句話記住:

Logical Plan 是 SQL 的資料操作藍圖;
Physical Plan 才是實際執行的施工方式。

下一篇會使用 EXPLAIN 偷看 DataFusion 產生的查詢計畫,觀察 Logical Plan 與 Physical Plan 的差異。


延伸閱讀


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

尚未有邦友留言

立即登入留言