共计 2671 个字符,预计需要花费 7 分钟才能阅读完成。
背景痛点:为什么下载 ClickHouse 数据这么难?
最近在项目里频繁使用 ClickHouse 做数据分析,发现从 CK 导出数据到本地时经常遇到各种 ” 坑 ”。这里总结几个最常见的痛点:

- 大文件超时中断 :导出 GB 级数据时,网络稍不稳定就会中断,又得从头开始
- 特殊字符转义问题 :字段里包含引号、换行符时,CSV 格式直接乱套
- 列类型映射错误 :DateTime 类型转到其他系统时莫名其妙差 8 小时
- 内存爆仓 :不小心 SELECT * 查了大表,直接把客户端内存撑爆
对比常用的两种下载方式:
- 直接用 wget/curl 调用 HTTP 接口
- 优点:简单快速,适合小数据量
-
缺点:没有断点续传,无法处理复杂查询
-
使用 clickhouse-client
- 优点:支持完整 SQL 语法,类型转换更准确
- 缺点:需要安装客户端,大结果集可能卡死
技术方案:用 Python 打造稳定下载工具
协议选择:HTTP vs Native TCP
ClickHouse 提供两种协议接口:
- HTTP 接口 (端口 8123)
- 兼容性好,各种语言都能调用
-
适合简单查询和中小数据量
-
Native TCP 协议 (端口 9000)
- 二进制传输效率更高
- 支持压缩和更复杂的数据类型
- 需要官方客户端库
建议 :
– 数据量 <1GB 用 HTTP+JSON 格式
– 大数据量用 Native 协议 +RowBinary 格式
Python 实现代码(带重试和流式处理)
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# 重试策略(指数退避)retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[500, 502, 503, 504]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session = requests.Session()
session.mount("http://", adapter)
session.mount("https://", adapter)
# 流式下载大数据集
url = "http://localhost:8123/?query=SELECT+*+FROM+dataset+WHERE+date>='2023-01-01'+FORMAT+CSVWithNames"
with session.get(url, stream=True) as r:
r.raise_for_status()
with open('output.csv', 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
关键点说明:
FORMAT CSVWithNames保证第一行是列名stream=True避免内存中加载完整响应- 分块大小 8192 字节是经过测试的平衡值
生产环境必备 SQL 模板
-- 带分页的查询(避免超时)SELECT * FROM big_table
WHERE create_time > '2023-01-01'
LIMIT 1000000 OFFSET 0
-- 指定输出格式(Parquet 更省空间)SELECT col1, col2 FROM table
FORMAT Parquet
-- 处理时区问题(转 UTC 时间)SELECT toTimeZone(event_time, 'UTC') FROM logs
生产级考量:稳定性与安全性
网络抖动应对方案
除了代码中的指数退避重试,还需要:
- 监控 HTTP 状态码,遇到 429 立即暂停
- 记录最后成功的时间点,下次从断点继续
- 设置合理的超时时间:
session.timeout = (30, 300) # (连接超时, 读取超时)
内存控制黄金法则
- 永远不用
SELECT *,明确列出所需字段 - 对于 Array/Tuple 类型,考虑用
arrayStringConcat转成文本 - 超过 1GB 的数据必须用流式处理
权限最小化配置
在 users.xml 中创建只读用户:
<users>
<readonly_user>
<password>safe_password</password>
<networks>
<ip>192.168.1.100</ip>
</networks>
<profile>readonly</profile>
<quota>default</quota>
</readonly_user>
</users>
<profiles>
<readonly>
<readonly>1</readonly>
<allow_ddl>0</allow_ddl>
</readonly>
</profiles>
避坑指南:血泪教训总结
时区问题三板斧
- 写入时统一用 UTC 时间戳
- 查询时用
toTimeZone()显式转换 - 客户端设置
use_client_time_zone=0
特殊字符处理技巧
- CSV 格式用
CSVWithNamesAndTypes自动处理转义 - 或者用
JSONEachRow格式避免分隔符冲突 - 在 SQL 中用
replaceRegexpAll提前清洗数据
磁盘空间监控
推荐用 Python 的 shutil.disk_usage 提前检查:
import shutil
total, used, free = shutil.disk_usage("/")
if free < 1024**3: # 小于 1GB 空间
raise RuntimeError("磁盘空间不足")
延伸思考:进阶优化方向
增量导出方案
-
创建物化视图记录上次导出位置:
CREATE MATERIALIZED VIEW export_status ENGINE = Log AS SELECT max(event_time) AS last_export FROM table -
下次查询时基于这个时间点继续:
SELECT * FROM table WHERE event_time > (SELECT last_export FROM export_status)
超大数据集处理
当数据量超过 1TB 时:
-
按分区并行导出(需要 MergeTree 引擎)
SELECT * FROM table WHERE partition_id = '202301' -
用
clickhouse-copier工具做分布式传输 - 考虑直接备份整个 part 目录
实践心得
经过多次踩坑后总结出的最佳实践:
- 小数据(<1GB)用 HTTP+CSV,简单直接
- 中等数据(1GB-100GB)用 Native 协议 +RowBinary
- 超大数据(>100GB)考虑按分区并行处理
- 永远要测试时区转换和特殊字符场景
希望这篇实战总结能帮你少走弯路,如果有更好的解决方案欢迎交流!
正文完
发表至: 技术分享
近一天内
