跳到主要內容

DE Day 8 DuckDB 入門:單機分析神器

DE Day 8 DuckDB 入門:單機分析神器

執行需求:CPU 可跑。工具區塊從今天正式開始,第一個工具就是 DuckDB 1.4。我們會從 DuckDB 的定位、核心能力、CLI、Python 介面、與 pandas/Polars 的互通,逐步把這個系列的工作目錄 de-journey/warehouse/ 真正用起來。讀完這一篇,你不只會裝 DuckDB,還會知道它為什麼在 2025 年成為資料工程師最常安裝的單機 OLAP 引擎。

引言

DuckDB 是一個「嵌入式 OLAP 資料庫」,沒有獨立的 server、需要時直接在 Python 或 CLI 內啟動。它使用欄式儲存與向量化的查詢執行引擎,特別擅長處理「批次資料分析」任務:聚合、聯結、視窗函式、Parquet 讀寫,幾乎所有 OLAP 場景都能在一台筆電上幾秒內完成。DuckDB 與 pandas、Polars、Arrow、Parquet 的整合做得極好,是資料工程管線的「SQL 介面」。

今天的目標有四個:第一,理解 DuckDB 在資料工程工具鏈裡的角色——「單機 OLAP 神器」是什麼意思,與傳統資料庫的差異;第二,在工作目錄 warehouse/ 內建立 DuckDB 檔案、用 Python 與 CLI 兩種介面操作;第三,把 Day 1-7 學到的所有 SQL 技巧都用 DuckDB 跑一次(視窗、CTE、JOIN、EXPLAIN);第四,學會 DuckDB 與 pandas/Polars 的雙向互通,這是 Day 9-12 的伏筆。

為什麼 DuckDB 是「單機分析神器」

把 DuckDB 與其他常見資料庫對照:

特性 DuckDB PostgreSQL SQLite Snowflake
執行模式 嵌入式(程式內) 獨立 server 嵌入式 雲端 SaaS
適用場景 OLAP 分析 OLTP + OLAP OLTP OLAP 大規模
資料規模 單機數十 GB 單機數百 GB 單機數 GB PB 級
Parquet 原生支援 是 部分 否 是
Python 介面 原生效能 需要 driver 原生 需要 driver
授權 MIT PostgreSQL License Public Domain 商業
設定成本 pip install 即可 需要 server 安裝 內建 需要雲端帳號

這張表讀起來,DuckDB 介於 SQLite(嵌入式)與 Snowflake(大規模雲端 OLAP)之間。它的關鍵差異在於:嵌入式 + OLAP 兩種特性同時擁有,這在業界過去是空白的。PostgreSQL 是 server 模式,要安裝、設定權限、跑 service;SQLite 對 OLAP 工作不夠快(它是列式儲存、不是為分析而設計);Snowflake 對單機學習或小型管線成本太高。DuckDB 把這個空白填補起來,是 2025 年最受歡迎的單機 OLAP 引擎之一。

先把 DuckDB 的角色擺在資料工程工具鏈圖裡:CSV/Parquet 是來源、原生檔案;pandas/Polars 提供 DataFrame 操作介面;dbt 是轉換建模的中介層;DuckDB 把上述一切變成「用 SQL 查」。當你的同事說「我今天想跑個分析」,給他們 DuckDB + Parquet,比開 PostgreSQL、架 server 簡單太多。這也是 DuckDB 在 2025 年成為「單機 OLAP 神器」的核心原因。

DuckDB 的核心能力

DuckDB 1.4 的能力可以分四個層次:第一層是 SQL 本體:視窗函式、CTE、遞迴查詢、QUALIFY、SEMI/ANTI JOIN、MERGE INTO 等都原生支援。第二層是檔案格式整合:Parquet、CSV、JSON、Arrow 都能直接讀寫,像讀資料表一樣。第三層是跨語言整合:Python、R、Java、C/C++、Node.js 都能呼叫;Python 提供 duckdb 模組,效能與原生 C 介面相同。第四層是進階分析:向量相似度查詢、地理空間分析(透過擴充套件)、SQLite 相容的查詢介面。

