快轉到主要內容
  1. 教學文章/

Python DuckDB 實戰:用 SQL 快速分析 CSV 與 Parquet

·6 分鐘· loading · loading · ·
Python Duckdb SQL Parquet Data-Analysis Developer-Tools
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 56: 本文

featured

一. 前言:資料就在檔案裡,為什麼還要搬來搬去?
#

你有沒有遇過這種情境:一包 CSV 丟過來,要你「快速看一下趨勢」;一個資料夾每天吐出 Parquet,要你抽幾個欄位算報表;資料不大到需要 Spark,但用 pandas 全部讀進記憶體又有點煩。 這時候 DuckDB 就很香。 DuckDB 可以把它想成「SQLite 的分析型表親」。SQLite 很適合做輕量 transactional database;DuckDB 則很適合做本機 OLAP:直接查 CSV、直接查 Parquet、直接在 Python 裡跑 SQL,而且不用開 server。 拍拍君最喜歡它的一點是:你可以把散在資料夾裡的檔案,當成一個可以查詢的小型資料倉儲。 如果你之前看過拍拍君寫的 Polarssqlite3,可以這樣分工:

  • Polars:用 DataFrame expression 處理資料。
  • sqlite3:用內嵌資料庫保存應用資料。
  • DuckDB:用 SQL 直接分析本機檔案與 columnar data。 三個都很棒,只是戰場不一樣。今天我們就用 DuckDB 做一個小型銷售資料分析流程。

二. 安裝:一個套件就能開始查資料
#

先建立專案:

mkdir duckdb-demo
cd duckdb-demo
uv init
uv add duckdb pandas pyarrow

如果你不用 uv,也可以:

python -m venv .venv
source .venv/bin/activate
pip install duckdb pandas pyarrow

今天會用到三個套件:

  • duckdb:主角,負責查詢。
  • pandas:方便看結果與接後續工具。
  • pyarrow:讓 Parquet 支援更順。 確認一下版本:
python - <<'PY'
import duckdb
print(duckdb.__version__)
PY

DuckDB 沒有需要你啟動的 daemon。沒有 port、帳號密碼、Docker Compose。這種工具很適合放進 notebook、CLI、排程腳本或資料清理流程。

三. 準備範例資料:先做一份銷售紀錄
#

建立 make_data.py

from __future__ import annotations
import csv
from pathlib import Path
from random import Random
rng = Random(42)
products = [
    ("keyboard", "hardware"),
    ("mouse", "hardware"),
    ("monitor", "hardware"),
    ("notebook", "stationery"),
    ("sticker", "stationery"),
    ("coffee", "food"),
]
cities = ["Taipei", "Tainan", "Taichung", "Kaohsiung"]
Path("data").mkdir(exist_ok=True)
with open("data/orders.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(
        f,
        fieldnames=["order_id", "date", "city", "product", "category", "price", "quantity"],
    )
    writer.writeheader()
    for order_id in range(1, 501):
        product, category = rng.choice(products)
        month = rng.randint(1, 4)
        day = rng.randint(1, 28)
        price = rng.choice([60, 120, 250, 800, 1200, 3200])
        quantity = rng.randint(1, 5)
        writer.writerow(
            {
                "order_id": order_id,
                "date": f"2026-{month:02d}-{day:02d}",
                "city": rng.choice(cities),
                "product": product,
                "category": category,
                "price": price,
                "quantity": quantity,
            }
        )

執行:

python make_data.py
head data/orders.csv

你會得到像這樣的資料:

order_id,date,city,product,category,price,quantity
1,2026-01-01,Taichung,coffee,food,60,3
2,2026-01-24,Taipei,mouse,hardware,3200,5

這份資料不大,但夠我們示範 DuckDB 的日常用法。

四. 第一個查詢:直接 SELECT CSV
#

建立 query_csv.py

import duckdb
result = duckdb.sql("""
    SELECT
        city,
        COUNT(*) AS orders,
        SUM(price * quantity) AS revenue
    FROM read_csv_auto('data/orders.csv')
    GROUP BY city
    ORDER BY revenue DESC
""")
print(result)

執行:

python query_csv.py

重點是這一段:

FROM read_csv_auto('data/orders.csv')

DuckDB 會自動推測欄位型別。你不用先建立 table,也不用先 import 資料。CSV 放在那裡,它就可以查。 也可以直接在 SQL 裡做運算:

SUM(price * quantity) AS revenue

這就是 DuckDB 很適合 quick analysis 的地方:資料還在檔案裡,但你已經可以用 SQL 問問題。

五. 把結果拿回 Python:relation、fetch、DataFrame
#

duckdb.sql() 回傳的是 DuckDB relation。你可以直接印出來,也可以拿回 Python 使用。

import duckdb
rel = duckdb.sql("""
    SELECT
        category,
        SUM(price * quantity) AS revenue
    FROM read_csv_auto('data/orders.csv')
    GROUP BY category
    ORDER BY revenue DESC
""")
print(rel.fetchall())
print(rel.columns)

