当前位置:首页 > 云谷精选

ClickHouse查询慢怎么从表引擎和索引上优化

admin2026年03月18日云谷精选13.92万
摘要:# 当ClickHouse慢到让人想砸键盘,先别急着加内存 我前两天刚处理完一个客户的紧急求助,电话那头声音都快冒烟了:“老师,我们那个ClickHouse报表,昨天还好好的,今天突然慢成狗了,一个简单查询都要半分钟!” 我让他先别慌,远程连上去一看—…

当ClickHouse慢到让人想砸键盘,先别急着加内存

我前两天刚处理完一个客户的紧急求助,电话那头声音都快冒烟了:“老师,我们那个ClickHouse报表,昨天还好好的,今天突然慢成狗了,一个简单查询都要半分钟!”

我让他先别慌,远程连上去一看——好家伙,一张核心大表,几亿条数据,用的还是默认的MergeTree,没任何索引优化,分区键设得跟闹着玩似的。这场景你应该不陌生吧?很多团队一开始图省事,直接CREATE TABLE一路回车,等数据量上来了,查询慢得让人怀疑人生。

说真的,ClickHouse快起来是真快,但要是没配好,慢起来也真是能把你急死。今天咱不聊那些空泛的“优化十大原则”,就扎扎实实聊聊,当查询慢得受不了时,怎么从表引擎和索引这两个最根本的地方动刀子。很多所谓优化方案,文档上写得天花乱坠,真到实战时才发现压根不是那么回事。

表引擎选错了,后面再怎么折腾都白搭

ClickHouse的表引擎,说白了就是数据的“生存方式”。你选错了,就像在高速公路上开拖拉机——发动机再吼也跑不快。

最常见的坑,就是所有表无脑用MergeTree。 这玩意儿是通用,但不是什么场景都合适。我自己看过不少部署,问题往往不是没上ClickHouse,而是引擎压根没选对。

比如,你有一张表,每天就用来做实时流水写入,然后几乎全是按照时间范围做聚合查询,很少去更新或删除某条具体记录。这时候你还用标准的MergeTree,甚至用ReplacingMergeTree(它要负责去重,有开销),那就是给自己找不痛快。

——这种情况,直接用AggregatingMergeTree或者SummingMergeTree可能更香。它们能在合并数据部分时预聚合,查询时直接拿“半成品”结果,速度能快上一个数量级。但很多人压根不知道或者不敢用,为什么?因为怕“特性复杂”、“不好掌控”。其实吧,你业务如果就是计数、求和、求最大最小值这类标准聚合,用它们就是降维打击。

再举个具体例子。有个做电商数据分析的团队,有张表存用户行为事件,每天几亿条。他们老抱怨“查询某商品点击UV太慢”。我一看,他们用MergeTree,查询时不得不用COUNT(DISTINCT user_id),在PB级数据上做去重,能不慢吗?后来我让他们改成用AggregatingMergeTree,配合uniqState这种聚合函数状态,数据在后台合并时就去重计数好了。查询时直接用uniqMerge,从扫描几亿行变成扫描几千个预聚合后的数据块,速度直接从30多秒降到1秒内。这种提升,比你加内存加CPU粗暴扩容实在多了。

还有种更隐蔽的坑:用错了表引擎家族。 ClickHouse有集成外部系统的引擎(比如MySQL, Kafka),有日志系列引擎(TinyLog, StripeLog),有特殊用途引擎(比如Dictionary, Memory)。如果你需要高频、低延迟地查询一些维度表(比如商品信息、用户属性),这些数据本身存在MySQL里,你每次查询都跨库JOIN,那真是慢得没边了。这时候,直接用MySQL表引擎或者Dictionary引擎把数据拉进ClickHouse内存里查,可能就是一条捷径。虽然数据同步有点延迟,但换来的查询速度提升是实实在在的。这思路很多搞大数据的人转不过来,总觉得数据就得在一个库里才“正统”。

分区键:不是随便选个日期字段就完事了

分区(Partition)这概念,几乎所有数据库都有。但ClickHouse里的分区,直接决定了你数据在磁盘上的物理组织方式。分区键设得不好,查询慢还是小事,搞不好能把ZK(ZooKeeper)给拖垮,这话真不是吓唬你。

