提到資料工程,你最先想到的可能是 ETL:
但資料工程真的只是「把資料從一個地方搬到另一個地方」嗎?
假設我們有一份訂單資料,現在想計算 2026 年截至今天(2026 年 8 月 17 日),每個城市的訂單總金額:
SELECT city, SUM(amount) AS total_amount
FROM orders
WHERE order_date BETWEEN DATE '2026-01-01'
AND DATE '2026-08-17'
GROUP BY city;
對使用者來說,我們只送出了一段 SQL。
但從 SQL 送出到結果顯示以前,系統還要經過解析、規劃、最佳化與實際運算。
負責這些工作的核心元件,就是今天要認識的 Query Engine(查詢引擎)。
ETL 分別代表:
Extract → Transform → Load
擷取資料 → 轉換資料 → 載入資料
三個階段各自負責:
Transform 常見的操作包括:
WHERE 篩選資料JOIN 整合不同資料集GROUP BY 進行分組SUM、COUNT、AVG 產生彙總結果不過,ETL 階段和 SQL 語法並不是一對一對應。
例如,SELECT * FROM orders 可以用來從來源取得資料,因此能視為 Extract;如果 SELECT 中還包含欄位計算、篩選或聚合,它也同時參與了 Transform。
換句話說,SQL 描述的是資料操作方式,Extract、Transform、Load 描述的則是這些操作在整個資料流程中扮演的角色。
一套資料工程 Pipeline 通常需要處理:
資料工程 Pipeline
├── 資料從哪裡來
├── 需要經過哪些轉換
├── 何時執行
├── 失敗時如何處理
└── 結果要寫到哪裡
Query Engine 的角色,則是理解 SQL 描述的需求,將篩選、欄位選擇、聚合與 JOIN 等操作,轉換成電腦可以執行的資料運算流程。
Query Engine 也不只存在於傳統資料庫中。它可以被嵌入資料處理工具、DataFrame 函式庫、串流系統,以及其他需要分析資料的應用程式。
Query Engine 可以先簡單理解成:
把使用者描述的查詢需求,轉換成實際可執行的資料處理流程。
以剛才的 SQL 為例,Query Engine 必須理解:
orders
city 分組SUM(amount)
city 與 total_amount
它不只要知道「要做什麼」,還要決定「實際怎麼做」。
一段 SQL 從輸入到產生結果,可以先簡化成:
SQL
↓
SQL Parsing
↓
Logical Planning ← Data Source / Schema
↓
Logical Optimization
↓
Physical Planning
↓
Physical Optimization
↓
Execution → 讀取 Data Source
↓
Result
接下來看看每個階段負責什麼。
SQL 對人類來說是一段文字,但 Query Engine 不會直接拿著字串開始計算。
它會先進行 SQL Parsing,辨認其中的資料表、欄位、函數、條件與運算子。
例如:
SELECT
├── city
└── SUM(amount)
FROM
└── orders
WHERE
└── order_date BETWEEN ...
GROUP BY
└── city
如果把 SELECT 寫成 SELEC,這類語法錯誤通常也會在解析階段被發現。
SQL 解析完成後,Query Engine 會建立 Logical Plan。
Logical Plan 描述的是:
這段查詢在邏輯上需要完成哪些操作?
剛才的 SQL 可以簡化成:
Projection: city, total_amount
↓
Aggregate: GROUP BY city, SUM(amount)
↓
Filter: order_date BETWEEN ...
↓
TableScan: orders
這棵計畫可以由下往上閱讀:
orders
city 分組並計算總金額建立及驗證 Logical Plan 時,Query Engine 需要找到 orders 對應的資料來源,並取得它的 Schema:
orders
├── order_id: Int64
├── city: Utf8
├── amount: Float64
└── order_date: Date32
有了 Schema,系統才能檢查欄位是否存在、資料型別是否能進行指定運算,以及查詢結果應該包含哪些欄位。
此時主要是在規劃查詢,真正大量讀取資料內容通常會在執行階段發生。
同一個查詢結果,可能有不同的執行方式。
Optimizer(查詢最佳化器)會嘗試在不改變查詢結果的前提下,減少不必要的工作。
例如,orders 可能有許多欄位,但這段 SQL 實際只需要:
city
amount
order_date
Query Engine 可以嘗試避免讀取其他欄位。這類最佳化稱為 Projection Pushdown,也常稱為欄位裁剪。
WHERE 條件也可能被移動到更接近資料來源的位置,讓不符合條件的資料儘早被排除。這稱為 Filter Pushdown。
讀取資料
↓
儘早篩選
↓
只處理剩餘資料
現在只要先記住:
最佳化不是改變 SQL 的答案,而是在維持相同結果的前提下,尋找更適合的執行方式。
我們會在 Day 16~20 進一步觀察這些最佳化規則。
Logical Plan 描述「要做什麼」,Physical Plan 則描述:
實際上要怎麼執行?
到了這個階段,Query Engine 會考慮更多執行細節,例如:
簡化後的 Physical Plan 可能像這樣:
ProjectionExec
↓
AggregateExec
↓
FilterExec
↓
DataSourceExec
其中的 Exec 可以先理解為負責實際執行某項操作的元件。
真實的 Physical Plan 可能包含更多節點,但 Day 1 先掌握 Logical Plan 與 Physical Plan 的角色差異即可。
Physical Plan 準備完成後,Query Engine 才真正開始讀取與處理資料。
大致流程是:
Apache DataFusion 使用 Apache Arrow 的欄式記憶體格式。執行過程中,資料會以一批一批的 RecordBatch 在不同執行節點之間傳遞。
最後得到的結果可能是:
+-----------+--------------+
| city | total_amount |
+-----------+--------------+
| Taipei | 152000 |
| Taoyuan | 98000 |
| Kaohsiung | 76000 |
+-----------+--------------+
結果除了顯示在畫面上,也可以:
理解 Query Engine,可以幫助我們回答:
WHERE 條件有沒有提早套用?EXPLAIN 顯示的 Query Plan 代表什麼?這些問題同時涉及資料格式、查詢設計、系統效能與資源使用,也是資料工程不只停留在「搬運資料」的重要原因。
今天先建立了一段 SQL 的生命週期:
SQL
↓
SQL Parsing
↓
Logical Plan
↓
Logical Optimization
↓
Physical Plan
↓
Physical Optimization
↓
Execution
↓
Result
目前只要先掌握三件事:
接下來 30 天,我們會使用 Rust 與 Apache DataFusion,逐步拆解這條流程,最後完成一套可以載入 CSV、JSON、Parquet,執行 SQL、查看 Query Plan 並輸出結果的本機資料分析工具。
下一篇將先回答:
為什麼選擇 Rust 與 Apache DataFusion?
把 SQL 先拆成 Parsing、Logical Plan、Physical Plan 再到 Execution,整條路徑一下就有呼吸感了,尤其 Projection Pushdown 跟 Filter Pushdown 這種「先少做一點」的味道,真的很像在替查詢省力。還有 DataFusion 用 RecordBatch 在節點間傳資料,那個欄式記憶體的畫面很清楚,也讓人更懂 EXPLAIN 背後在看什麼;這系列也很自然讓我想到 AI 開發時把流程拆細後,實作門檻會低很多。我手邊有多的 Lovable 額度想送給有緣人,有興趣可從連結看看我的系列。 https://ithelp.ithome.com.tw/articles/10401174