news 2026/3/13 15:52:26

ClickHouse实战:十亿级数据OLAP查询优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ClickHouse实战:十亿级数据OLAP查询优化

前言

去年公司要搞一个数据分析平台,日志量大概一天3亿条,保留90天,总共约300亿条数据。之前用MySQL分库分表勉强扛着,但复杂查询动不动就几十秒,业务那边已经骂了好几次。

调研了一圈,Druid、Doris、ClickHouse,最后选了ClickHouse。原因很简单:单机性能强悍,运维相对简单,社区活跃。

用了一年多,踩了不少坑,也总结了一些优化经验。这篇文章分享出来,希望能帮到同样在做数据分析的朋友。

ClickHouse是什么

一句话:列式存储的OLAP数据库,专门用来做数据分析,写进去快,查得更快。

和MySQL这类行存数据库的核心区别:

  • 行存:一行数据存在一起,适合CRUD单条记录
  • 列存:同一列的数据存在一起,适合聚合统计

举个例子,有张100列的表,查询只涉及3列,MySQL要把整行读出来再挑3列,ClickHouse直接只读3列的数据,IO少一个数量级。

环境搭建

单机版很简单:

# Ubuntu/Debianapt-getinstall-y apt-transport-https ca-certificates apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 8919F6BD2B48D754echo"deb https://packages.clickhouse.com/deb stable main">/etc/apt/sources.list.d/clickhouse.listapt-getupdateapt-getinstall-y clickhouse-server clickhouse-client# 启动systemctl start clickhouse-server# 连接clickhouse-client

默认配置文件在/etc/clickhouse-server/,主要改这几个:

<!-- config.xml --><max_memory_usage>10000000000</max_memory_usage><!-- 单查询内存限制10G --><max_threads>8</max_threads><!-- 查询并行度 -->

建表:选对引擎很重要

ClickHouse有几十种引擎,生产环境90%用MergeTree家族。

基础MergeTree

CREATETABLElogs(event_dateDate,event_timeDateTime,user_id UInt64,actionString,duration UInt32,city String,device String)ENGINE=MergeTree()PARTITIONBYtoYYYYMM(event_date)ORDERBY(user_id,event_time)SETTINGS index_granularity=8192;

几个关键点:

  1. PARTITION BY:分区键,按月分区是常见做法。查询时指定月份可以跳过无关分区。
  2. ORDER BY:排序键,决定数据物理存储顺序。查询最常用的筛选条件要放进去。
  3. index_granularity:索引粒度,默认8192行一个索引项。

选错ORDER BY的代价

第一版我把ORDER BY写成了(event_time, user_id),结果按user_id查询特别慢。

原因:ORDER BY决定数据排列顺序,也决定稀疏索引的结构。user_id放第二位,按它查需要扫描大量数据块。

改成(user_id, event_time)后,按user_id查询速度提升10倍以上。

原则:把等值查询最常用的列放前面,范围查询的列放后面。

写入优化

批量写入是基本

ClickHouse的写入不是针对小事务设计的。每次INSERT都会生成一个数据part,小批量频繁写入会产生大量parts,影响查询性能。

# 错误方式:一条条插forrowindata: insert(row)# 正确方式:批量插# 至少1000条起批,建议1万~10万条一批insert_batch(data[:10000])

官方建议每批次不少于1000行,每秒写入不超过1次。

用Buffer引擎缓冲

如果必须高频小批量写入,用Buffer引擎:

CREATETABLElogs_bufferASlogsENGINE=Buffer(currentDatabase(),logs,16,-- num_layers10,100,-- min_time, max_time10000,1000000,-- min_rows, max_rows10000000,100000000-- min_bytes, max_bytes);

数据先写Buffer表,积攒到一定量或时间后自动flush到主表。

实际写入架构

我们的架构是 Kafka -> Flink -> ClickHouse:

Kafka (原始日志) | v Flink (聚合、清洗) | v ClickHouse (每10秒一批,每批约50万条)

Flink里做时间窗口聚合,积攒10秒的数据一次写入,避免小批量问题。

查询优化

看执行计划

EXPLAINSELECTuser_id,count()FROMlogsWHEREevent_date='2024-12-25'ANDcity='Beijing'GROUPBYuser_id;

关注点:

  • 有没有用到分区裁剪
  • 有没有用到主键索引
  • 扫描了多少行