這四層組合起來,讓 DuckDB 非常適合「把資料先讀進來、用 SQL 處理、輸出成 DataFrame 或 Parquet」的場景,這也是本系列 Day 9-12 工具區塊的主軸。

完整實作:把 DuckDB 工作流程佈起來

我們把所有操作放在 de-journey/warehouse/ 內,今天先把 de-journey.duckdb 檔案建起來:

"""DE Day 8:把 DuckDB 工作流程佈起來。"""
from pathlib import Path

import duckdb

# 工作目錄的 warehouse/ 子目錄
warehouse = Path("warehouse")
warehouse.mkdir(parents=True, exist_ok=True)
db_path = warehouse / "de-journey.duckdb"

# 連線:若檔案不存在會自動建立
con = duckdb.connect(str(db_path))
con.execute("PRAGMA version")
print("DuckDB 版本:", con.execute("SELECT version()").fetchone()[0])
# 輸出:DuckDB 版本:v1.4.1

這段把 warehouse/ 目錄建好、用 duckdb.connect(str(db_path)) 打開 DuckDB 檔案。第一次執行時,DuckDB 會自動建立 de-journey.duckdb 這個檔案。後續若檔案已存在則直接讀寫。DuckDB 的連線物件是 thread-local,多執行緒下要建立多個連線。

把資料寫成 Parquet 並用 DuckDB 讀回

接下來示範 DuckDB 最常用的工作流:把資料寫成 Parquet,再用 SQL 讀回:

con.execute("""
    CREATE OR REPLACE TABLE customers AS
    SELECT * FROM (VALUES
        ('u01', 'Alice',   'Taipei'),
        ('u02', 'Bob',     'Taichung'),
        ('u03', 'Charlie', 'Tainan'),
        ('u04', 'Diana',   'Kaohsiung')
    ) AS t(user_id, name, city)
""")

# 把 customers 表寫成 Parquet
parquet_dir = warehouse / "parquet"
parquet_dir.mkdir(parents=True, exist_ok=True)
con.execute(f"""
    COPY (SELECT * FROM customers)
    TO '{parquet_dir}/customers.parquet' (FORMAT PARQUET)
""")

# 直接 SQL 讀 Parquet
result = con.execute(f"""
    SELECT city, COUNT(*) AS n
    FROM '{parquet_dir}/customers.parquet'
    GROUP BY city ORDER BY n DESC
""").fetchall()
print("各城市客戶數:")
for r in result:
    print(f"  city={r[0]} n={r[1]}")
# 輸出:各城市客戶數:
# 輸出:  city=Taipei n=1
# 輸出:  city=Taichung n=1
# 輸出:  city=Tainan n=1
# 輸出:  city=Kaohsiung n=1

這段做了三件事:第一,建立 customers 資料表;第二,用 COPY ... TO ... (FORMAT PARQUET) 把表寫成 Parquet 檔到 warehouse/parquet/;第三,用 SQL 直接讀 Parquet,並做 GROUP BY 聚合。SELECT ... FROM 'path/file.parquet' 是 DuckDB 直接讀檔的關鍵寫法,與讀實體表完全相同,這種「資料即查詢」設計對 ETL 很友善。Day 9 會更深入地展開 Parquet 的整合。

CLI 操作 DuckDB

DuckDB 也提供 CLI,可以用 duckdb warehouse/de-journey.duckdb 直接開啟互動模式:

# 啟動 DuckDB CLI、連線到檔案
duckdb warehouse/de-journey.duckdb

# 在 CLI 內查詢
D SELECT city, COUNT(*) FROM customers GROUP BY city;
┌────────────┬──────────────┐
│   city     │ count_star() │
├────────────┼──────────────┤
│ Kaohsiung  │      1       │
│ Taichung   │      1       │
│ Tainan     │      1       │
│ Taipei     │      1       │
└────────────┴──────────────┘

