ClickHouse(ck)数据集下载实战指南:从零搭建高效数据管道

1次阅读
没有评论

共计 2315 个字符,预计需要花费 6 分钟才能阅读完成。

image.webp

背景痛点分析

刚开始接触 ClickHouse 时,下载数据集经常会遇到各种头疼的问题。最常见的有三种:

ClickHouse(ck)数据集下载实战指南:从零搭建高效数据管道

  • 连接中断:特别是大表下载时,网络稍微波动就会导致连接超时,又得从头开始
  • 内存不足:直接用 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

关键优化技巧

  1. 并发控制:通过修改 max_threads 参数加速

    # 在 Client 初始化时设置
    client = Client(
        host=CH_HOST, 
        port=CH_PORT,
        settings={'max_threads': 8}  # 根据 CPU 核心数调整
    )

  2. 进度可视化: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 的方案:

  1. 将每个分块上传到对象存储
  2. 记录已完成的 offset 到 redis
  3. 程序重启时先检查存储状态

数据完整性校验

推荐两种方法组合使用:

  1. 行数校验:

    # 下载前后对比
    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

  2. 哈希校验:

    import hashlib
    
    def get_file_hash(filename):
        with open(filename, 'rb') as f:
            return hashlib.md5(f.read()).hexdigest()
    
    # 比较服务端和本地文件的哈希值

写在最后

经过几次实战,我总结出 ClickHouse 数据下载的黄金法则:分而治之。通过合理的分块策略、适当的并发控制和完备的错误处理,现在处理上亿行数据也能稳如老狗。

下次如果遇到更复杂的场景,比如需要实时同步增量数据,可以考虑使用 MaterializedView+ReplacingMergeTree 的组合方案。不过那就是另一个故事了 …

正文完
 0
评论(没有评论)