很多新手(包括一些老手)的直觉是:按时间分区啊,天经地义。于是PARTITION BY toYYYYMMDD(event_time)就写上去了。如果每天数据量均匀,比如就几百万条,那没问题。但如果你业务有波峰波谷,大促那天数据量是平时的几十倍,问题就来了:一个分区文件巨大,后台合并(Merge)任务迟迟完不成,导致分区目录越来越多,查询时要打开的文件句柄数激增,不慢才怪。

我个人的经验是,分区粒度的选择,取决于你最常见的查询模式和数据删除策略

  • 如果你的查询总是围绕“最近7天”,并且需要定期删除老旧数据(比如只保留90天),那么按天分区甚至按周分区是合理的,删除时直接DROP PARTITION,干净利落。
  • 但如果你数据几乎不删,查询又经常要跨很长的日期范围(比如按月、按年统计),那按天分区就会导致查询时要扫描几百个分区目录,元数据开销巨大。这时候,按月分区甚至更粗的粒度,反而可能更好。

说白了,分区是为了缩小查询时的数据扫描范围。你把它切得太碎,管理成本就上来了;切得太粗,过滤效果又不好。这个平衡点,得看你自己的数据量和查询习惯。

这里插一句大实话:别太迷信自动分区。 ClickHouse有一些实验性的自适应分区功能,但现阶段,一个经过深思熟虑的静态分区策略,远比一个不可控的自动策略要靠谱得多。毕竟,谁也不想在半夜被报警叫醒,原因是分区太多把磁盘inode用光了。

索引:ClickHouse的“快”和“慢”都在这了

说到ClickHouse的索引,很多人第一反应就是主键索引(Primary Key)。但这里有个天大的误解:ClickHouse的主键并不用于去重(除非你用ReplacingMergeTree),它最主要的作用是生成一级跳数索引(primary.idx),帮助快速定位数据块(Granule)

所以,你主键列的顺序,直接决定了你的查询能有多快。原则就一条:把那些在WHERE子句里最常出现、高筛选度的列,放在主键最前面。

举个例子。你有张表,99%的查询都是WHERE user_id = xxx AND event_date BETWEEN ...。那么你的主键就应该是(user_id, event_date),而不是反过来(event_date, user_id)。因为user_id的筛选度通常比日期更高,先通过user_id锁定一个小的数据范围,再在这个范围里按时间过滤,效率高得多。如果反过来,先按日期找到一大片数据,再在里面捞某个user_id,主索引就基本失效了。

但光有主键索引还不够。对于那种查询条件无法从主键最左边开始匹配的场景,你就得请出二级索引(Data Skipping Index)了。

比如,你的主键是(city, event_date),但有个高频查询是WHERE device_type = 'ios'。device_type这列不在主键里,且值分布很散(有ios, android, web等),这时候查询就得全表扫描。解决方法就是给device_type加一个SET类型的跳数索引:INDEX idx_device device_type TYPE set(100) GRANULARITY 4。它的原理是,每4个数据块(Granule)记录一下这个块里包含哪些device_type值。查询时,先快速检查这些索引块,跳过那些根本不可能包含'ios'的数据块,从而减少磁盘IO。

二级索引类型很多,minmax适合范围查询,ngrambf_v1适合字符串模糊匹配,tokenbf_v1适合分词搜索。选哪个,取决于你的查询模式。但记住一个核心:二级索引是用空间和写入性能换查询性能的。它不是在每行数据上建索引,而是在数据块上建摘要,所以有一定误判率(可能跳过包含目标数据的数据块)。同时,每次写入都要更新索引,会有开销。

我的建议是:先通过主键设计覆盖80%的高频查询路径,剩下的20%再用二级索引来针对性优化。 别一开始就建一堆二级索引,最后发现写入慢得受不了,又得回头删。

数据块大小与压缩:容易被忽略的“细活儿”

最后聊点更底层的。ClickHouse存储数据的基本单位是数据块(Granule),默认8192行。这个数能改吗?能。index_granularity这个参数就是干这个的。

调大它(比如改成16384),每个数据块更大,主键索引条目更少,索引文件更小,对于全表扫描或范围查询可能更快,因为顺序读的效率更高。但副作用是,对于点查(比如按主键查某一行),定位精度下降,可能要多读一些不需要的数据。

调小它(比如改成4096),索引更“密集”,点查更快更准,但索引本身变大,对于扫描查询不利。

——看到没?又是权衡。绝大多数情况下,你用默认的8192就行,这是经过大量实践验证的平衡点。除非你有非常极端的、特征明显的查询模式,否则别轻易动它。我见过有人为了优化一个特定查询,把这参数调得乱七八糟,结果导致其他查询性能全面下降,典型的捡芝麻丢西瓜。

