共计 2315 个字符,预计需要花费 6 分钟才能阅读完成。
背景痛点分析
刚开始接触 ClickHouse 时,下载数据集经常会遇到各种头疼的问题。最常见的有三种:

- 连接中断:特别是大表下载时,网络稍微波动就会导致连接超时,又得从头开始
- 内存不足:直接用 SELECT * 下载大表,内存直接爆掉
- 格式混乱:CSV 文件里出现乱码、字段错位,导入其他系统时各种报错
我第一次尝试从 CK 导出一张 500 万行的表时,就经历了所有这些坑——程序跑了半小时突然断开,好不容易下载完发现列顺序错乱,最惨的是有一次直接让服务器 OOM 了。
协议选型:HTTP vs Native
ClickHouse 提供两种数据传输协议,选择合适的方式能事半功倍:
| 对比维度 | HTTP 接口 | Native 协议 |
|---|---|---|
| 传输速度 | 较慢(文本协议) | 快(二进制协议) |
| 压缩支持 | 依赖 gzip | 内置 LZ4 压缩 |
| 内存消耗 | 较高 | 较低 |
| 开发便利性 | 通用性强(curl 可用) | 需要专用驱动 |
| 适用场景 | 小批量数据 / 简单查询 | 大数据量 / 复杂查询 |
实践建议:超过 100MB 的数据集强烈推荐用 native 协议,速度能快 3 - 5 倍。
Python 实现分块下载
基础代码框架
from clickhouse_driver import Client
from tqdm import tqdm
import pandas as pd
CH_HOST = 'localhost'
CH_PORT = 9000
CHUNK_SIZE = 100000 # 每批次获取 10 万行
client = Client(host=CH_HOST, port=CH_PORT)
def download_table(table_name):
# 先获取总行数
total = client.execute(f'SELECT count() FROM {table_name}')[0][0]
# 使用 with 语句确保资源释放
with tqdm(total=total, desc='Downloading') as pbar:
for offset in range(0, total, CHUNK_SIZE):
# 关键:分批查询避免 OOM
query = f'''
SELECT * FROM {table_name}
LIMIT {CHUNK_SIZE} OFFSET {offset}
'''
try:
data = client.execute(query)
df = pd.DataFrame(data)
# 这里可以添加 CRC 校验
if len(df) != CHUNK_SIZE and (offset + CHUNK_SIZE) < total:
raise ValueError('数据不完整!')
# 存储到本地(示例用 CSV)df.to_csv(f'{table_name}_part_{offset}.csv', index=False)
pbar.update(len(df))
except Exception as e:
print(f'第 {offset} 行开始出错: {str(e)}')
# 实现自动重试逻辑
retry_count = 0
while retry_count < 3:
try:
data = client.execute(query)
break
except:
retry_count += 1
关键优化技巧
-
并发控制:通过修改 max_threads 参数加速
# 在 Client 初始化时设置 client = Client( host=CH_HOST, port=CH_PORT, settings={'max_threads': 8} # 根据 CPU 核心数调整 ) -
进度可视化:tqdm 的妙用
# 在循环外初始化进度条 with tqdm(total=total, unit='rows', bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt}') as pbar: # 每次获取数据后更新 pbar.update(len(data))
避坑指南
高频陷阱与解决方案
- OOM 杀手:永远不要直接
SELECT * FROM huge_table -
正确做法:必须带 LIMIT 分页,或者用 WHERE 过滤
-
数据截断:LIMIT 可能丢失最后部分数据
-
改用
WITH TIES语法:SELECT * FROM table ORDER BY date LIMIT 10000 WITH TIES -
分区表处理:按分区目录结构存储
dataset/ ├── partition1/ │ ├── part1.csv │ └── part2.csv └── partition2/ ├── part1.csv └── part2.csv
进阶思考
断点续传实现
结合 S3/MinIO 的方案:
- 将每个分块上传到对象存储
- 记录已完成的 offset 到 redis
- 程序重启时先检查存储状态
数据完整性校验
推荐两种方法组合使用:
-
行数校验:
# 下载前后对比 source_count = client.execute('SELECT count() FROM source_table')[0][0] downloaded_count = sum(len(pd.read_csv(f)) for f in files) assert source_count == downloaded_count -
哈希校验:
import hashlib def get_file_hash(filename): with open(filename, 'rb') as f: return hashlib.md5(f.read()).hexdigest() # 比较服务端和本地文件的哈希值
写在最后
经过几次实战,我总结出 ClickHouse 数据下载的黄金法则:分而治之。通过合理的分块策略、适当的并发控制和完备的错误处理,现在处理上亿行数据也能稳如老狗。
下次如果遇到更复杂的场景,比如需要实时同步增量数据,可以考虑使用 MaterializedView+ReplacingMergeTree 的组合方案。不过那就是另一个故事了 …
正文完
发表至: 未分类
近一天内