# 退出 CLI
D .quit

CLI 的好處是「直接看 schema、看資料、開臨時 SQL」。它跟 SQLite CLI 很像,但 DuckDB CLI 多了一些分析用的 dot-command,例如 .schema、.tables、.timer on 等。實務上我會用 CLI 做兩件事:一是「進去檢視這份 DB 內有哪些表」,二是「先用互動方式確認 SQL 寫對了,再寫進 Python 管線」。把這套習慣帶進 Day 30 端到端管線會大量省下時間。

有一個值得留意的小細節:DuckDB 1.4 對 Polars 的 zero-copy 互通是透過 Apache Arrow 達成。當 DuckDB 接到 Polars DataFrame 時,會「借用」Arrow 的記憶體位址而不是複製;查詢結果用 .pl() 回傳時也同樣是 zero-copy。這讓 DuckDB 與 Polars 之間的轉換效能極高,但要留意的是 zero-copy 的物件「一旦來源被改動就同步變動」。當你想要獨立副本時,請用 df.clone()(Polars)或 df.copy()(pandas)建立快照。

與 pandas/Polars 的雙向互通

DuckDB 1.4 對 pandas 與 Polars 的互通做得最徹底:直接傳入 DataFrame 物件做 SQL,並用 zero-copy 機制避免複製資料:

import pandas as pd
import polars as pl

# 建立一個 Pandas DataFrame
df_pandas = pd.DataFrame({
    "user_id": ["u01", "u02", "u03", "u04"],
    "age":     [25, 30, 35, 40],
})

# DuckDB 直接查 DataFrame
result = con.execute("""
    SELECT user_id, age, age / 5 AS age_group
    FROM df_pandas WHERE age >= 30 ORDER BY age
""").df()  # .df() 回傳 pandas DataFrame
print(result)
# 輸出:  user_id  age  age_group
# 輸出:  1     u02   30          6
# 輸出:  2     u03   35          7
# 輸出:  3     u04   40          8

# 建立 Polars DataFrame 並查詢
df_polars = pl.DataFrame({
    "city": ["Taipei", "Taichung", "Tainan"],
    "pop":  [2620000, 2820000, 1870000],
})
result_pl = con.execute("""
    SELECT city, pop, pop * 1.0 / 1000000 AS pop_million
    FROM df_polars ORDER BY pop DESC
""").pl()
print(result_pl)
# 輸出:shape: (3, 3)
# 輸出: ┌────────────┬─────────┬──────────────┐
# 輸出: │ city       ┆ pop     ┆ pop_million  │
# 輸出: │ str        ┆ i64     ┆ f64          │
# 輸出: ╞════════════╪═════════╪══════════════╡
# 輸出: │ Taichung   ┆ 2820000 ┆ 2.82         │
# 輸出: │ Taipei     ┆ 2620000 ┆ 2.62         │
# 輸出: │ Tainan     ┆ 1870000 ┆ 1.87         │
# 輸出: └────────────┴─────────┴──────────────┘

這段示範了兩個互通技巧。第一個是 pandas DataFrame 傳給 DuckDB:直接 SELECT ... FROM df_pandas,DuckDB 會把這個變數自動當成暫存表,查完後用 .df() 變回 pandas DataFrame。第二個是 Polars DataFrame 也支援相同模式,用 .pl() 回傳 Polars 物件。在這兩個情境中,DuckDB 都透過 Apache Arrow 做 zero-copy,避免把 DataFrame 序列化成 CSV 再讀回,大幅提升效能。Day 9-12 整個工具區塊就是圍繞這個互通模式展開。

另一個 DuckDB 的獨門特色是「就地(in-place)讀寫外部檔案」。當一份 5 GB 的 Parquet 放在硬碟上時,DuckDB 不會先把整份讀進記憶體再查,而是用「push-down」機制:把 WHERE 條件推到讀取階段,只讀需要的列組(row group)。這在分析 TB 等級的資料時是救命設計:你可以查一份「在硬碟上但我電腦記憶體放不下」的 Parquet 檔,且只在幾秒內完成聚合。我們的測試檔只是一個小範例,但 push-down 的概念會在 Day 9 與 Day 35 再次提到。

