Featured image of post DuckDB扩展生态:httpfs、spatial、parquet元数据实战

DuckDB扩展生态:httpfs、spatial、parquet元数据实战

DuckDB扩展生态实战。httpfs直查远程文件、spatial地理计算、parquet元数据优化,6个真实场景,Parquet比CSV快10倍的性能数据。

上周查一个线上数据问题,需要读一个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表格式。

最实用的三个扩展:

  1. httpfs:直查远程文件,不用下载
  2. spatial:地理计算,处理位置数据
  3. Parquet元数据:查询优化,理解数据分布

如果你还在用pandas读CSV,试试DuckDB的Parquet支持。10倍的性能差距,不是开玩笑的。

参考资源

Built with Hugo
Theme Stack designed by Jimmy