还有压缩算法(compression编解码器)。默认的LZ4在速度和压缩比上取得了很好的平衡。如果你的数据是重复率极高的枚举值(比如状态码、省份代码),用DeltaDoubleDelta编码可能体积能小几十倍。如果是低基数的整型,试试Gorilla。但说实话,除非你的磁盘真的非常紧张,或者网络传输带宽是瓶颈,否则在压缩算法上投入的优化收益,往往不如前面提到的引擎、分区、索引的调整来得明显。别本末倒置了。

写在最后:优化是门手艺,不是玄学

ClickHouse查询慢,千万别上来就怪硬件不行或者ClickHouse垃圾。静下心来,用EXPLAIN看看执行计划(虽然它比较简略),用system.query_logsystem.trace_log(如果开了)分析一下慢查询到底时间花在哪了——是在读取数据?还是在做聚合?还是网络传输?

优化就像看病,得先号准脉。是表引擎水土不服,还是分区切得稀碎,或者是索引根本没命中?从这些最根本的“器官”入手去调整,往往比吃“加内存”这种补药要管用得多。

行了,不废话了,如果你正对着慢查询日志发愁,不如就从重新审视一下你的表结构和索引设计开始吧。有时候,一个简单的字段顺序调换,可能就是“快如闪电”和“慢到抓狂”的区别。

扫描二维码推送至手机访问。

版权声明:本文由www.ysyg.cn发布,如需转载请注明出处。

本文链接:http://www.ysyg.cn:80/?id=449

“ClickHouse查询慢怎么从表引擎和索引上优化” 的相关文章

扛不住!华中地区网站老板,正被这种“温柔一刀”放倒

# 扛不住!华中地区网站老板,正被这种“温柔一刀”放倒 我上个月跟武汉一个做本地电商的朋友吃饭,他愁眉苦脸地跟我说:“服务器最近又抽风了,一到下午就卡,用户投诉刷屏,但后台CPU和带宽看着都正常啊。” 我让他把访问日志拉出来看看。好家伙,满屏都是来自同…

分析高防CDN的Cookie校验与重定向算法对CC肉鸡的自动清洗

# 当Cookie遇上“肉鸡”:高防CDN那点不为人知的清洗内幕 说实话,我这两年看过的站点防护配置,少说也有几百个了。最让我哭笑不得的不是那些裸奔的——人家至少心里有数。反而是那些上了“高防”还被打趴的,问题往往出在细节上,比如今天要聊的这个:**Co…

探究针对UDP反射攻击的报文荷载深度匹配(DPI)过滤算法

# 当UDP洪水“借刀杀人”,我们怎么把真凶揪出来? 我得先跟你讲个真事儿。 上个月,有个做游戏联运的朋友半夜给我打电话,声音都是抖的。他们服务器突然就瘫了,流量监控上那条线直接顶到天花板。客服电话被打爆,玩家群里骂声一片。最要命的是——他们明明买了“…

详解HTTP请求头解析算法在过滤变种应用层攻击中的作用

# HTTP请求头里藏玄机:一招拆穿变种应用层攻击的“假身份” 咱们做防护的,最头疼的可能不是那种“硬碰硬”的流量洪水——毕竟堆带宽、上高防还能扛一扛。真正让人后背发凉的,是那些伪装成正常请求的变种应用层攻击。它们就像混进人群的刺客,穿着和你一样的衣服,…

解析高防 CDN 接入后搜索引擎收录异常的 Crawl 抓取规则优化

# 高防CDN一上,网站就“消失”了?聊聊搜索引擎抓取那些坑 这事儿我上个月刚帮一个做电商的朋友处理完,太典型了。 他兴冲冲地给官网上了个高防CDN,防护效果是立竿见影,攻击流量被洗得干干净净。结果没高兴两天,运营就跑来哭诉:“老板,咱们网站在百度上搜…

探讨高防 CDN 应对 API 羊毛党恶意请求的频率检测与拦截逻辑

# 当羊毛党盯上你的API:高防CDN怎么把“薅羊毛”变成“啃钢板”? 我前两天跟一个做电商的朋友喝酒,他愁眉苦脸地说,刚上线的“新人1分钱领好礼”活动,后台API差点被刷爆了。活动预算半天就没了,进来的全是机器人,真用户一个没见着。他最后苦笑:“那感觉…