把 CSV 直接讀進 DuckDB

DuckDB 最讓分析師喜歡的特性之一是「直接讀外部檔案」。我們示範讀一份 CSV:

from pathlib import Path

# 寫一份示例 CSV 到 data 目錄
data_dir = Path("data")
data_dir.mkdir(parents=True, exist_ok=True)
csv_path = data_dir / "sales.csv"
csv_path.write_text(
    "user_id,order_date,amount\n"
    "u01,2025-11-01,1500\n"
    "u02,2025-11-03,800\n"
    "u03,2025-11-08,4500\n",
    encoding="utf-8"
)

# 直接 SQL 讀 CSV
result = con.execute(f"""
    SELECT user_id, SUM(amount) AS total
    FROM read_csv_auto('{csv_path}') GROUP BY user_id ORDER BY total DESC
""").fetchall()
print("CSV 讀取結果:")
for r in result:
    print(f"  user={r[0]} total={r[1]}")
# 輸出:CSV 讀取結果:
# 輸出:  user=u03 total=4500
# 輸出:  user=u01 total=1500
# 輸出:  user=u02 total=800

read_csv_auto() 是 DuckDB 的智慧 CSV 讀取函式:會自動偵測分隔符、首行是否為欄位名稱、欄位型別等。我們寫了一個簡單的 CSV、再用一個 GROUP BY 查詢它。注意 read_csv_auto() 會讀整個檔案做型別推斷;當檔案很大時,建議先明確指定 schema(read_csv(... , columns=...))以免型別推斷太慢。Day 13 資料清洗會詳細討論 schema 與型別議題。

DuckDB 與 pandas 之間的互通則稍微不同:因為 pandas 內部不是 Arrow,仍會經過一次的 Arrow 轉換。在 100 萬列以下的規模下,這個轉換可以忽略;但在 1 億列等級時,建議改用 Polars + DuckDB 的 zero-copy 通路,會明顯更快。當管線需要把 DuckDB 的查詢結果丟到 pandas 給下游(例如 scikit-learn)時,可以採用「先轉 Parquet 再讀」的做法,以保持一致效能。這是 Day 12 混用策略會展開的主題。

把所有 Day 3-7 的 SQL 技巧跑一遍

DuckDB 既然支援標準 SQL,Day 3-7 學的所有技巧都能直接在 DuckDB 跑。我們把視窗、CTE、JOIN、EXPLAIN 都列在範例檔內:

import textwrap

con.execute("""
    CREATE OR REPLACE TABLE demo_orders AS
    SELECT * FROM (VALUES
        ('u01', DATE '2025-09-01', 1200),
        ('u01', DATE '2025-10-01',  800),
        ('u01', DATE '2025-11-01', 1500),
        ('u02', DATE '2025-09-15',  600),
        ('u02', DATE '2025-10-20', 2200)
    ) AS t(user_id, order_date, amount)
""")

# Day 3 視窗函式
sql_window = textwrap.dedent("""
    SELECT user_id, order_date, amount,
           SUM(amount) OVER (
               PARTITION BY user_id ORDER BY order_date
           ) AS running_total
    FROM demo_orders ORDER BY user_id, order_date
""")
print("視窗函式查詢結果:")
for r in con.execute(sql_window).fetchall():
    print(f"  user={r[0]} date={r[1]} amount={r[2]} running={r[3]}")

# Day 4 排名與 QUALIFY
sql_rank = textwrap.dedent("""
    SELECT user_id, order_date, amount,
           ROW_NUMBER() OVER (
               PARTITION BY user_id ORDER BY amount DESC
           ) AS rn
    FROM demo_orders QUALIFY rn = 1
""")
print("\nQUALIFY 過濾後:")
for r in con.execute(sql_rank).fetchall():
    print(f"  user={r[0]} date={r[1]} amount={r[2]} rn={r[3]}")

