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 是描述查詢邏輯的中間表示。
它關心的是:
查詢想完成什麼工作?
而不是:
要使用哪種 Join 演算法?
要分成幾個 Partition?
要如何平行讀取 Parquet?
這些細節會在 Physical Plan 階段才決定。
因此可以先這樣區分:
| Plan | 主要回答的問題 |
|---|---|
| Logical Plan | 要做什麼? |
| Physical Plan | 實際怎麼做? |
DataFusion 的 Logical Plan 是一棵由關聯式運算元組成的樹,例如 TableScan、Filter、Projection、Aggregate 與 Join。DataFusion Building Logical Plans
先用一張圖看整體結構:

圖 1:一段 SQL 對應的 DataFusion Logical Plan。Logical Plan 描述查詢要做什麼,經過最佳化後才會轉換為 Physical Plan。圖由本文整理。
這棵樹要從最下層開始理解:
TableScan
→ 取得資料
→ Filter 篩選資料
→ Aggregate 分組與聚合
→ Projection 決定輸出欄位
SQL 是給人撰寫的查詢語言;Logical Plan 則是讓 Query Engine 能夠分析與最佳化的資料結構。
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 對應 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 對應 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 對應 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 用來合併兩個資料來源:
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。
例如:
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
→ 描述查詢要做什麼
Physical Plan
→ 決定實際怎麼做
Execution
→ 真正讀取資料並產生 RecordBatch
完整流程可以表示成:
SQL
↓
Parser
↓
AST
↓
Logical Plan
↓
Logical Optimizer
↓
Physical Plan
↓
Execution
↓
Arrow RecordBatch Stream
↓
Query Result
因此:
Logical Plan 是藍圖。
Physical Plan 是施工方法。
Execution 是實際施工。
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 的差異。