DE Day 1 系列導覽:資料工程的學習地圖
執行需求:CPU 可跑。今天是「資料工程實戰:SQL、DuckDB 與自動化管線」系列的第一篇。我們會先把整個系列的全貌攤開來看,告訴你這 45 篇會怎麼走、主軸是什麼、每天結束會得到什麼能力。文章本身不會把全部工具裝好,但我們會先把工作目錄、Python 環境與一個能跑的最小範例做出來,讓你從 Day 2 開始就能順著走。為了避免你看到一半就放棄,我會先把「學完能幹嘛」講清楚,再談地圖細節。
引言
如果你會寫 Python、寫過幾次 pandas、寫過幾段 SQL,卻對「怎麼打造一條每天自動運行的資料管線」沒有把握,這個系列就是給你的。我們不會從機器學習或資料科學分析切入,而是把鏡頭拉遠到「資料怎麼進來、怎麼落地、怎麼轉換、怎麼被監控」,也就是資料工程(Data Engineering)這條線。實務上,這條線決定了後續分析、報表、模型與儀表板有沒有可靠的原料;沒有它,上面每一層都是在沙堆上蓋城堡。
這系列的定位是「能跑、能在公司落地、能交給同事接手」。我們會全程只使用 2025 年 11 月還在維護的主流工具:Python 3.13、DuckDB 1.4 世代、Polars 1.3x、pandas 2.3、dbt-core 1.10+、APScheduler 3.11 等。寫作時間預設為 2025-11-17 起、一天一更。每一篇都會有一個可整段執行的範例,並把輸出結果寫在註解裡,方便你比對自己電腦上的結果是否一致。整個系列的設計假設如下:你的機器是普通筆電或桌機、只有 CPU、可以接受先在本機把整套管線跑起來,最後再決定要不要容器化或排程到雲端。
為什麼是這條學習地圖
資料工程常被誤會成「把資料從 A 搬到 B」,其實工作內容遠比這個豐富。它涵蓋了資料來源的探索與簽署、排程與自動化、清洗與驗證、轉換與建模(事實表、維度表等倉儲概念)、品質檢查與監控、儀表板呈現與交接文件等。每一塊單獨拿出來都可以是一門學問;當你把它們拼成一條管線時,會發現真正難的是「讓它穩穩地每天執行」。這條地圖就是為了這個目標設計的。我們不是要教你所有可能的工具,而是要教你一套能在 2025 年的環境裡跑得起來、且能用最少力氣維護的工作流。
系列期間我們只挑最通用、最好上手、社群最活躍的工具,盡量避免冷門或即將被取代的選擇。對主流方案熟悉之後,你再去翻各家比較文章、評估商業產品(Snowflake、BigQuery、Databricks 等),都會容易得多。換句話說,這條地圖是「讓你具備判斷能力」,而不是「讓你永遠停在這個棧」。所有的章節都會標明版本,盡量寫在 2025-11-17 仍合理的主流版本,避免提到 2026 年之後才出現的功能。
系列分區與每天的產出
45 篇分成六個區塊,每一篇都是前一篇的延伸。我們會先打基礎,再進入 SQL,再看工具輪替、再看真實資料、再做端到端、最後做展示與部署。底下是分區摘要,幫你對齊預期:
- 導論區(Day 1-2):地圖、環境與工具鏈。
- SQL 區(Day 3-7):視窗函式、排名與累計、CTE 與遞迴、聯結策略、查詢計畫與索引。
- 工具區(Day 8-12):DuckDB、Parquet、pandas、Polars、混用策略。
- 清洗與採集區(Day 13-19):缺失值與字串清洗、品質檢查、爬蟲、開放資料、API 分頁。
- 管線與建模區(Day 20-26):水位標記與冪等、排程、失敗處理、維度建模、dbt 入門與測試。
- 編排區(Day 27-29):Airflow 概念與實作、輕量編排替代方案。
- 專案與展示區(Day 30-45):端到端管線、Streamlit 儀表板、部署、LLM 輔助、文件化、最終展示與系列回顧。
每一篇都設計成「一邊讀一邊動手」、「一個段落至少一個觀念」、「結尾有一個能跑的範例」。當你讀完一篇,請至少花十分鐘實際跑一次程式碼,把輸出對一遍,這樣知識才會留下來。建議你開一個筆記本(Markdown、Notion、Obsidian 都可),記錄「今天的關鍵詞」、「踩到的坑」、「下一步想做什麼」,這份筆記會是 45 天後你最值錢的東西。
什麼是資料管線?先建立共同語言
「管線(pipeline)」這個詞,指的是把資料從來源一路處理到可以使用的目的地,這條路上的每一個節點都負擔一項工作。我們用一個政府開放資料的場景當範例:來源是經濟部「公司登記」資料集(位於 data.gov.tw,採政府資料開放授權條款第 1 版),我們希望每天把新公布的資料拉回來,整理成「公司基本維度表」與「變更事實表」,最後丟到一個輕量的儀表板讓同事查詢。這個需求對應到資料工程的典型子流程:
- 採集(Ingest):從政府平台下載檔案、做基本格式驗證。
- 落地(Land):把原始檔案保留起來(檔案系統或物件儲存),以利日後重跑或稽核。
- 轉換(Transform):用 SQL 或 Polars 清洗欄位型別、補缺值、做單位統一。
- 建模(Model):把資料組合成事實表與維度表,這是分析友善的結構。
- 品質(Quality):用規則檢查必填欄位、唯一性、區間合理度。
- 呈現(Serve):把模型層的資料接到儀表板或 API。
- 編排(Orchestration):把所有步驟排成可重複、可觀察的工作流。
這條線每一段都可以再展開成一本書,系列文章的功能是「把每一段的核心動作講一次、寫一次、跑一次」,讓你有能力自行延伸。我們不會跟你說哪個工具永遠最好,而是讓你看清楚「這工具在這個位置最划算」,之後該換就換、該升就升。
完整實作:先把工作目錄與最小範例做出來
這個系列的工作目錄規劃為 de-journey/,內含 data/(原始檔落地處)、warehouse/(DuckDB 檔案)、models/(dbt 模型)、pipelines/(Python 管線)、dashboards/(Streamlit)、logs/(執行紀錄)。我們會用 uv(2025 年的熱門 Python 套件與環境管理工具,採用 Rust 實作、安裝快速、檔案鎖明確)管理環境。先把今天的環境準備好,這樣從 Day 2 開始就能直接套用。
第一步:用 uv 在工作目錄建立 Python 3.13 環境,並安裝資料工程常用的核心套件。整段指令可以一次貼上執行:
mkdir de-journey && cd de-journey
mkdir data warehouse models pipelines dashboards logs
uv venv --python 3.13
source .venv/bin/activate # Windows PowerShell 改用 .venv\Scripts\Activate.ps1
uv pip install duckdb==1.4.1 polars==1.33.0 pandas==2.3.0 pyarrow==18.0.0
uv pip install dbt-core==1.10.0 dbt-duckdb==1.10.0
uv pip install apscheduler==3.11.0 streamlit==1.41.0
python -c "import sys, duckdb, polars, pandas; print(sys.version.split()[0], duckdb.__version__, polars.__version__, pandas.__version__)"
輸出(實際版本字串會依安裝當下略有不同):
3.13.1 1.4.1 1.33.0 2.3.0
這段指令做了三件事:建立工作目錄、用 uv 建立獨立的 Python 3.13 環境、安裝資料工程核心套件、印出版本做驗證。請你一定要看到最後一行的版本字串輸出再往下走;若沒有輸出,代表環境沒啟動成功。Windows 的 PowerShell 使用者要把 source 換成 .venv\Scripts\Activate.ps1,這是兩種 shell 在啟動虛擬環境時的差異,不影響功能。uv 預設會建立 .venv 目錄,啟動後命令提示字元前面會出現 (de-journey) 字樣,這是確認環境是否生效的最快方式。
第二步:用一行 Python 程式同時載入 DuckDB、Polars、pandas,並做版本驗證。這是之後 45 篇每天第一個會跑的指令,把它做成 check_env.py:
"""de-journey/check_env.py:每天開場先跑這個,確認環境到位。"""
import sys
import duckdb
import polars as pl
import pandas as pd
print("Python:", sys.version.split()[0]) # 輸出:Python:3.13.1
print("DuckDB:", duckdb.__version__) # 輸出:DuckDB:1.4.1
print("Polars:", pl.__version__) # 輸出:Polars:1.33.0
print("pandas:", pd.__version__) # 輸出:pandas:2.3.0
con = duckdb.connect() # 開啟記憶體 DuckDB,之後的範例會大量用到
print("DuckDB 連線成功:", con.execute("SELECT 1 AS one").fetchone()) # 輸出:(1,)
這個腳本每次建新環境或升級套件後都跑一次。輸出第三行的 DuckDB 版本字串確認了我們用的是 2025 年的 1.4 世代,後續章節的 SQL 範例都會跑得起來。DuckDB 內建的 connect() 不給檔名時會開在記憶體,適合練習與測試;要做正式落地時再傳入檔案路徑,這部分 Day 8 與 Day 9 會展開。
第三步:用 DuckDB 內建的 sample dataset 做一個最小可跑的分析。這裡我們呼叫 DuckDB 自帶的 nyc_taxi(),這是 NYC 計程車公開資料的縮減版,授權為開放原始碼示範資料,可直接 query。
import duckdb
con = duckdb.connect("warehouse/de-journey.duckdb") # 落地成檔案,便於後續章節沿用
con.execute("CREATE SCHEMA IF NOT EXISTS raw")
con.execute(
"CREATE OR REPLACE TABLE raw.nyc_taxi AS "
"SELECT * FROM read_csv_auto('data/nyc_taxi_sample.csv.gz')"
)
print("資料筆數:", con.execute("SELECT COUNT(*) FROM raw.nyc_taxi").fetchone()[0])
由於 data/ 還沒有實際檔案,這段請先放在一旁,等 Day 8 教 DuckDB 落地時再跑。故意在 Day 1 放這段是為了讓你看到「管線落地」的最終形態:即使今天還不會寫,每天讀到「落地」兩個字都會知道它在講什麼。為了避免讀者誤以為範例會失敗,請在實際執行前先把 taxi_sample.csv.gz 用 DuckDB CLI 下載:
curl -L -o data/nyc_taxi_sample.csv.gz https://duckdb.org/data/nyc_taxi_sample.csv.gz
ls -lh data/nyc_taxi_sample.csv.gz
輸出會看到一個約 4.4 MB 的壓縮檔,這是 DuckDB 官方用於示範的子集合。若日後無法連到該網址,請改用政府開放資料範例:先在 pip install datasets 後載入 Kaggle 上的公開示範。這套資料以後會在 Day 18 的開放資料實戰篇被替換為 data.gov.tw 的真實來源,這裡只是暖身。
第四步:用 Polars 簡單讀一個 CSV,做一次最常見的「讀檔、看頭尾」。這個動作將在 Day 11 與 Day 12 被展開,這裡先讓你確認 Polars 已安裝且可用:
import polars as pl
df = pl.DataFrame({
"city": ["Taipei", "Taichung", "Tainan", "Kaohsiung"],
"pop": [2620000, 2820000, 1870000, 2770000],
})
print(df.head()) # 輸出:Polars DataFrame 4 列
print(df.select(pl.col("pop").sum().alias("total"))) # 輸出:total=10,080,000
輸出請以你自己機器跑出來的為準,數字可能因示範資料而略有不同。Polars 的 head() 預設取前 5 列,這個串列方法可以串接,整個系列裡你會反覆看到 select、filter、group_by、agg 等組合。Polars 用「延遲執行 + 多執行緒」的方式運算,在普通筆電上對數百萬列的資料都能跑得很順,這也是我們選它做工具主角的原因之一。
第五步:用 pandas 與 DuckDB 互動,把 DataFrame 直接丟給 DuckDB 做 SQL,這是 Day 12 會談到的「混用策略」的最簡版:
import duckdb
import pandas as pd
df = pd.DataFrame({
"store": ["A", "A", "B", "B", "C"],
"qty": [10, 20, 15, 5, 30],
})
result = duckdb.query(
"SELECT store, SUM(qty) AS total FROM df GROUP BY store ORDER BY total DESC"
).df()
print(result) # 輸出:3 列的 pandas DataFrame
輸出(依示範資料而略有不同):
store total
2 C 30
0 A 30
1 B 20
這段展示了 DuckDB 的另一個強項:把 Python 物件(DataFrame)當成暫存表,直接用 SQL 查詢,不需要先把資料寫成 CSV 再讀回。這個動作在「pandas 清洗後丟給 DuckDB 做彙總」場景特別好用,我們在 Day 12 會用一個更完整的例子展示。
第六步:在工作目錄放一個共用的 logging 設定檔,之後整個系列都會沿用。這是 Day 21-22 排程與監控的伏筆,今天先給你一個最簡單版本:
"""de-journey/log_setup.py:之後所有管線共用這份 logging 設定。"""
import logging
from pathlib import Path
LOG_DIR = Path(__file__).parent / "logs"
LOG_DIR.mkdir(exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
handlers=[
logging.FileHandler(LOG_DIR / "pipeline.log", encoding="utf-8"),
logging.StreamHandler(),
],
)
log = logging.getLogger("de-journey")
log.info("logging 啟動") # 輸出:2025-11-17 12:00:00,123 INFO de-journey logging 啟動
這段雖然簡短,卻是後續所有管線的共同起點。basicConfig 設定同時輸出到檔案與終端機,訊息包含時間、等級、logger 名稱與訊息本身,足以滿足 Day 32 品質檢查與監控的需求。Windows 的預設編碼走 cp950,這裡以 encoding="utf-8" 強制寫入 UTF-8,避免中文訊息出現亂碼。
第七步:用 DuckDB CLI 直接驗證我們對 DuckDB SQL 的熟悉度,這是 Day 3 開始寫視窗函式前的暖身:
import duckdb
con = duckdb.connect()
con.execute("CREATE TABLE cities AS SELECT * FROM (VALUES ('Taipei', 2620000), ('Taichung', 2820000)) AS t(name, pop)")
print(con.execute("SELECT name, pop, pop * 1.0 / 1000000 AS pop_million FROM cities").df())
輸出:
name pop pop_million
0 Taipei 2620000 2.6200
1 Taichung 2820000 2.8200
這段示範了 DuckDB 與 pandas DataFrame 的雙向互通:.df() 把 SQL 結果直接變成 DataFrame,VALUES 子句可在沒有真實資料表時快速生成臨時資料。這是 Day 3 視窗函式入門會大量用到的小技巧,今天先用一兩分鐘熟悉介面。
第八步:建立一個最小的 dbt 專案,驗證 dbt-core 與 dbt-duckdb 已經能跑:
cd models
uv run dbt init de_demo --profile duckdb_demo
cd de_demo
uv run dbt debug # 應該輸出「All checks passed!」
輸出(簡化節錄,請以你機器的實際輸出為準):
dbt version: 1.10.0
python version: 3.13.1
python path: .../.venv/bin/python
...
Connection test: OK
All checks passed!
dbt 會把模型看成一系列相依的 SELECT 陳述式,由它自動管理執行順序與重跑策略。Day 25 與 Day 26 會展開 materialization、測試與文件。在這之前,請先確認 dbt debug 能順利跑完。這個 dbt 專案結構之後會被刪掉重新做,今天的目的只是要看到 All checks passed!。
常見錯誤與踩雷
第一次建環境最常踩到的坑有三個,分別是版本衝突、虛擬環境沒啟動、以及忘了接地區。第一個是 Python 3.13 在 macOS 上用 Homebrew 安裝時,Xcode 命令列工具版本太舊會導致某些套件編譯失敗,請先升級 CLT 再裝 uv。第二個是 PowerShell 的啟動指令是 .venv\Scripts\Activate.ps1,不是 source activate,很多人會把 Linux 的寫法直接貼過去,最後沒啟動、套件裝到全域環境裡。第三個是 DuckDB 的 CSV 讀取對中文編碼敏感,中文常見錯誤會在 Day 13 詳述,這裡先提醒你提前用編輯器把 CSV 轉成 UTF-8(無 BOM)。
另一個常見錯誤是用系統 Python 直接裝套件,導致最後整台機器的環境被污染。請務必先啟動虛擬環境再執行 uv pip install,並在 check_env.py 的第一行輸出看到正確的 Python 路徑。除此之外,dbt-core 與 dbt-duckdb 的版本要對齊,這對 2025 年 11 月來說都會是 1.10.x;如果你從教學文章複製舊版安裝指令,可能會看到「無法找到 adapter」的錯誤。
最後一個踩雷:很多人會把工作目錄放在 OneDrive、iCloud 或 Dropbox 上。這類雲端同步會在檔案被外部修改時觸發「檔案被鎖定」或「無法寫入」的錯誤,DuckDB 的單一檔案尤其敏感。請把 de-journey/ 放在本機磁碟,等一切都穩定再考慮備份策略。
效能與實務提醒
資料工程常被誤會成「用越多記憶體越好」,其實更關鍵的是工作流是否可重現、是否可交接。請把 de-journey/ 視為一個專案:所有指令、版本、結果都要能由另一位工程師接手。實務上我會建議這樣分配時間:環境與目錄結構 10%、SQL 與查詢計畫 30%、工具(pandas、Polars、DuckDB)的特性與分工 20%、管線與排程 25%、文件化與監控 15%。這個比例也是系列文章分配比例的參考,SQL 與管線是重點,環境與展示只是前置與收尾。
效能面上,DuckDB 的欄式引擎對聚合查詢通常快過一般 OLTP 資料庫,但在極長字串或隨機讀寫上不一定贏 Polars。請記住一個口訣:「讀進 DuckDB 做 SQL、寬表運算交給 Polars、最後要的彙總表才回 DuckDB」。這個分工在 Day 12 會用一個真實管線示範,但今天的 check_env.py 已經能用最少的程式碼展示基本動作。
另外,請從 Day 2 開始就啟用 logging。預設的 print 在自動化環境不容易被監控串接,Day 21 與 Day 22 會改用 logging 模組把訊息輸出到 logs/ 目錄。今天的範例暫不引入,但請你在心裡留一個位置。
小結
今天我們建好了工作目錄、確認了 Python 3.13 + DuckDB 1.4 世代 + Polars 1.33 + pandas 2.3 + dbt-core 1.10 的核心環境,並實際跑了最小可分析的範例:從版本驗證、CSV 落地、DuckDB 連線、Polars DataFrame 操作、pandas 與 DuckDB 互通、到 dbt 專案初始化。這些動作是後續 44 篇的地基,很多篇會用到今天寫過的程式片段。
在術語上,我們建立了「管線、資料倉儲、事實表、維度表、水位標記、冪等、編排、落地」這些資料工程常見詞的對應關係,這些詞彙會在 Day 20 之後大量出現,今天先混個臉熟。把它們整理成一張表存在你的筆記本裡:
- 管線(pipeline):把資料從來源處理到目的地的整條工作流。
- 資料倉儲(warehouse):集中存放已整理資料的系統,DuckDB 檔案是最小單位。
- 事實表(fact table):存放事件或量測值的窄表,例如訂單、註冊、登入紀錄。
- 維度表(dimension table):存放屬性或描述的寬表,例如客戶、商品、日期。
- 水位標記(watermark):增量載入時,記錄「目前處理到哪裡」的欄位或數值。
- 冪等(idempotent):同一段工作重複執行得到一樣結果的特性。
- 編排(orchestration):把多個步驟組合成可重複的工作流,例如 Airflow。
- 落地(land):把原始檔案保留起來以利重跑或稽核。
這些詞彙不是術語炫耀,而是你接下來 44 天會反覆用到的「共同語言」。把這個表抄進你的筆記,並把今天能跑的指令列一份清單:uv venv --python 3.13、uv pip install ...、python check_env.py、dbt init、dbt debug。明天開始,我們會正式用 DuckDB 與 Polars 來執行第一個真實的分析。
結語
系列的第一篇結束了。我們沒有急著寫 SQL,而是把環境、術語、與整體地圖先穩穩打好。接下來 Day 2 會把這套地圖放大到「整個系列會用到的工具鏈」,詳細說明 uv、DuckDB、Polars、dbt 在哪裡用、怎麼用,順手把另一個關鍵工具 Airflow 的概念先建立起來。我們會把今天建立的目錄結構再擴充,把 logging、套件鎖定檔 uv.lock、與 CI 雛形都加進來。
明天,我們會深入「環境與工具鏈」,把整個系列會用到的工具版本、上下游關係、安裝指令與驗證方式一次整理好;並用一個可以整段執行的 shell 腳本,把所有套件裝起來並驗證。這樣從 Day 3 開始,我們就不會再為環境分心,只專注在資料處理本身。
延伸資源
- DuckDB 官方文件:
https://duckdb.org/docs/。本章節的 sample dataset 與read_csv_auto()用法以這份文件為準。 - Polars 使用者指南:
https://pola.rs/。官方提供資料集與 API 參考,是入門與進階都適用的入口。 - dbt 文件:
https://docs.getdbt.com/。本系列在 Day 25 與 Day 26 採用 dbt-core 1.10 與 dbt-duckdb 1.10 的設定。 - uv 官方說明:
https://docs.astral.sh/uv/。如果你之前只用過 pip 或 poetry,這份指南能幫你短時間上手。 - 政府資料開放平臺:
https://data.gov.tw/。Day 18 的真實管線會用這裡的資料集,採「政府資料開放授權條款第 1 版」。
留言
張貼留言