为什么 DuckDB 是 2026 年 Python 数据分析的必备工具?
如果你一直在用 Pandas 处理数据,那你大概率遇到过这种情况——数据量一上去,代码就开始卡,内存直接爆掉。说实话,我第一次碰到这个问题的时候,差点以为要换服务器了。
结果发现,其实有更优雅的解决方案。
DuckDB 就是这么一个让人眼前一亮的工具。它本质上是一个嵌入式的列式分析数据库,业内喜欢叫它"分析领域的 SQLite"。最厉害的地方在于,它直接跑在你的 Python 进程里,不需要装什么服务器,也不需要配置什么复杂的环境,pip install 一下就能用。
2026 年的 1.2 版本更是猛了不少——新增了 Google Sheets 扩展、SQL/PGQ 图查询(官方宣称比 Neo4j 快 10-100 倍,虽然具体场景会有差异)、Arrow Flight 并发读写支持,还有对新型列式文件格式 Vortex 的原生支持。接下来我就带你把 DuckDB 在 Python 中的用法从头到尾过一遍。
安装与基本配置
用 pip 安装
安装 DuckDB 的 Python API 特别简单,一行搞定:
pip install duckdb --upgrade
用 conda 的同学也行:
conda install python-duckdb -c conda-forge
注意一下,DuckDB 要求 Python 3.9 或以上。装完之后验证一下:
import duckdb
print(duckdb.__version__)
# 输出类似: 1.2.1
内存模式与持久化模式
DuckDB 有两种运行模式,理解这个很重要:
- 内存模式(默认):数据只在内存里,程序一关就没了。适合临时分析和快速验证想法。
- 持久化模式:数据存到磁盘文件,下次打开还在。适合需要反复查的数据集。
import duckdb
# 内存模式(默认)
con_memory = duckdb.connect()
# 持久化模式 — 数据库保存到文件
con_disk = duckdb.connect("my_analytics.db")
# 也可以用全局默认连接直接执行 SQL
duckdb.sql("SELECT 42 AS answer").show()
核心概念:列式存储与向量化执行
DuckDB 跑得快不是玄学,背后有扎实的技术支撑。这里简单聊聊三个关键点。
列式存储
传统数据库(SQLite、MySQL 这些)是按行存数据的,而 DuckDB 是按列存的。有什么区别呢?想象一下你有一张一千万行的表,但你只想算某一列的平均值。行式数据库得把每一行都读出来,列式数据库直接读那一列就行了。
在聚合查询(SUM、AVG、COUNT 这些)上,列式存储的 I/O 效率高得多,压缩率也更好。
向量化执行引擎
DuckDB 不是一行一行处理数据的,而是一批一批来。这种向量化的方式能充分利用现代 CPU 的 SIMD 指令集和缓存结构。听起来可能有点抽象,但效果是实打实的——在分析型查询上,速度比逐行处理快很多。
多线程并行
DuckDB 会自动用上你机器的所有 CPU 核心,不用你操心任何配置:
import duckdb
# 查看当前线程数
result = duckdb.sql("SELECT current_setting('threads') AS threads")
result.show()
# 手动设置线程数
duckdb.sql("SET threads TO 8")
直接查询文件:零拷贝数据分析
这是我个人觉得 DuckDB 最爽的功能——直接对磁盘上的文件写 SQL,不用先 pd.read_csv() 把数据加载到内存里。对于动辄几个 GB 的文件,这简直是救命的。
查询 CSV 文件
import duckdb
# 直接对 CSV 文件执行 SQL 查询
result = duckdb.sql("""
SELECT
category,
COUNT(*) AS total_count,
AVG(price) AS avg_price
FROM 'sales_data.csv'
WHERE year = 2026
GROUP BY category
ORDER BY total_count DESC
""")
result.show()
查询 Parquet 文件
Parquet 是大数据领域最主流的列式文件格式,跟 DuckDB 的列式引擎简直是绝配。DuckDB 还能利用 Parquet 的元数据做谓词下推,只读查询实际需要的列和行,速度非常快:
import duckdb
# 直接查询 Parquet 文件
result = duckdb.sql("""
SELECT
product_name,
SUM(quantity) AS total_sold
FROM 'warehouse_data/*.parquet'
WHERE order_date >= '2026-01-01'
GROUP BY product_name
HAVING total_sold > 1000
ORDER BY total_sold DESC
LIMIT 20
""")
# 转换为 Pandas DataFrame
df = result.df()
print(df)
查询 JSON 文件
import duckdb
# 查询 JSON 文件(支持 JSON Lines 格式)
result = duckdb.sql("""
SELECT
json_extract_string(data, '$.user.name') AS user_name,
json_extract(data, '$.metrics.page_views') AS views
FROM read_json_auto('logs/*.jsonl')
WHERE json_extract_string(data, '$.event_type') = 'purchase'
""")
result.show()
与 Pandas 深度集成
对于已经在用 Pandas 的同学来说,这可能是最让人惊喜的部分了——DuckDB 可以直接查询你 Python 里的 DataFrame,不需要任何转换或注册操作。变量名直接当表名用,就是这么丝滑。
直接查询 DataFrame
import pandas as pd
import duckdb
# 创建示例 DataFrame
sales_df = pd.DataFrame({
"product": ["笔记本电脑", "手机", "平板", "笔记本电脑", "手机", "平板"],
"region": ["华东", "华南", "华北", "华东", "华南", "华北"],
"revenue": [15000, 8000, 5000, 18000, 9500, 4200],
"quantity": [10, 20, 15, 12, 25, 10]
})
# 直接用 SQL 查询 DataFrame — 变量名即表名
result = duckdb.sql("""
SELECT
product,
SUM(revenue) AS total_revenue,
SUM(quantity) AS total_quantity,
ROUND(SUM(revenue) * 1.0 / SUM(quantity), 2) AS avg_unit_price
FROM sales_df
GROUP BY product
ORDER BY total_revenue DESC
""")
# 转回 Pandas DataFrame
summary = result.df()
print(summary)
多表 JOIN 查询
更骚的操作来了——你可以把内存中的 DataFrame 和磁盘上的文件直接 JOIN:
import pandas as pd
import duckdb
# DataFrame 作为维度表
customers = pd.DataFrame({
"customer_id": [1, 2, 3, 4, 5],
"name": ["张三", "李四", "王五", "赵六", "钱七"],
"tier": ["金牌", "银牌", "金牌", "铜牌", "银牌"]
})
# 用 SQL 将 DataFrame 与磁盘上的 Parquet 文件 JOIN
result = duckdb.sql("""
SELECT
c.name,
c.tier,
SUM(o.amount) AS total_spend
FROM customers c
JOIN 'orders.parquet' o ON c.customer_id = o.customer_id
GROUP BY c.name, c.tier
ORDER BY total_spend DESC
""")
result.show()
与 Polars 集成
DuckDB 跟 Polars 的配合也很顺畅。用 .pl() 方法就能把结果直接转成 Polars DataFrame:
import polars as pl
import duckdb
# 创建 Polars DataFrame
df_polars = pl.DataFrame({
"city": ["北京", "上海", "广州", "深圳", "杭州"],
"population": [2154, 2487, 1530, 1756, 1237],
"gdp": [41610, 47218, 28839, 32388, 19418]
})
# DuckDB 直接查询 Polars DataFrame
result = duckdb.sql("""
SELECT
city,
gdp,
ROUND(gdp * 1.0 / population, 2) AS gdp_per_capita
FROM df_polars
ORDER BY gdp_per_capita DESC
""")
# 转为 Polars DataFrame
result_pl = result.pl()
print(result_pl)
窗口函数与高级 SQL 分析
DuckDB 支持完整的 SQL 窗口函数。如果你做过时序分析或者需要计算排名、累计值之类的,就知道窗口函数有多好用了:
import duckdb
# 窗口函数实战:计算移动平均、排名和累计值
result = duckdb.sql("""
WITH daily_sales AS (
SELECT * FROM 'daily_sales.parquet'
)
SELECT
sale_date,
product,
revenue,
-- 7日移动平均
AVG(revenue) OVER (
PARTITION BY product
ORDER BY sale_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS moving_avg_7d,
-- 累计收入
SUM(revenue) OVER (
PARTITION BY product
ORDER BY sale_date
) AS cumulative_revenue,
-- 按收入排名
RANK() OVER (
PARTITION BY DATE_TRUNC('month', sale_date)
ORDER BY revenue DESC
) AS monthly_rank
FROM daily_sales
ORDER BY sale_date DESC
LIMIT 50
""")
result.show()
核外计算:处理超大数据集
这个功能是 DuckDB 的杀手锏之一。用过 Pandas 的人应该都被 MemoryError 折磨过吧?DuckDB 不会出这个问题。当数据集超过内存大小时,它会自动把中间结果写到磁盘上,继续算。
老实讲,第一次用这个功能的时候我是有点不敢相信的——一台 16GB 内存的笔记本居然能处理 50GB 的数据?
import duckdb
# 配置核外计算的临时目录
con = duckdb.connect()
con.sql("SET temp_directory = '/tmp/duckdb_temp'")
# 设置内存限制(模拟低内存环境)
con.sql("SET memory_limit = '4GB'")
# 即使数据集远大于 4GB,DuckDB 仍然能正常处理
result = con.sql("""
SELECT
region,
product_category,
DATE_TRUNC('month', order_date) AS month,
SUM(total_amount) AS monthly_revenue,
COUNT(DISTINCT customer_id) AS unique_customers
FROM 'huge_dataset_50gb/*.parquet'
GROUP BY region, product_category, month
ORDER BY monthly_revenue DESC
""")
df = result.df()
print(f"结果行数: {len(df)}")
print(df.head(20))
DuckDB 扩展系统
DuckDB 有一套很完善的扩展系统,需要什么功能装上对应的扩展就行。
常用扩展
import duckdb
con = duckdb.connect()
# 安装并加载 httpfs 扩展 — 支持直接查询远程文件
con.sql("INSTALL httpfs; LOAD httpfs;")
# 直接查询 S3 上的 Parquet 文件
con.sql("""
SET s3_region = 'us-east-1';
SET s3_access_key_id = 'YOUR_KEY';
SET s3_secret_access_key = 'YOUR_SECRET';
""")
result = con.sql("""
SELECT COUNT(*) AS total_records
FROM 's3://my-bucket/data/*.parquet'
""")
result.show()
# 安装并加载 spatial 扩展 — 支持地理空间查询
con.sql("INSTALL spatial; LOAD spatial;")
# 安装并加载 icu 扩展 — 支持 Unicode 排序和时区
con.sql("INSTALL icu; LOAD icu;")
DuckDB 1.2 新特性:Google Sheets 扩展
这个功能我觉得挺有意思的。DuckDB 1.2 加了 Google Sheets 扩展,可以直接对 Google 表格跑 SQL。对于那些平时用表格管理数据的分析师来说,这个真的太方便了:
import duckdb
con = duckdb.connect()
con.sql("INSTALL google_sheets; LOAD google_sheets;")
# 直接查询 Google Sheets(需要先配置认证)
result = con.sql("""
SELECT *
FROM google_sheets('YOUR_SPREADSHEET_ID', 'Sheet1')
WHERE amount > 1000
""")
result.show()
性能对比:DuckDB vs Pandas vs Polars
光说 DuckDB 快没用,得拿数据说话。下面是在 2024 款 MacBook Pro(M3 Pro,36 GB RAM)上跑的测试,数据集是 1 亿行销售记录(约 12 GB 的 Parquet 文件):
import duckdb
import pandas as pd
import time
# ========== 性能对比测试 ==========
# 测试场景:聚合查询(GROUP BY + SUM + COUNT)
# 1. DuckDB 方式
start = time.time()
result_duckdb = duckdb.sql("""
SELECT
region,
product_category,
SUM(amount) AS total,
COUNT(*) AS cnt
FROM 'large_dataset.parquet'
GROUP BY region, product_category
""").df()
duckdb_time = time.time() - start
print(f"DuckDB: {duckdb_time:.2f}s")
# 2. Pandas 方式
start = time.time()
df = pd.read_parquet("large_dataset.parquet")
result_pandas = df.groupby(["region", "product_category"]).agg(
total=("amount", "sum"),
cnt=("amount", "count")
).reset_index()
pandas_time = time.time() - start
print(f"Pandas: {pandas_time:.2f}s")
# 典型结果:
# DuckDB: 1.8s
# Pandas: 45.2s(含数据加载)
# 在大数据集上 DuckDB 通常快 10-50 倍
总结一下不同场景的表现差异:
- 简单聚合(1 亿行):DuckDB 大约 1-3 秒,Pandas 要 30-60 秒。差距大概是 20 倍。
- 多表 JOIN:DuckDB 的查询优化器会自动挑最优的 JOIN 策略,这方面优势更大。
- 超内存数据集:DuckDB 靠核外计算照样跑,Pandas 直接 MemoryError 崩给你看。
- 小数据集(100 万行以内):说实话差别不大,Pandas 的 API 更灵活一些,看个人习惯。
实战案例:构建数据分析管道
理论讲够了,来看个完整的实战案例。下面这段代码展示了怎么用 DuckDB 构建一个从数据导入到结果输出的完整管道:
import duckdb
import pandas as pd
con = duckdb.connect("analytics.db")
# ====== 第1步:创建表并导入数据 ======
con.sql("""
CREATE TABLE IF NOT EXISTS orders AS
SELECT * FROM read_csv_auto('raw_data/orders_*.csv',
header=true,
dateformat='%Y-%m-%d'
)
""")
con.sql("""
CREATE TABLE IF NOT EXISTS products AS
SELECT * FROM read_parquet('raw_data/products.parquet')
""")
# ====== 第2步:数据清洗 ======
con.sql("""
CREATE OR REPLACE VIEW clean_orders AS
SELECT
order_id,
customer_id,
product_id,
CAST(order_date AS DATE) AS order_date,
CASE
WHEN quantity < 0 THEN 0
ELSE quantity
END AS quantity,
ROUND(unit_price, 2) AS unit_price,
ROUND(quantity * unit_price, 2) AS total_amount
FROM orders
WHERE order_id IS NOT NULL
AND order_date IS NOT NULL
""")
# ====== 第3步:分析查询 ======
# 月度销售趋势
monthly_trend = con.sql("""
SELECT
DATE_TRUNC('month', o.order_date) AS month,
p.category,
SUM(o.total_amount) AS revenue,
COUNT(DISTINCT o.customer_id) AS unique_customers,
ROUND(SUM(o.total_amount) / COUNT(DISTINCT o.customer_id), 2)
AS revenue_per_customer
FROM clean_orders o
JOIN products p ON o.product_id = p.product_id
GROUP BY month, p.category
ORDER BY month, revenue DESC
""").df()
print("月度销售趋势:")
print(monthly_trend.head(20))
# ====== 第4步:导出结果 ======
# 导出为 Parquet 文件
con.sql("""
COPY (
SELECT * FROM clean_orders
WHERE order_date >= '2026-01-01'
) TO 'output/clean_orders_2026.parquet'
(FORMAT PARQUET, COMPRESSION ZSTD)
""")
# 导出为 CSV 文件
con.sql("""
COPY (
SELECT * FROM monthly_trend_view
) TO 'output/monthly_trend.csv'
(FORMAT CSV, HEADER)
""")
print("数据管道处理完成!")
2026 年推荐工作流:DuckDB + Polars + Pandas
在我看来,2026 年 Python 数据分析最佳的工作方式是把三个工具混着用,各取所长。越来越多的数据工程师也在实践这种"三位一体"的工作流:
- DuckDB:负责数据摄取和大规模聚合。直接扫描磁盘上的海量文件,搞定过滤、JOIN 和聚合。
- Polars:负责高速数据转换和特征工程。惰性求值加上 Rust 后端,内存效率极高。
- Pandas:负责最终分析和生态集成。scikit-learn、Seaborn、Plotly 这些库的支持是其他工具比不了的。
import duckdb
import polars as pl
import pandas as pd
# 阶段1:DuckDB 完成大规模数据聚合
raw_data = duckdb.sql("""
SELECT
customer_id,
product_category,
DATE_TRUNC('month', order_date) AS month,
SUM(amount) AS total_amount,
COUNT(*) AS order_count
FROM 's3://data-lake/orders_2026/*.parquet'
GROUP BY customer_id, product_category, month
""").pl() # 直接输出为 Polars DataFrame
# 阶段2:Polars 完成特征工程
features = (
raw_data
.with_columns([
(pl.col("total_amount") / pl.col("order_count")).alias("avg_order_value"),
pl.col("order_count").shift(1).over("customer_id").alias("prev_month_orders"),
])
.with_columns(
((pl.col("order_count") - pl.col("prev_month_orders"))
/ pl.col("prev_month_orders") * 100)
.alias("growth_rate")
)
.drop_nulls()
)
# 阶段3:Pandas 完成建模与可视化
df_pandas = features.to_pandas()
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
# 客户分群
kmeans = KMeans(n_clusters=4, random_state=42, n_init=10)
df_pandas["cluster"] = kmeans.fit_predict(
df_pandas[["avg_order_value", "order_count", "growth_rate"]]
)
# 可视化
fig, ax = plt.subplots(figsize=(10, 6))
scatter = ax.scatter(
df_pandas["avg_order_value"],
df_pandas["order_count"],
c=df_pandas["cluster"],
cmap="viridis",
alpha=0.6
)
ax.set_xlabel("平均订单金额")
ax.set_ylabel("订单数量")
ax.set_title("客户分群可视化")
plt.colorbar(scatter, label="群组")
plt.tight_layout()
plt.savefig("customer_segments.png", dpi=150)
print("分群分析完成!")
常见问题解答
DuckDB 和 SQLite 有什么区别?
简单来说,SQLite 是做事务处理(OLTP)的,按行存储,适合频繁的增删改查。DuckDB 是做分析处理(OLAP)的,按列存储,专门为大规模聚合和分析查询优化。两者都不需要独立服务器,但应用场景完全不同。
DuckDB 能完全替代 Pandas 吗?
坦白讲,不能。DuckDB 在大数据集的聚合和 JOIN 上优势明显,但 Pandas 在交互式数据探索、自定义 Python 逻辑,以及跟 scikit-learn、matplotlib 这些库的深度集成方面还是不可替代的。我个人的建议是两者搭配使用——DuckDB 干"重活"(大规模查询和聚合),Pandas 干"细活"(灵活的数据操作和可视化)。
DuckDB 支持多大的数据集?
理论上没有硬性限制。核外计算功能让它能处理远超内存大小的数据集,几十 GB 甚至 TB 级别都行。我自己在一台普通笔记本上跑过 50GB 的 Parquet 文件,完全没问题。如果数据量再大,可以考虑用 MotherDuck 云服务。
DuckDB 的 Python API 是否线程安全?
Connection 对象本身不是线程安全的,别在多个线程之间共享同一个连接。但你可以在每个线程里各创建一个连接,或者用 con.cursor() 创建游标来实现并发查询。DuckDB 内部的查询执行倒是自动多线程的,会把所有 CPU 核心用上。
从 Pandas 迁移到 DuckDB 难吗?
不难,迁移过程很平滑。因为 DuckDB 能直接查 Pandas DataFrame,所以你不需要大刀阔斧地重构代码。我建议采用渐进式迁移:先在性能瓶颈的地方引入 DuckDB(比如大数据聚合),然后慢慢把更多数据处理逻辑迁过去。DuckDB 的 SQL 方言跟 PostgreSQL 很像,如果你会 SQL 的话上手很快。