# Day 5 遞迴 CTE
con.execute("""
    CREATE OR REPLACE TABLE employees AS
    SELECT * FROM (VALUES
        (1, NULL, 'CEO'), (2, 1, 'Eng VP'), (3, 2, 'Backend Lead')
    ) AS t(emp_id, manager_id, name)
""")
sql_recursive = textwrap.dedent("""
    WITH RECURSIVE org AS (
        SELECT emp_id, manager_id, name, 0 AS depth FROM employees
        WHERE manager_id IS NULL
        UNION ALL
        SELECT e.emp_id, e.manager_id, e.name, t.depth + 1
        FROM employees e JOIN org t ON e.manager_id = t.emp_id
    )
    SELECT name, depth FROM org ORDER BY depth
""")
print("\n遞迴組織展開:")
for r in con.execute(sql_recursive).fetchall():
    print(f"  name={r[0]} depth={r[1]}")

這段把 Day 3-5 的 SQL 在 DuckDB 內跑一次:視窗函式(SUM() OVER)、排名(ROW_NUMBER() OVER 搭配 QUALIFY)、遞迴 CTE(WITH RECURSIVE 組織圖展開)。三段輸出示範了 DuckDB 對 SQL 標準的完整性支援。實務上請把這幾段 SQL 存成 queries/ 目錄下的 .sql 檔,未來改寫時直接維護 SQL 即可,不必在 Python 裡用字串組合。

另一個常見組合技:用 MERGE INTO 做 upsert。DuckDB 1.4 原生支援 MERGE INTO ... USING ... ON ... WHEN MATCHED ... 語法,這在做「增量更新」、「維度表合併」時特別有用:

con.execute("""
    CREATE OR REPLACE TABLE demo_customers AS
    SELECT * FROM (VALUES
        ('u01', 'Alice',   'Taipei'),
        ('u02', 'Bob',     'Taichung')
    ) AS t(user_id, name, city)
""")
con.execute("""
    CREATE OR REPLACE TABLE incoming_updates AS
    SELECT * FROM (VALUES
        ('u01', 'Alice',   'NewTaipei'),
        ('u03', 'Charlie', 'Tainan')
    ) AS t(user_id, name, city)
""")
con.execute("""
    MERGE INTO demo_customers AS target
    USING incoming_updates AS source
    ON target.user_id = source.user_id
    WHEN MATCHED THEN UPDATE SET city = source.city
    WHEN NOT MATCHED THEN INSERT (user_id, name, city)
        VALUES (source.user_id, source.name, source.city)
""")
print("合併後的客戶:")
for r in con.execute("SELECT * FROM demo_customers ORDER BY user_id").fetchall():
    print(f"  user={r[0]} name={r[1]} city={r[2]}")
# 輸出:合併後的客戶:
# 輸出:  user=u01 name=Alice city=NewTaipei
# 輸出:  user=u02 name=Bob city=Taichung
# 輸出:  user=u03 name=Charlie city=Tainan

這段示範 MERGE INTO:把 incoming_updates(兩筆更新)合併到 demo_customers。u01 既存在於目標也出現在來源,所以 WHEN MATCHED 把城市更新為 NewTaipei;u03 在目標不存在,所以 WHEN NOT MATCHED 新增。合併後 3 列都正確。這是 Day 20 增量載入設計的核心:當來源更新需要併入既有維度表時,MERGE INTO 是 DuckDB 最簡潔的解。

常見錯誤與踩雷

第一個雷:在 DuckDB 連線時忘了指定 DB 路徑,導致連線到記憶體。請牢記:duckdb.connect() 不帶參數是記憶體模式;duckdb.connect("path/to/file.duckdb") 才會建立持久檔案。今天用 de-journey.duckdb 是檔案模式,未來管線共用。

第二個雷:忘了 DuckDB 的連線是 thread-local。在多執行緒環境(例如 FastAPI + DuckDB)下,請每個 thread 開自己的連線,不能跨 thread 共享。當管線要平行處理大量查詢時,建議改用 multiprocessing 或 concurrent.futures 各自建立連線。