更详细的:

EXPLAINPIPELINESELECT...

能看到每个处理阶段的执行计划。

分区裁剪

-- 好:指定分区SELECT*FROMlogsWHEREevent_date>='2024-12-01'ANDevent_date<'2024-12-31'-- 差:分区列加函数SELECT*FROMlogsWHEREtoYYYYMM(event_date)=202412

第二种写法分区裁剪失效,会扫全表。

利用主键索引

ORDER BY的列才有稀疏索引:

-- 表定义 ORDER BY (user_id, event_time)-- 快:user_id等值查询,精准定位SELECT*FROMlogsWHEREuser_id=12345-- 较快:user_id + event_time范围SELECT*FROMlogsWHEREuser_id=12345ANDevent_time>'2024-12-01'-- 慢:跳过user_id直接查event_timeSELECT*FROMlogsWHEREevent_time>'2024-12-01'

避免SELECT *

列存数据库的优势就是只读需要的列:

-- 差:读所有列SELECT*FROMlogsWHEREuser_id=12345-- 好:只读需要的列SELECTuser_id,action,durationFROMlogsWHEREuser_id=12345

100列的表,读3列和读100列,IO差距巨大。

prewhere代替where

SELECT*FROMlogs PREWHERE event_date='2024-12-25'-- 先过滤WHEREcity='Beijing'-- 再过滤

prewhere在读取其他列之前先过滤,减少需要读取的数据量。过滤性强的条件用prewhere。

物化视图预聚合

高频查询的聚合结果提前算好:

-- 创建物化视图,按天按城市预聚合CREATEMATERIALIZEDVIEWlogs_daily_cityENGINE=SummingMergeTree()PARTITIONBYtoYYYYMM(event_date)ORDERBY(event_date,city)ASSELECTevent_date,city,count()ascnt,sum(duration)astotal_durationFROMlogsGROUPBYevent_date,city;

新数据写入logs时自动聚合到物化视图。查询日/城市维度的汇总直接查物化视图,速度快几十倍。

跳数索引

对于不在ORDER BY里的列,可以加跳数索引:

-- 给city列加布隆过滤器索引ALTERTABLElogsADDINDEXidx_city cityTYPEbloom_filter GRANULARITY4;-- 给duration加minmax索引ALTERTABLElogsADDINDEXidx_duration durationTYPEminmax GRANULARITY4;

bloom_filter适合等值查询,minmax适合范围查询。

几个踩过的坑

坑1:String类型太慢

最开始action、city这些列都用String,后来发现GROUP BY特别慢。

改用LowCardinality:

ALTERTABLElogsMODIFYCOLUMNcity LowCardinality(String);ALTERTABLElogsMODIFYCOLUMNactionLowCardinality(String);

LowCardinality内部是字典编码,对于基数低(不同值少)的列,存储和计算都更高效。我们改完后相关查询快了3倍。

坑2:不小心全表扫描

线上出过一次事故,有人写了个没有where条件的count查询,直接把CPU打满了。

解决:设置查询限制

-- 用户级别限制CREATEUSERanalyst SETTINGS max_rows_to_read=100000000;-- 查询级别限制SETmax_rows_to_read=100000000;SELECT...

坑3:JOIN性能

ClickHouse的JOIN不是强项,大表JOIN特别慢。

优化方向:

  1. 小表放右边(会被广播到所有节点)
  2. 用字典代替维度表JOIN
  3. 数据预先宽表化,避免JOIN
-- 创建字典CREATEDICTIONARY city_dict(city_id UInt32,city_name String,province String)PRIMARYKEYcity_id SOURCE(CLICKHOUSE(HOST'localhost'PORT9000USER'default'TABLE'city_dim'DB'default'))LIFETIME(MIN300MAX600)LAYOUT(HASHED());-- 用字典代替JOINSELECTdictGet('city_dict','city_name',city_id)ascity_name,count()FROMlogsGROUPBYcity_id;

坑4:磁盘打满

日志数据量太大,磁盘很快就满了。

配置TTL自动清理:

ALTERTABLElogsMODIFYTTL event_date+INTERVAL90DAY;

90天前的数据自动删除。

也可以配置冷热分层,旧数据迁移到便宜的存储:

