上一篇介紹了 Logical Plan,知道 SQL 會被轉換成一棵由 TableScan、Filter、Aggregate 與 Projection 組成的操作樹。
不過,實際使用 DataFusion 時,要怎麼查看這些計畫?
答案就是:
EXPLAIN
EXPLAIN 不會直接回傳查詢結果,而是顯示 DataFusion 為 SQL 建立的查詢計畫,讓我們觀察 SQL 如何從 Logical Plan 逐步轉換成 Physical Plan。
本文使用 macOS 示範。
DataFusion 提供命令列工具 datafusion-cli。如果已經安裝 Homebrew,可以直接執行:
brew install datafusion
安裝完成後確認版本:
datafusion-cli --version
如果看到版本資訊,就代表安裝成功。
也可以使用 Rust 的 Cargo 安裝:
cargo install datafusion-cli
若 Cargo 安裝完成後仍找不到指令,請確認 Cargo 的執行檔路徑已加入 PATH:
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
接著再次確認:
datafusion-cli --version
官方安裝說明可以參考:
在終端機輸入:
datafusion-cli
成功啟動後,會進入互動式 SQL 命令列環境。
為了示範,我們先建立一張簡單的 orders 表:
CREATE TABLE orders AS
VALUES
(1, 'Taipei', 1200.0, true),
(2, 'Taichung', 850.0, false),
(3, 'Taipei', 2300.0, true),
(4, 'Kaohsiung', 560.5, false),
(5, 'Taoyuan', 1750.0, true);
這個範例沒有指定欄位名稱,因此 DataFusion 會自動使用:
column1
column2
column3
column4
分別代表:
column1:訂單編號
column2:城市
column3:訂單金額
column4:是否為會員
先確認資料:
SELECT * FROM orders;
現在執行一段分析查詢:
EXPLAIN
SELECT column2 AS city,
SUM(column3) AS total_amount
FROM orders
WHERE column4 = true
GROUP BY column2;
DataFusion 會顯示查詢計畫,而不是直接顯示最終結果。
輸出通常會包含兩個重要部分:
logical_plan
physical_plan
其中:
logical_plan 描述「這段查詢需要做什麼」physical_plan 描述「實際要如何執行這段查詢」為了讓樹狀結構更容易閱讀,可以使用:
EXPLAIN FORMAT INDENT
SELECT column2 AS city,
SUM(column3) AS total_amount
FROM orders
WHERE column4 = true
GROUP BY column2;
不同 DataFusion 版本、資料來源與執行設定,輸出的細節可能會不同。以下是常見的輸出形式:
+---------------+------------------------------------------------------------------------------------------------------------------------------------------------+
| plan_type | plan |
+---------------+------------------------------------------------------------------------------------------------------------------------------------------------+
| logical_plan | Projection: orders.column2 AS city, SUM(orders.column3) AS total_amount |
| | Aggregate: groupBy=[[orders.column2]], aggr=[[SUM(orders.column3)]] |
| | Filter: orders.column4 = Boolean(true) |
| | TableScan: orders projection=[column2, column3, column4] |
| physical_plan | ProjectionExec: expr=[column2@0 as city, SUM(orders.column3)@1 as total_amount] |
| | AggregateExec: mode=FinalPartitioned, gby=[column2@0 as column2], aggr=[SUM(orders.column3)] |
| | CoalesceBatchesExec: target_batch_size=8192 |
| | RepartitionExec: partitioning=Hash([column2@0], 8), input_partitions=8 |
| | AggregateExec: mode=Partial, gby=[column2@0 as column2], aggr=[SUM(orders.column3)] |
| | CoalesceBatchesExec: target_batch_size=8192 |
| | FilterExec: column4@2 = true |
| | MemoryExec: partitions=1, partition_sizes=[1] |
+---------------+------------------------------------------------------------------------------------------------------------------------------------------------+
實際輸出會依照 DataFusion 版本、資料來源、分割區數量與執行設定而有所不同,因此不一定會與範例完全相同。
先看輸出的 logical_plan:
Projection
Aggregate
Filter
TableScan
這棵樹通常要由下往上閱讀。
TableScan: orders
代表從 orders 資料表讀取資料。
如果只需要部分欄位,計畫中可能會看到:
projection=[column2, column3, column4]
這表示查詢不需要讀取 column1,因此可以避免掃描不必要的欄位。
Filter: orders.column4 = Boolean(true)
代表套用:
WHERE column4 = true
只有會員資料會繼續往上傳遞。
Aggregate:
groupBy=[[orders.column2]]
aggr=[[SUM(orders.column3)]]
代表依照城市分組,並計算每個城市的訂單金額總和。
對應的 SQL 是:
GROUP BY column2
SUM(column3)
Projection:
orders.column2 AS city
SUM(orders.column3) AS total_amount
最後只輸出兩個欄位:
city
total_amount
因此,Logical Plan 可以理解成:
讀取 orders
↓
篩選會員資料
↓
依城市分組並加總金額
↓
輸出 city 與 total_amount
Logical Plan 描述的是查詢的邏輯需求:
我要掃描資料
我要篩選會員
我要依城市分組
我要計算金額總和
它不需要決定:
這些工作會交給 Physical Planner 與 Execution Engine。
Physical Plan 可能會出現:
ProjectionExec
AggregateExec
FilterExec
MemoryExec
RepartitionExec
CoalesceBatchesExec
名稱結尾的 Exec 可以理解成實際執行階段所使用的算子。
例如:
FilterExec
代表實際執行篩選條件。
AggregateExec
代表實際執行分組與聚合。
MemoryExec
代表從記憶體中的資料批次讀取資料。在讀取 CSV、JSON 或 Parquet 時,也可能看到其他資料來源相關的 Execution Plan 節點。
同一段 SQL 可以有不同的執行方式。
例如:
SELECT column2, SUM(column3)
FROM orders
WHERE column4 = true
GROUP BY column2;
在 Logical Plan 中,只需要描述:
TableScan → Filter → Aggregate → Projection
但在 Physical Plan 中,DataFusion 還需要決定:
因此 Physical Plan 通常會包含更多執行細節。
這也是為什麼 Logical Plan 比較適合用來理解查詢語意,而 Physical Plan 比較適合用來分析效能與實際執行方式。
一般的:
EXPLAIN SELECT ...;
主要是顯示查詢計畫,不會像一般 SELECT 一樣直接輸出查詢結果。
如果希望查看實際執行時的統計資訊,可以使用:
EXPLAIN ANALYZE
SELECT column2 AS city,
SUM(column3) AS total_amount
FROM orders
WHERE column4 = true
GROUP BY column2;
EXPLAIN ANALYZE 會實際執行查詢,並顯示執行時間、輸出批次與其他執行指標。
因此可以這樣區分:
EXPLAIN
查看查詢計畫,不重點關注執行結果
EXPLAIN ANALYZE
執行查詢,同時查看實際執行統計
在範例中,查詢只使用:
column2
column3
column4
因此 DataFusion 可能會在 TableScan 顯示:
projection=[column2, column3, column4]
這表示 column1 不需要被讀取。
如果資料來源支援欄位裁剪,例如 Parquet,DataFusion 就能只讀取真正需要的欄位,減少:
這就是 Query Optimizer 可能帶來的效能改善之一。
可以把這個查詢簡化成:
TableScan
↓
Filter
↓
Aggregate
↓
Projection
資料會先從資料來源讀入,再經過每一個執行節點。
在 DataFusion 中,這些資料通常會以 Apache Arrow 的 RecordBatch 形式在不同算子之間傳遞。
因此,前幾天介紹的概念可以串在一起:
CSV / JSON / Parquet
↓
Data Source
↓
TableProvider
↓
Logical Plan
↓
Physical Plan
↓
Arrow RecordBatch
↓
Execution Operators
↓
Query Result
今天使用 DataFusion CLI 實際執行了 EXPLAIN,觀察 SQL 背後的查詢計畫。
我們學到:
EXPLAIN 可以查看查詢計畫TableScan 負責讀取資料Filter 負責篩選資料Aggregate 負責分組與聚合Projection 負責選擇輸出欄位EXPLAIN ANALYZE 可以查看實際執行統計下一篇將從 Logical Plan 往下一層,介紹 Physical Plan。
Logical Plan 描述的是「查詢想完成什麼」;Physical Plan 則會進一步決定「這些工作要如何實際執行」。
我們會看到 TableScan 如何變成資料來源執行算子、Filter 如何變成 FilterExec,以及 DataFusion 如何選擇聚合、分區與資料處理方式。