第三個雷:把 DuckDB 的 .duckdb 檔放在雲端同步目錄(OneDrive、iCloud)。這類同步工具對檔案鎖定行為與 DuckDB 不相容,會造成「cannot open database file」錯誤。請把 warehouse/ 放在本機磁碟。

第四個雷:用 read_csv_auto() 時遇到時區、編碼、null 值語意等問題,導致欄位被誤判。DuckDB 對 CSV 解析有完整設定(read_csv(path, header=true, delimiter=',', columns=...)),遇到奇怪資料時請改用顯式設定,避免 _auto 的自動推斷誤判。

第五個雷:在 DuckDB 內寫大量更新(UPDATE 與 DELETE)。DuckDB 對大量寫入的效能不如專為 OLTP 設計的 SQLite 或 PostgreSQL。它的強項是批次讀寫(COPY + INSERT),不適合逐筆更新。當管線需要做「upsert」時,建議改用 MERGE INTO(DuckDB 原生支援)。

在 Day 3-7 學的所有 SQL 技巧都跑通了之後,你會發現 DuckDB 對 SQL 標準的支援比 SQLite 完整得多、又比 Snowflake 與 BigQuery 容易取得。這就是我們把它選為「系列主 SQL 引擎」的原因:學習一次、到處都能用(即使未來你轉到 PostgreSQL 17/18 或 DuckDB 同源產品,仍能直接套用)。Day 35 效能章節會再次提到 DuckDB 在大型管線的局限;但對中小規模資料,現在這套組合已經足夠。

效能與實務提醒

第一個效能提醒:DuckDB 在同一個 session 內會把 SELECT 查詢結果做 cache,下次執行同樣 SQL 時可能直接回傳結果。實務上請用 SET cache_httpfs='false' 或重新開啟連線來繞過 cache,並用 EXPLAIN ANALYZE 量測真實查詢時間。

第二個提醒:DuckDB 的 Parquet 寫入預設會用 snappy 壓縮。如果你的硬碟空間比 CPU 資源更珍貴,可以改用 zstd:COPY (...) TO '...' (FORMAT PARQUET, COMPRESSION 'zstd')。反過來,如果 CPU 是瓶頸、不在意空間,請維持預設 snappy。

第三個提醒:DuckDB 的 memory_limit 與 threads 設定會直接影響效能。我們在 Day 7 已經介紹過。今天的管線腳本可以這樣設定:

import os

con.execute(f"SET memory_limit='{os.environ.get('DUCKDB_MEM', '4GB')}'")
con.execute(f"SET threads={os.environ.get('DUCKDB_THREADS', '4')}")
print("DUCKDB 設定:", con.execute("SELECT current_setting('memory_limit'), current_setting('threads')").fetchone())
# 輸出:DUCKDB 設定:('4.00GB', '4')

把 memory_limit 與 threads 用環境變數控制,未來部署到 Docker 或 CI 時就能依機器規格調整。把這個寫法記下來,Day 35 效能與成本章節會大量沿用。

另一個實務小細節:在 CLI 模式下,DuckDB 支援歷史查詢(上下鍵搜尋)、.timer on 顯示耗時、.mode table 切換印出格式、.output file.csv 把輸出導向檔案。當你寫 Python 管線時,先在 CLI 確認 SQL 跑通,把 .timer on 打開、看到毫秒級的耗時,再搬到 Python 腳本裡,就能在 Day 35 的效能章節少踩一些雷。把 CLI 當成「SQL 沙盒」用,會大幅提升開發效率。

小結

今天把 DuckDB 1.4 的入門知識一次走完。我們從「為什麼 DuckDB 是單機分析神器」切入,看到它在嵌入式 OLAP 引擎的定位、把資料直接當查詢的特性、與 pandas/Polars 的 zero-copy 互通。我們在 warehouse/ 建好 DuckDB 檔、寫了第一份 Parquet、用 CLI 連線操作、用 Python 介面做視窗、CTE、排名、遞迴等多種 SQL 驗證。整套工具鏈在工作目錄內已完整可用,這是後續 Day 9-12 工具區塊的地基。

