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