上周查一个线上数据问题,需要读一个S3上的Parquet文件。以前得先下载到本地,再用pandas加载。这次试了DuckDB的httpfs扩展,一行SQL直接查远程文件。
当时就觉得,这玩意也太方便了。
DuckDB的扩展生态比我想象的丰富得多。30多个扩展,从云存储到地理计算,从全文搜索到Excel读取。这篇文章挑了6个最常用的扩展,每个都有真实代码和测试数据。
查看已安装的扩展
先看看DuckDB有哪些扩展:
1
2
3
4
5
6
| import duckdb
con = duckdb.connect()
extensions = con.execute("SELECT * FROM duckdb_extensions()").fetchdf()
print(f"Total extensions: {len(extensions)}")
print(extensions[['extension_name', 'loaded', 'installed']].head(10))
|
实测有30个扩展,包括httpfs、spatial、icu、parquet、json等。
1. httpfs:直查远程文件
httpfs让DuckDB直接查询HTTP/HTTPS/S3上的文件,不用下载。
1
2
3
4
5
6
7
8
9
| con.execute("INSTALL httpfs")
con.execute("LOAD httpfs")
# 查询远程CSV
result = con.execute("""
SELECT * FROM read_csv_auto(
'https://raw.githubusercontent.com/datasets/odata-blp/latest/data/indicators.csv'
) LIMIT 3
""").fetchdf()
|
S3也支持,配置一下凭证就行:
1
2
3
4
5
6
7
8
9
| con.execute("""
SET s3_region='us-east-1';
SET s3_access_key_id='YOUR_KEY';
SET s3_secret_access_key='YOUR_SECRET';
""")
result = con.execute("""
SELECT * FROM read_parquet('s3://bucket/data/file.parquet')
""").fetchdf()
|
实测从S3读Parquet文件,比先下载再读快很多,因为DuckDB只拉需要的列。
2. spatial:地理空间计算
spatial扩展支持点、线、面的地理计算。
1
2
3
4
5
6
7
8
9
10
11
12
13
| con.execute("INSTALL spatial")
con.execute("LOAD spatial")
# 创建地理点
result = con.execute("""
SELECT
ST_Point(116.4, 39.9) as beijing_point,
ST_AsText(ST_Point(116.4, 39.9)) as wkt
""").fetchdf()
print(result)
# beijing_point wkt
# 0 bytearray(b'\x01\x01\x00\x00\x00\x9a\x99\x99... POINT (116.4 39.9)
|
计算两个城市的距离:
1
2
3
4
5
6
7
8
9
10
11
| result = con.execute("""
SELECT
ST_Distance(
ST_Point(116.4, 39.9), -- 北京
ST_Point(121.5, 31.2) -- 上海
) as raw_distance
""").fetchdf()
print(result)
# raw_distance
# 0 10.084642
|
注意这个距离是经纬度的欧氏距离,不是实际公里数。要算实际距离,需要用球面几何函数。
3. JSON扩展:嵌套数据展开
JSON扩展是内置的,不需要安装。处理嵌套JSON特别方便:
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
28
29
| import json
json_data = """
[
{"name": "Alice", "skills": ["Python", "SQL", "DuckDB"]},
{"name": "Bob", "skills": ["Java", "Spark"]},
{"name": "Charlie", "skills": ["Python", "Rust", "Go"]}
]
"""
with open('/tmp/test.json', 'w') as f:
f.write(json_data)
result = con.execute("""
SELECT name, unnest(skills) as skill
FROM read_json_auto('/tmp/test.json')
ORDER BY name, skill
""").fetchdf()
print(result)
# name skill
# 0 Alice DuckDB
# 1 Alice Python
# 2 Alice SQL
# 3 Bob Java
# 4 Bob Spark
# 5 Charlie Go
# 6 Charlie Python
# 7 Charlie Rust
|
unnest把数组展开成多行。这个功能处理API返回的嵌套JSON特别有用。
4. ICU扩展:中文排序
ICU扩展支持国际化排序规则,中文排序终于正常了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| con.execute("INSTALL icu")
con.execute("LOAD icu")
import pandas as pd
df_cn = pd.DataFrame({
'city': ['北京', '上海', '广州', '深圳', '杭州'],
'population': [2154, 2487, 1868, 1756, 1237]
})
result = con.execute("""
SELECT city, population
FROM df_cn
ORDER BY population DESC
LIMIT 3
""").fetchdf()
print(result)
# city population
# 0 上海 2487
# 1 北京 2154
# 2 广州 1868
|
5. Parquet元数据:查询优化利器
Parquet文件自带元数据,DuckDB可以直接读取:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| import numpy as np
big_df = pd.DataFrame({
'id': range(50000),
'category': np.random.choice(['tech', 'finance', 'health', 'edu'], 50000),
'score': np.random.uniform(0, 100, 50000)
})
con.execute("COPY big_df TO '/tmp/test.parquet' (FORMAT PARQUET)")
# 查看Parquet元数据
result = con.execute("""
SELECT * FROM parquet_metadata('/tmp/test.parquet')
""").fetchdf()
print(f"Row groups: {result['row_group_id'].nunique()}")
print(f"Columns: {result['path_in_schema'].unique()}")
|
查看Parquet的schema:
1
2
3
4
5
6
7
8
9
10
| result = con.execute("""
SELECT * FROM parquet_schema('/tmp/test.parquet')
""").fetchdf()
print(result)
# file_name name duckdb_type column_id
# 0 /tmp/test.parquet duckdb_schema NaN 0
# 1 /tmp/test.parquet id BIGINT 1
# 2 /tmp/test.parquet category VARCHAR 2
# 3 /tmp/test.parquet score DOUBLE 3
|
元数据里最有用的是stats_min和stats_max,DuckDB用它做谓词下推,跳过不需要的数据块。
6. 性能实测:Parquet vs CSV
50行数据,同样的查询,对比Parquet和CSV的性能:
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
| import time
# 写入CSV
big_df.to_csv('/tmp/test.csv', index=False)
# Parquet查询
t0 = time.time()
r1 = con.execute("""
SELECT category, AVG(score)
FROM read_parquet('/tmp/test.parquet')
GROUP BY category
""").fetchdf()
parquet_time = time.time() - t0
# CSV查询
t0 = time.time()
r2 = con.execute("""
SELECT category, AVG(score)
FROM read_csv_auto('/tmp/test.csv')
GROUP BY category
""").fetchdf()
csv_time = time.time() - t0
print(f"Parquet: {parquet_time*1000:.2f}ms")
print(f"CSV: {csv_time*1000:.2f}ms")
print(f"Speedup: {csv_time/parquet_time:.1f}x")
|
实测结果:
- Parquet: 1.44ms
- CSV: 15.19ms
- Parquet快10.6倍
Parquet是列式存储,只读需要的列。CSV是行式存储,得全读进来再过滤。数据量越大,差距越大。
踩坑记录
坑1:httpfs SSL错误
在某些网络环境下,httpfs会报SSL连接错误。可以试试设置环境变量:
1
2
| import os
os.environ['DUCKDB_ALLOW_UNSIGNED_EXTENSIONS'] = '1'
|
坑2:spatial距离计算
ST_Distance算的是欧氏距离,不是球面距离。要算实际公里数,得用ST_Distance_Sphere(需要proj支持)。
坑3:扩展安装失败
某些网络环境下INSTALL会失败。可以手动下载扩展文件,放到DuckDB的扩展目录。
总结
DuckDB的扩展生态是它最大的优势之一。30多个扩展覆盖了大部分数据场景:云存储、地理计算、全文搜索、Excel读取、Iceberg表格式。
最实用的三个扩展:
- httpfs:直查远程文件,不用下载
- spatial:地理计算,处理位置数据
- Parquet元数据:查询优化,理解数据分布
如果你还在用pandas读CSV,试试DuckDB的Parquet支持。10倍的性能差距,不是开玩笑的。
参考资源