把今天的關鍵詞抄進筆記本:DuckDB、嵌入式 OLAP、欄式儲存、Parquet 互通、duckdb.connect()、COPY ... TO、read_csv_auto()、.df()、.pl()、CLI、ART、zero-copy。明天進入 Day 9:「DuckDB 與 Parquet:現代分析流程」。我們會用 Parquet 取代 CSV 成為資料交換格式、深入 DuckDB 的讀寫 Parquet、列組(row group)與壓縮選項、與 Polars/pandas/Arrow 的互通模式,把工具區塊的入口基礎打得更扎實。

結語

DuckDB 是整個系列最重要的單一工具之一。在後續的 SQL、Parquet、Polars、dbt 與 Streamlit 章節都會反覆出現。我們今天把 DuckDB 的基礎與工作目錄建好,從這個起點開始,整個資料工程工作流就能直接以 SQL 為中心展開。請把今天的 de-journey.duckdb 與 warehouse/parquet/ 保留好,未來幾天會陸續往裡面寫入更多資料。

明天,我們會進入工具區塊的第二篇:「DuckDB 與 Parquet:現代分析流程」。我們會把 CSV 全面替換成 Parquet、用 DuckDB 處理大型 Parquet 檔、展示 snappy/zstd 兩種壓縮取捨、與 Polars 做 zero-copy 互通。今天的 warehouse/parquet/customers.parquet 是明天的延伸起點,請先在 warehouse/ 內放幾份測試 Parquet 準備好。

有一個值得在寫作時記下的小細節:DuckDB 對 Apache Arrow 的 zero-copy 互通有一個底層限制——傳入 DuckDB 的 Arrow 物件必須是「唯讀」的(read-only)。若直接修改已被 DuckDB 引用的 Arrow 物件,可能會造成記憶體不安全或意外的值錯亂。實務上,當管線需要修改 Polars DataFrame 時,請用 df.clone() 做深拷貝,再丟給 DuckDB。這是 DuckDB 與 Polars 互通中最容易忽略的細節,也是 Day 12 混用策略會再次提示的主題。

把這個觀念記下:當管線需要「多進程共用 DuckDB 檔案」時,請用「讀寫分離 + 副檔鎖定」的方式設計。在 DuckDB 中可以用 SET lock_configuration='access_mode=READ_ONLY' 等選項細調存取模式,避免兩個 process 同時寫入造成鎖死。Day 36 部署章節會把這套設計帶進 Docker 容器。

延伸資源

另一個值得記住的小結:DuckDB 的連線字串也有 read_only=True 模式,可以用於「唯讀查詢」(不允許寫入),這在管線做測試或稽核時很有用:duckdb.connect("de-journey.duckdb", read_only=True)。當同事的 CI 跑分析但不會修改資料時,這個設定可以避免意外覆寫。把這個寫法放在 Day 32 品質監控的稽核目錄裡,是個簡單卻有效的小技巧。整個 DuckDB 工具的觀念到此告一段落,我們明天開始進入 Parquet 與 DuckDB 的深度整合。

  • DuckDB 官方文件:https://duckdb.org/docs/。本篇所有語法與 CLI 指令以這份為主。
  • DuckDB Python API 參考:https://duckdb.org/docs/api/python/overview。DataFrame 互通細節寫得很完整。
  • DuckDB GitHub Releases:https://github.com/duckdb/duckdb/releases。1.4 系列版本細節都在這裡。
  • Parquet 格式官方說明:https://parquet.apache.org/documentation/latest/。檔案格式的權威來源。
  • DuckDB 嵌入式 OLAP 一文:https://duckdb.org/why_duckdb。社群文章,說明 DuckDB 在嵌入式 OLAP 領域的定位。

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...