一. 前言:資料在雲端,不代表要先下載 #
很多資料管線都有同一個儀式:先把 S3 檔案下載到本機,解壓、查詢,最後再清掉暫存檔。
資料只有幾 MB 時沒什麼感覺;bucket 累積幾十 GB 的 Parquet 後,真正浪費時間的常常不是 SQL,而是搬回根本用不到的欄位與月份。
DuckDB 的 httpfs extension 可以直接讀取 HTTPS、S3 與 S3-compatible object storage。
配合 Parquet 的 columnar layout、row group statistics 與 HTTP Range request,查詢只需要取得相關片段。
這篇拍拍君會完成一條可重用的遠端查詢流程:
- 用 HTTPS 公開檔案理解 Range read
- 用 DuckDB Secrets 安全連接 S3 與 Cloudflare R2
- 用 glob 與 Hive partition 掃描多檔資料集
- 用 projection/filter pushdown 減少傳輸
- 用
EXPLAIN ANALYZE找出遠端 I/O 問題
如果還沒碰過 DuckDB,建議先讀 DuckDB 基礎篇;想理解 Parquet schema 與 dataset,則可搭配 PyArrow 實戰。
二. 安裝:DuckDB 加上 httpfs #
建立一個乾淨專案:
uv init duckdb-remote-demo
cd duckdb-remote-demo
uv add duckdb
使用 pip 也可以:
python -m pip install duckdb
進入 DuckDB CLI 或 Python connection 後,安裝並載入 httpfs:
INSTALL httpfs;
LOAD httpfs;
INSTALL 會把 extension 安裝到本機,通常只需一次;LOAD 則載入目前的 DuckDB process。
一般連網環境第一次使用 HTTPS 或 S3 時也會自動載入官方 extension,但部署與除錯時明確寫出兩步,失敗位置比較清楚。
Python 版可以集中在 connection 初始化:
from __future__ import annotations
import duckdb
def open_analytics() -> duckdb.DuckDBPyConnection:
con = duckdb.connect("analytics.duckdb")
con.execute("INSTALL httpfs")
con.execute("LOAD httpfs")
return con
若只是一次性查詢,可改成 duckdb.connect(":memory:")。
會重複查同一批遠端資料時,重用 connection 通常更有效率,因為 metadata 與部分遠端資料可以留在 cache。
三. 先從公開 HTTPS Parquet 開始 #
S3 認證會增加變數,所以先用公開 HTTPS 檔案確認整條路徑:
SELECT
play_name,
count(*) AS line_count
FROM read_parquet(
'https://blobs.duckdb.org/data/shakespeare.parquet'
)
GROUP BY play_name
ORDER BY line_count DESC
LIMIT 5;
DuckDB 會透過 httpfs 讀取遠端檔,不要求你先 curl 到 /tmp。
一開始先看欄位,別急著 SELECT *:
DESCRIBE SELECT *
FROM read_parquet(
'https://blobs.duckdb.org/data/shakespeare.parquet'
);
也可以只看 file-level metadata:
SELECT
num_rows,
num_row_groups,
file_size_bytes,
created_by
FROM parquet_file_metadata(
'https://blobs.duckdb.org/data/shakespeare.parquet'
);
DuckDB 可以先取得 Parquet footer,再用 HTTP Range request 讀需要的 column chunks。 CSV 是 row-oriented text,多數情況仍得把整份內容讀過才能判斷哪些列符合條件。 遠端分析優先選 Parquet,不是因為副檔名比較潮,而是它讓 query engine 有機會少讀很多資料。
四. 連接 S3:用 Secrets Manager 管認證 #
舊文章常用 SET s3_access_key_id 與 SET s3_secret_access_key。
DuckDB 現在建議使用 Secrets Manager:可以設定 type、provider 與 scope,也比較不容易在檢查 settings 時意外曝光認證。
在 AWS CLI 已登入、環境能取得 credentials 時,使用 credential chain:
CREATE OR REPLACE SECRET analytics_s3 (
TYPE s3,
PROVIDER credential_chain,
REGION 'ap-northeast-1',
SCOPE 's3://pypy-analytics/events/'
);
接著直接查詢:
SELECT count(*) AS rows
FROM read_parquet(
's3://pypy-analytics/events/year=2026/month=08/*.parquet'
);
SCOPE 會讓 credential 只套用在指定 prefix。
同一個 process 讀多個組織或環境的 bucket 時,DuckDB 會選擇路徑最精確的 secret scope。
若是短命 API key,也能用 config provider:
CREATE OR REPLACE SECRET temporary_s3 (
TYPE s3,
PROVIDER config,
KEY_ID 'REPLACE_ME',
SECRET 'REPLACE_ME',
SESSION_TOKEN 'REPLACE_ME',
REGION 'ap-northeast-1',
SCOPE 's3://pypy-analytics/events/'
);
這只是語法模板;真實專案不要把值 commit 進 SQL 或 Python。
應由部署平台的 secret store、AWS profile、role 或短期環境變數提供。
CREATE SECRET 預設是 process 生命週期內的 temporary secret。
CREATE PERSISTENT SECRET 雖方便,但官方文件提醒 persistent secrets 會以未加密 binary 格式存在磁碟;共用主機上不要把它當加密保管箱。
五. Cloudflare R2:使用 r2:// 與專用類型 #
R2 提供 S3-compatible API,DuckDB 另外提供 R2 secret type,會依 ACCOUNT_ID 組出 endpoint:
CREATE OR REPLACE SECRET analytics_r2 (
TYPE r2,
PROVIDER config,
KEY_ID 'REPLACE_ME',
SECRET 'REPLACE_ME',
ACCOUNT_ID 'REPLACE_WITH_ACCOUNT_ID',
SCOPE 'r2://pypy-lake/events/'
);
查詢時也使用 r2://:
SELECT
event_type,
count(*) AS event_count
FROM read_parquet(
'r2://pypy-lake/events/year=2026/month=08/*.parquet'
)
GROUP BY event_type
ORDER BY event_count DESC;
R2、MinIO、lakeFS 與其他 S3-compatible service 的支援不一定完全相同。
公開讀取依靠 Range request,私有讀取需要認證,glob 需要 ListObjectsV2,遠端寫入則需要 multipart upload。
所以「能讀一個已知 object」不代表 glob 與寫入也一定能用。
六. 多檔、Glob 與 Hive Partition Pruning #
真實資料湖通常每天或每小時新增一批 Parquet:
s3://pypy-analytics/events/
├── year=2026/month=07/part-000.parquet
├── year=2026/month=08/part-000.parquet
└── year=2026/month=08/part-001.parquet
DuckDB 可以把 Hive-style path 裡的 key 當成 column:
SELECT
event_type,
sum(amount) AS revenue
FROM read_parquet(
's3://pypy-analytics/events/**/*.parquet',
hive_partitioning = true
)
WHERE year = 2026
AND month = 8
GROUP BY event_type
ORDER BY revenue DESC;
這裡有兩層減量:partition filter 先排除整個 object,Parquet filter 再依 row group statistics 跳過不相關資料。
需要追查來源時,選取 filename virtual column:
SELECT filename, event_id, event_type
FROM read_parquet(
's3://pypy-analytics/events/year=2026/month=08/*.parquet'
)
WHERE event_type = 'purchase'
LIMIT 20;
如果不同日期新增 optional column,可使用 union_by_name = true 按欄名對齊,缺少欄位會變成 NULL。
它適合漸進新增欄位,不該拿來掩蓋 incompatible type change。
先檢查 schema 更安全:
SELECT file_name, name, type, logical_type
FROM parquet_schema(
's3://pypy-analytics/events/year=2026/month=08/*.parquet'
)
WHERE name IN ('event_id', 'occurred_at', 'amount');
若需要更通用的 storage API,例如 memory filesystem、zip 或 GCS,可讀 fsspec 實戰;這篇專注 DuckDB 自己的遠端掃描能力。
七. Projection 與 Filter Pushdown:少讀才是真的快 #
昂貴版本通常長這樣:
SELECT *
FROM read_parquet(
's3://pypy-analytics/events/year=2026/month=08/*.parquet'
);
若只要每日營收,應在 SQL 裡直接選欄位、過濾與聚合:
SELECT
CAST(occurred_at AS DATE) AS day,
sum(amount) AS revenue
FROM read_parquet(
's3://pypy-analytics/events/year=2026/month=08/*.parquet'
)
WHERE status = 'paid'
AND occurred_at >= TIMESTAMP '2026-08-20 00:00:00'
GROUP BY day
ORDER BY day;
DuckDB 會自動做兩件事:
- Projection pushdown:只讀
occurred_at、amount、status - Filter pushdown:把條件推進 Parquet scan,用 statistics 跳過 row group
Pushdown 不代表任何 WHERE 都能完全下推。
複雜函式、型別轉換、資料分布與缺少 statistics 都可能降低 pruning 效果。
因此不要憑感覺,要看 query plan:
EXPLAIN ANALYZE
SELECT sum(amount)
FROM read_parquet(
's3://pypy-analytics/events/year=2026/month=08/*.parquet'
)
WHERE status = 'paid'
AND occurred_at >= TIMESTAMP '2026-08-20 00:00:00';
遠端檔案的 EXPLAIN ANALYZE 可以顯示 request 數量與傳輸量。
比較版本時不要只看總秒數;網路會抖,讀了多少 object、發出多少 request、傳輸多少 bytes 更能解釋差異。
八. 用 Metadata 判斷能不能跳過 #
Parquet row group statistics 決定 filter pruning 有多少發揮空間:
SELECT
row_group_id,
path_in_schema,
stats_min_value,
stats_max_value,
row_group_num_rows
FROM parquet_metadata(
's3://pypy-analytics/events/year=2026/month=08/part-000.parquet'
)
WHERE path_in_schema IN ('occurred_at', 'status', 'amount')
ORDER BY row_group_id, path_in_schema;
若每個 row group 的 occurred_at 只涵蓋一天,查單日就能跳過其他 group。
若時間完全亂序,每個 group 的 min/max 都跨整月,filter 仍正確,但 pruning 空間變小。
產生資料時,可以讓常用 filter column 大致有序,並選擇合理 row group size:
COPY (
SELECT * FROM clean_events ORDER BY occurred_at
)
TO 'events-2026-08.parquet' (
FORMAT parquet,
COMPRESSION zstd,
ROW_GROUP_SIZE 100000
);
另外別把 partition 切得太碎。 每分鐘一個小檔會製造大量 LIST、HEAD 與 GET,metadata overhead 甚至可能大於真正掃描資料的時間。 查詢端的 pushdown 上限,往往在資料產生時就已經決定一半。
九. Python 實戰:受控的遠端摘要器 #
把路徑留在程式內,使用者只傳年月,避免任意 SQL 與 bucket path:
from dataclasses import dataclass
import duckdb
@dataclass(frozen=True)
class MonthlyQuery:
year: int
month: int
@property
def parquet_glob(self) -> str:
if not 2020 <= self.year <= 2100:
raise ValueError("year 超出允許範圍")
if not 1 <= self.month <= 12:
raise ValueError("month 必須介於 1 到 12")
return (
"s3://pypy-analytics/events/"
f"year={self.year}/month={self.month:02d}/*.parquet"
)
查詢先在 DuckDB 完成 filter 與 aggregation,再回傳小結果:
SUMMARY_SQL = """
SELECT
event_type,
count(*) AS event_count,
sum(amount) FILTER (WHERE status = 'paid') AS paid_amount
FROM read_parquet(?)
GROUP BY event_type
ORDER BY event_count DESC
"""
def monthly_summary(
con: duckdb.DuckDBPyConnection,
query: MonthlyQuery,
):
return con.execute(
SUMMARY_SQL,
[query.parquet_glob],
).fetch_arrow_table()
最後建立 scoped secret:
con = open_analytics()
con.execute("""
CREATE OR REPLACE SECRET analytics_s3 (
TYPE s3,
PROVIDER credential_chain,
REGION 'ap-northeast-1',
SCOPE 's3://pypy-analytics/events/'
)
""")
print(monthly_summary(con, MonthlyQuery(2026, 8)))
fetch_arrow_table() 適合把小結果交給 PyArrow、Polars 或其他 columnar tool。
不要把整份遠端明細轉成 pandas 之後才過濾,那會放棄前面努力爭取的 pushdown。
十. 常見錯誤與上線檢查 #
Connection error for HTTP HEAD:先查 endpoint、region、DNS 與 TLS;AWS bucket region 不符時,應在 secret 指定正確 region 或 endpoint。
403 Forbidden:檢查 scope、credential 與 IAM;glob 還需要列舉 object 的權限。
單檔能讀但 glob 不行:S3 glob 依賴 ListObjectsV2,自架 service 若未實作完整 list API,就傳入明確檔案清單。
少量查詢仍很慢:用 EXPLAIN ANALYZE 檢查 SELECT *、filter pushdown、partition pruning、tiny files 與 connection 是否反覆重建。
上線前再走一次 checklist:
- Secrets 由平台注入,沒有 key 進入 repo
- Secret 使用最小必要
SCOPE與 IAM 權限 - 查詢明確選欄位,日期與 partition filter 盡早出現
- 用
EXPLAIN ANALYZE記錄 request 與傳輸量 - Schema 演進有檢查,不濫用
union_by_name - 避免大量 tiny Parquet files
- 服務重用 connection,並設定合理 retry 邊界
DuckDB 會 cache 一部分遠端資料與 metadata,可用下列查詢檢查:
FROM duckdb_external_file_cache();
分析資料最好採 immutable object key,再用 manifest 或 partition 指向新版本,避免查詢期間 object 被覆寫。
結語:把 SQL 移到資料旁邊 #
DuckDB 讀遠端 Parquet 的重點,不是少打一個 aws s3 cp。
真正價值是把 projection、filter、partition pruning 與 aggregation 推到掃描階段,只拉回答案需要的 bytes。
httpfs 解決連線,Secrets Manager 管認證,Parquet metadata 提供跳讀線索,EXPLAIN ANALYZE 則讓你確認優化真的發生。
先從公開 HTTPS 練習,再接測試 bucket;先量 request 與 bytes,再談快不快。
拍拍君保證,第一次看到幾十 GB 的 dataset 只傳回幾 MB,就很難再接受「先全部下載再說」了。🦆