<storage_configuration><disks><hot><path>/data/clickhouse/hot/</path></hot><cold><path>/data/clickhouse/cold/</path></cold></disks><policies><tiered><volumes><hot><disk>hot</disk></hot><cold><disk>cold</disk></cold></volumes><move_factor>0.1</move_factor></tiered></policies></storage_configuration>

监控与运维

系统表查性能

-- 查询日志SELECTquery,read_rows,read_bytes,result_rows,memory_usage,query_duration_msFROMsystem.query_logWHEREtype='QueryFinish'ORDERBYquery_start_timeDESCLIMIT20;-- 慢查询SELECT*FROMsystem.query_logWHEREquery_duration_ms>10000ORDERBYquery_start_timeDESC;-- 各表大小SELECTtable,formatReadableSize(sum(bytes_on_disk))assize,sum(rows)asrowsFROMsystem.partsWHEREactiveGROUPBYtableORDERBYsum(bytes_on_disk)DESC;

常规运维操作

-- 手动合并parts(一般不需要,后台自动做)OPTIMIZETABLElogs FINAL;-- 查看partsSELECTtable,partition,name,rows,bytes_on_diskFROMsystem.partsWHEREtable='logs';-- 删除分区ALTERTABLElogsDROPPARTITION'202401';

多节点管理

多个ClickHouse节点时,我会用星空组网把它们串到一个虚拟网络里,维护的时候不用一台台跳。特别是跨机房部署的时候,组到一起后用Ansible批量执行命令很方便。

性能数据参考

分享一下我们的实际数据:

  • 单表300亿行,约8TB存储(压缩后)
  • 8核32G的机器
  • 简单聚合查询(单分区):100ms以内
  • 复杂聚合(跨月、多维度):3-10秒
  • 物化视图查询:50ms以内

相比之前MySQL分库分表的方案,复杂查询从几十秒降到几秒,业务那边再也没骂过。

总结

ClickHouse的使用核心就几点:

  1. 建表:ORDER BY要根据查询模式设计
  2. 写入:批量写,别小批量频繁写
  3. 查询:利用好分区裁剪和主键索引,只读需要的列
  4. 预计算:高频查询用物化视图

它不是万能的:OLTP场景别用,JOIN重的场景谨慎用。但在OLAP场景,特别是日志分析、用户行为分析、实时报表这些方向,确实是目前性价比最高的选择之一。

刚开始用的时候可能觉得概念多,用熟了会发现套路就那些。希望这篇文章能帮你少踩点坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/13 15:15:36

Python 设计模式

Python 设计模式概述 设计模式&#xff08;Design Patterns&#xff09;是软件设计中常见的、可复用的问题解决方案&#xff0c;由 GoF&#xff08;Gang of Four&#xff0c;四人组&#xff09;在1994年的经典书籍《设计模式&#xff1a;可复用面向对象软件的基础》中总结出23…

作者头像 李华
网站建设 2026/3/7 0:21:49

Python 外观模式

Python 中的外观模式&#xff08;Facade Pattern&#xff09; 外观模式是一种结构型设计模式&#xff0c;其核心目的是&#xff1a; 为一个复杂子系统提供一个简单的、统一的接口&#xff0c;隐藏子系统的复杂性&#xff0c;让客户端更容易使用。 形象比喻&#xff1a;就像一…

作者头像 李华
网站建设 2026/3/13 7:15:06

PSMNet立体匹配网络:从理论到实践的完整指南

PSMNet立体匹配网络&#xff1a;从理论到实践的完整指南 【免费下载链接】PSMNet Pyramid Stereo Matching Network (CVPR2018) 项目地址: https://gitcode.com/gh_mirrors/ps/PSMNet PSMNet&#xff08;Pyramid Stereo Matching Network&#xff09;是由Jia-Ren Chang和…

作者头像 李华
网站建设 2026/3/8 5:53:06

Open-AutoGLM技术内幕曝光:90%开发者不知道的7个隐藏功能

第一章&#xff1a;Open-AutoGLM详细介绍Open-AutoGLM 是一个开源的自动化通用语言模型&#xff08;GLM&#xff09;推理与部署框架&#xff0c;旨在降低大模型在实际业务场景中的应用门槛。该框架集成了模型压缩、自动提示工程、上下文学习优化以及多后端推理支持等核心功能&a…

作者头像 李华