Featured image of post DuckDB Python客户端:从DataFrame直查到UDF自定义函数

DuckDB Python客户端:从DataFrame直查到UDF自定义函数

DuckDB Python客户端完整指南。从pandas DataFrame直查、参数化查询、持久化数据库,到自定义UDF函数,8个真实代码示例,性能对比数据。

上个月用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链式调用更直观。

参考资源

Built with Hugo
Theme Stack designed by Jimmy