上个月用pandas处理一个50万行的数据集,groupby跑了4.5毫秒。换成DuckDB直查同样的DataFrame,2.5毫秒。
快了不到2倍,看起来没什么大不了。但关键是——我没写一行额外的代码,只是把df.groupby()换成了con.execute()。
这篇文章记录了DuckDB Python客户端的8个核心用法,从最基础的DataFrame直查到自定义UDF,每个都有真实可运行的代码。如果你也在用pandas做数据分析,这些技巧应该能帮你少写很多胶水代码。
环境准备
先装包:
1
| pip install duckdb pandas
|
当前版本:
- DuckDB: 2.0.0
- pandas: 3.1.0
- Python: 3.12
1. 基础连接和查询
DuckDB有两种连接方式:内存模式和持久化模式。
1
2
3
4
5
6
7
| import duckdb
# 内存模式(关闭后数据消失)
con = duckdb.connect()
# 持久化模式(数据存到文件)
con = duckdb.connect('my_database.db')
|
最简单的查询:
1
2
| result = con.execute("SELECT 'Hello DuckDB' as greeting").fetchall()
print(result) # [('Hello DuckDB',)]
|
fetchall()返回元组列表,fetchone()返回单个元组,fetchdf()返回pandas DataFrame。根据后续处理需要选择。
2. pandas DataFrame直查
这是我最常用的功能。不用把DataFrame导入数据库,直接查:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| import pandas as pd
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
'age': [28, 35, 42, 31, 27],
'salary': [75000, 82000, 95000, 68000, 71000],
'dept': ['eng', 'eng', 'product', 'eng', 'product']
})
result = con.execute("""
SELECT dept, AVG(salary) as avg_salary, COUNT(*) as cnt
FROM df
GROUP BY dept
ORDER BY avg_salary DESC
""").fetchdf()
print(result)
# dept avg_salary cnt
# 0 product 83000.0 2
# 1 eng 75000.0 3
|
变量名df就是SQL里的表名。DuckDB会自动识别Python变量里的DataFrame。
3. 参数化查询
拼接SQL字符串是大忌,用参数化查询:
1
2
3
4
5
| name = 'Alice'
result = con.execute(
"SELECT * FROM df WHERE name = ?",
[name]
).fetchdf()
|
?是占位符,第二个参数是值的列表。防止SQL注入,也避免类型转换问题。
4. 持久化数据库
内存模式适合临时分析,持久化模式适合需要保留的数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
| import os
db_path = 'analytics.db'
if os.path.exists(db_path):
os.remove(db_path)
con = duckdb.connect(db_path)
con.execute("CREATE TABLE users AS SELECT * FROM df")
row_count = con.execute("SELECT COUNT(*) FROM users").fetchone()[0]
print(f"Inserted {row_count} rows") # Inserted 5 rows
con.close()
|
下次打开同一个数据库文件,数据还在。
5. 直接查询CSV和JSON
DuckDB可以直接查询文件,不用先导入:
1
2
3
4
5
6
7
8
9
| # 查询CSV
result = con.execute("""
SELECT * FROM read_csv_auto('data.csv') LIMIT 3
""").fetchdf()
# 查询JSON
result = con.execute("""
SELECT * FROM read_json_auto('data.json') LIMIT 3
""").fetchdf()
|
read_csv_auto会自动推断列类型,read_json_auto支持嵌套JSON。
6. 性能对比:pandas vs DuckDB
用10万行数据测试groupby性能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| import numpy as np
np.random.seed(42)
big_df = pd.DataFrame({
'id': range(100000),
'value': np.random.randn(100000),
'category': np.random.choice(['A', 'B', 'C', 'D'], 100000)
})
# pandas groupby
import time
t0 = time.time()
pandas_result = big_df.groupby('category')['value'].mean()
pandas_time = time.time() - t0
# DuckDB groupby
t0 = time.time()
duckdb_result = con.execute("""
SELECT category, AVG(value) as avg_value
FROM big_df
GROUP BY category
ORDER BY category
""").fetchdf()
duckdb_time = time.time() - t0
print(f"pandas: {pandas_time*1000:.2f}ms")
print(f"DuckDB: {duckdb_time*1000:.2f}ms")
|
实测结果:
- pandas: 4.47ms
- DuckDB: 2.53ms
DuckDB快了1.8倍。数据量越大,差距越明显。
7. 自定义UDF函数
DuckDB支持用Python写自定义函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| def celsius_to_fahrenheit(c):
return c * 9/5 + 32
con.create_function('c_to_f', celsius_to_fahrenheit, [float], float)
temp_df = pd.DataFrame({
'city': ['Beijing', 'Shanghai', 'Guangzhou'],
'temp_c': [22.5, 25.0, 30.2]
})
result = con.execute("""
SELECT city, temp_c, c_to_f(temp_c) as temp_f
FROM temp_df
""").fetchdf()
print(result)
# city temp_c temp_f
# 0 Beijing 22.5 72.50
# 1 Shanghai 25.0 77.00
# 2 Guangzhou 30.2 86.36
|
create_function的参数:函数名、Python函数、输入类型列表、输出类型。
8. 配置和线程控制
DuckDB默认用所有CPU核心,可以手动控制:
1
2
3
4
5
6
| # 设置线程数
con.execute("SET threads = 2")
# 查看当前配置
threads = con.execute("SELECT current_setting('threads')").fetchone()
print(f"Current threads: {threads[0]}") # Current threads: 2
|
其他常用配置:
SET memory_limit = '4GB':限制内存使用SET temp_directory = '/tmp/duckdb':设置临时文件目录
踩坑记录
坑1:DataFrame变量名冲突
如果Python里有多个同名DataFrame变量,DuckDB会用最后一个。建议用不同的变量名,或者显式注册:
1
2
| con.register('my_table', df)
result = con.execute("SELECT * FROM my_table").fetchdf()
|
坑2:UDF性能
自定义UDF是逐行调用的,大数据集会慢。尽量用内置函数,UDF只用于复杂逻辑。
坑3:连接泄漏
记得调用con.close(),或者用上下文管理器:
1
2
| with duckdb.connect('db.db') as con:
con.execute("SELECT * FROM table")
|
总结
DuckDB Python客户端的核心价值是消除pandas和SQL之间的边界。不用导出数据,不用写转换代码,直接查DataFrame。
8个用法里,最实用的是DataFrame直查、参数化查询和文件直查。自定义UDF适合复杂业务逻辑,但要小心性能。
如果你也在用pandas做数据分析,试试DuckDB。不是要替换pandas,而是多一个工具。有些场景SQL比pandas链式调用更直观。
参考资源