如果想接到 pandas:

df = rel.df()
print(df)
print(df.dtypes)

拍拍君推薦的工作流是:先用 SQL 做篩選、聚合、join;查詢結果縮小後,再轉成 DataFrame。不要一開始就把巨大 CSV 全部讀進 pandas,除非你真的需要。

六. 用 connection 管理狀態
#

前面的 duckdb.sql() 很方便,但比較像快速模式。如果要做多個查詢,可以建立 connection。

import duckdb
con = duckdb.connect(":memory:")
con.execute("""
    CREATE VIEW orders AS
    SELECT *
    FROM read_csv_auto('data/orders.csv')
""")
summary = con.execute("""
    SELECT
        category,
        COUNT(*) AS orders,
        SUM(price * quantity) AS revenue
    FROM orders
    GROUP BY category
    ORDER BY revenue DESC
""").df()
print(summary)

:memory: 代表資料庫只存在記憶體。如果你想保存 view、table 或中間結果,可以用檔案:

con = duckdb.connect("analysis.duckdb")
con.execute("""
    CREATE OR REPLACE TABLE orders AS
    SELECT *
    FROM read_csv_auto('data/orders.csv')
""")

這樣 analysis.duckdb 就會留下來。下次開啟同一個檔案,可以繼續查。

con = duckdb.connect("analysis.duckdb")
print(con.execute("SELECT COUNT(*) FROM orders").fetchone())

簡單說:一次性分析直接查檔案;多步驟分析用 connection;需要保存中間資料就用 .duckdb 檔案。

七. Parquet 才是 DuckDB 的快樂老家
#

DuckDB 可以查 CSV,但它對 Parquet 更開心。Parquet 是 columnar format,分析查詢常常只需要幾個欄位,所以 columnar format 很省。 先把 CSV 轉成 Parquet:

import duckdb
con = duckdb.connect()
con.execute("""
    COPY (
        SELECT *
        FROM read_csv_auto('data/orders.csv')
    ) TO 'data/orders.parquet' (FORMAT PARQUET)
""")

接著直接查 Parquet:

import duckdb
print(duckdb.sql("""
    SELECT
        product,
        SUM(price * quantity) AS revenue
    FROM 'data/orders.parquet'
    GROUP BY product
    ORDER BY revenue DESC
    LIMIT 5
""").df())

注意這裡更懶:

FROM 'data/orders.parquet'

DuckDB 看到副檔名,就知道該怎麼讀。如果你有很多 Parquet,也可以用 glob:

SELECT *
FROM 'data/orders_*.parquet'

或是查分區資料夾:

SELECT *
FROM read_parquet('data/events/year=*/month=*/*.parquet', hive_partitioning = true)

這對日常資料工程很方便。你不一定要有完整 data warehouse,才能享受 SQL 查詢體驗。

八. 參數化查詢:不要用 f-string 拼使用者輸入
#

這個坑很重要。如果查詢條件來自 CLI 參數、表單或外部輸入,不要這樣寫:

city = "Taipei"
sql = f"SELECT * FROM orders WHERE city = '{city}'"  # 不推薦

請用參數化查詢:

import duckdb
con = duckdb.connect()
con.execute("""
    CREATE VIEW orders AS
    SELECT * FROM read_csv_auto('data/orders.csv')
""")
city = "Taipei"
rows = con.execute(
    """
    SELECT
        product,
        SUM(price * quantity) AS revenue
    FROM orders
    WHERE city = ?
    GROUP BY product
    ORDER BY revenue DESC
    """,
    [city],
).fetchall()
print(rows)

? 會由 DuckDB 安全處理。這不只是 SQL injection 問題,也能避免字串引號、日期格式、特殊字元把查詢弄壞。 拍拍君的小規則:查詢結構可以組裝,查詢值不要用字串硬拼。

九. 跟 pandas 混用:不是敵人,是分工
#

DuckDB 跟 pandas 不是互斥關係。你可以先有 pandas DataFrame,再用 DuckDB 查它。

import duckdb
import pandas as pd
orders = pd.read_csv("data/orders.csv")
result = duckdb.sql("""
    SELECT
        city,
        category,
        SUM(price * quantity) AS revenue
    FROM orders
    GROUP BY city, category
    ORDER BY city, revenue DESC
""").df()
print(result)

DuckDB 可以直接看到 Python 變數 orders。如果資料已經在 DataFrame 裡,這樣查很方便;如果資料還在檔案裡,通常直接查檔案更乾淨。 你也可以反過來:

filtered = duckdb.sql("""
    SELECT *
    FROM read_csv_auto('data/orders.csv')
    WHERE category = 'hardware'
""").df()

拍拍君會這樣分工:

任務 適合工具
從 CSV/Parquet 篩選聚合 DuckDB
SQL join 與 group by DuckDB
小型互動式整理 pandas
DataFrame expression pipeline Polars
應用程式內嵌資料保存 sqlite3
工具不是宗教,不用吵架。能準時下班的工具就是好工具。

十. 小專案:做一個銷售摘要 CLI
#

把前面的東西整理成一個小 CLI。建立 sales_summary.py

from __future__ import annotations
import argparse
from pathlib import Path
import duckdb
def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("path", type=Path, help="CSV file")
    parser.add_argument("--city", help="filter by city")
    parser.add_argument("--top", type=int, default=5, help="show top N products")
    return parser.parse_args()
def main() -> None:
    args = parse_args()
    if not args.path.exists():
        raise SystemExit(f"file not found: {args.path}")
    con = duckdb.connect()
    where = ""
    params: list[object] = [str(args.path)]
    if args.city:
        where = "WHERE city = ?"
        params.append(args.city)
    params.append(args.top)
    sql = f"""
        SELECT
            product,
            category,
            SUM(quantity) AS units,
            SUM(price * quantity) AS revenue
        FROM read_csv_auto(?)
        {where}
        GROUP BY product, category
        ORDER BY revenue DESC
        LIMIT ?
    """
    rows = con.execute(sql, params).fetchall()
    for product, category, units, revenue in rows:
        print(f"{product:10s} {category:10s} units={units:4.0f} revenue={revenue:8.0f}")
if __name__ == "__main__":
    main()

執行:

python sales_summary.py data/orders.csv
python sales_summary.py data/orders.csv --city Taipei --top 3

這裡的檔案路徑與查詢值都有參數化。唯一用 f-string 拼進 SQL 的是 where 片段,而且它是我們程式自己決定的白名單邏輯,不是使用者任意輸入。 如果想同時支援 Parquet,可以白名單選 reader:

reader = "read_parquet" if args.path.suffix == ".parquet" else "read_csv_auto"
sql = f"""
    SELECT product, SUM(price * quantity) AS revenue
    FROM {reader}(?)
    GROUP BY product
    ORDER BY revenue DESC
    LIMIT ?
"""

重點是:函式名稱可以由程式白名單決定,值仍然走參數。

十一. 常見實戰技巧
#

1. 只讀需要的欄位
#

不要每次都 SELECT *

SELECT date, city, price, quantity
FROM 'data/orders.parquet'
WHERE city = 'Taipei'

Parquet 可以利用欄位裁切,少讀很多資料。

2. 先聚合再轉 DataFrame
#

不推薦:

df = duckdb.sql("SELECT * FROM 'big.parquet'").df()

比較推薦:

df = duckdb.sql("""
    SELECT city, SUM(price * quantity) AS revenue
    FROM 'big.parquet'
    GROUP BY city
""").df()

讓 DuckDB 先把資料縮小。

3. 用 DESCRIBE 看欄位
#

print(duckdb.sql("DESCRIBE SELECT * FROM read_csv_auto('data/orders.csv')").df())

資料來源不熟時,先看 schema 很值得。

結語:SQL 沒有退流行,只是換了地方住
#

很多 Python 開發者學了 pandas 之後,會暫時忘記 SQL。但 SQL 對資料分析來說仍然非常強。 DuckDB 最棒的地方是,它讓 SQL 回到一個很輕的使用方式:不用架 server、不用先匯入資料、不用離開 Python,也不用為了查幾個檔案開一整套平台。 下次你看到一堆 CSV 或 Parquet,不妨先問自己:這件事是不是用一段 SQL 就能解決?如果答案是 yes,DuckDB 很可能就是今天最省力的選擇。 拍拍君拍一拍資料夾:不要怕,先 SELECT 下去再說。

延伸閱讀
#

Python 學習 - 本文屬於一個選集。
§ 56: 本文

相關文章

Rich + Typer:打造漂亮又好用的 Python CLI 體驗
·10 分鐘· loading · loading
Python Rich Typer Cli Command-Line Developer-Tools
Python Typer 進階:巢狀 subcommands、callback 與 CLI 架構
·9 分鐘· loading · loading
Python Typer Cli Command-Line Developer-Tools Testing
Python argparse 實戰:CLI 參數解析、旗標設計與 subcommands 完全攻略
·9 分鐘· loading · loading
Python Argparse Cli Command-Line Automation Developer-Tools
Streamlit 部署實戰:Secrets、設定檔與雲端上線完整攻略
·8 分鐘· loading · loading
Python Streamlit Deployment Secrets Config Cloud
Streamlit 進階:session_state、cache 與多頁 Dashboard 完全攻略
·11 分鐘· loading · loading
Python Streamlit Dashboard Data-App Cache Session-State
Python uv 進階:workspace、lockfile、script 與專案管理完全攻略
·8 分鐘· loading · loading
Python Uv Workspace Lockfile Packaging Dev Tools