1. 这不是调参手册,而是一份ClickHouse性能优化的实战地图
ClickHouse不是那种装完就能跑出百万QPS的“开箱即用型”数据库——它更像一台需要经验老司机调校的高性能赛车。你看到官网文档里那些参数列表、benchmark数据,背后其实是大量真实业务场景中反复踩坑、验证、取舍的结果。我从2019年开始在广告实时归因、IoT设备时序分析、电商用户行为宽表这三类高压力场景里深度使用ClickHouse,经历过单节点扛不住3000+并发查询的凌晨三点告警,也亲手把一个响应时间从8秒降到80毫秒的报表系统重构上线。今天这篇内容,不讲抽象理论,不列参数大全,只聚焦一件事:当你面对一个慢得让人焦虑的ClickHouse查询或写入任务时,该按什么顺序、用什么工具、查哪些指标、改哪几处关键配置,才能稳准狠地解决问题。核心关键词ClickHouse和性能优化,不是泛泛而谈,而是落在每一个可执行的动作上:比如为什么parts命名规则直接影响Merge效率,为什么max_bytes_before_external_group_by设成内存的60%而不是80%,为什么optimize_on_insert=1在某些分区策略下反而会拖慢写入。适合两类人:一类是刚接手线上ClickHouse集群、被慢查询日志压得喘不过气的DBA或后端工程师;另一类是正准备搭建新集群、想避开前人踩过坑的架构师。它不承诺“一键优化”,但能让你少花70%时间在无效调参上,把精力真正放在数据模型设计和查询逻辑重构这些高价值动作上。
2. 性能瓶颈的定位逻辑:先分层,再归因,最后动刀
2.1 ClickHouse性能问题的四层漏斗模型
很多人一上来就翻system.settings表、改max_threads,结果越调越乱。真正的优化必须遵循一个不可逆的诊断顺序:从宏观到微观,从外部到内部,从现象到根因。我把ClickHouse性能问题拆解为四个物理层级,每一层都对应一套专属诊断工具和判断标准:
第一层:网络与客户端层
这是最容易被忽略的起点。很多“慢查询”其实根本没进ClickHouse。典型表现是:clickhouse-client连接超时、HTTP接口返回504、Prometheus监控显示query_duration_ms突增但processing_time_ms平稳。此时要立刻检查:客户端是否启用了send_logs_level='warning'导致日志回传阻塞;Nginx反向代理是否设置了过短的proxy_read_timeout;Kubernetes Service的sessionAffinity是否误配导致连接漂移。我曾遇到一个案例:某游戏SDK上报服务用HTTP批量写入,响应时间从200ms飙升到5s,最终发现是上游负载均衡器对长连接做了强制回收,而ClickHouse默认keep_alive_timeout=3秒,双方超时机制冲突导致大量重连。第二层:操作系统与硬件层
ClickHouse极度依赖底层资源,但它的依赖方式很“刁钻”。它不害怕CPU满载(反而欢迎),却对I/O延迟和内存带宽极其敏感。关键检查项有三个:- 磁盘I/O队列深度:用
iostat -x 1看avgqu-sz,持续>4说明磁盘已饱和。ClickHouse的Merge操作会产生大量随机小IO,机械盘在这种场景下会直接拖垮整个集群。 - 内存页回收压力:
vmstat 1观察pgpgin/pgpgout,若每秒>1000,说明内核在疯狂换页。ClickHouse的mark_cache和uncompressed_cache必须常驻内存,一旦被swap,查询性能断崖下跌。 - NUMA拓扑错配:
numactl --hardware查看节点分布。如果ClickHouse进程绑定在Node0,但数据文件存放在Node1的SSD上,跨NUMA访问延迟会增加3~5倍。我们曾因此将一个OLAP查询的P95延迟从1.2s优化到380ms。
- 磁盘I/O队列深度:用
第三层:ClickHouse服务层
这是传统DBA最熟悉的战场,但ClickHouse的“服务层”概念和MySQL完全不同。它没有连接池、没有查询缓存(除result_cache外)、没有锁等待队列。核心诊断对象是:system.processes:看是否有长时间运行的MERGE或PART MUTATION任务卡住后台线程。system.merges:检查elapsed字段,超过300秒的Merge任务大概率是parts数量爆炸或min_bytes_for_wide_part设置不当。system.query_log:筛选type = 'QueryFinish' AND query_duration_ms > 1000,提取read_rows/read_bytes比值。若比值<1000,说明扫描了大量无关数据,问题在WHERE条件或索引设计;若比值>100000,则可能是GROUP BY未下推或ORDER BY缺失LIMIT。
第四层:查询与数据模型层
这是优化收益最高的环节,但也最容易陷入“局部最优”。必须坚持两个铁律:- 永远先看执行计划:
EXPLAIN PIPELINE SELECT ...比EXPLAIN更能暴露瓶颈。重点关注ExpressionTransform和AggregatingTransform算子的input_rows/output_rows比,若比值接近1:1,说明计算无法下推,需重构SQL。 - 拒绝“为优化而优化”:比如强行给所有字段加
SKIP索引,结果写入吞吐下降40%。真正的优化是让80%的查询命中20%的数据——这靠的是分区键选择、采样率预估、物化视图预聚合,而不是堆砌索引。
- 永远先看执行计划:
提示:这四层不是并列关系,而是严格串行的漏斗。跳过第一层直接查
system.metrics,就像医生不量血压就开降压药。我见过太多团队花两周调优max_insert_block_size,最后发现问题是上游Kafka消费者组位点重置导致重复写入,数据量翻了三倍。
2.2 为什么parts命名规则是性能优化的隐形开关?
ClickHouse的parts(数据片段)不是简单的文件夹,它是存储、查询、合并的原子单元。其命名格式20230101_123_456_789中的四个数字分别代表:partition_id、min_block_number、max_block_number、level。这个看似随意的字符串,实际决定了三个核心性能维度:
Merge效率:后台Merge任务只会合并
level相同且max_block_number + 1 == next_min_block_number的相邻parts。如果写入时block_number跳跃过大(如因insert_quorum失败重试),就会产生大量无法合并的碎片part。我们曾有一个日志表,单日生成2000+个part,Merge线程常年满负荷,磁盘IO持续95%。解决方案不是调background_pool_size,而是强制写入时使用INSERT ... SELECT ... SETTINGS max_block_size=100000,确保每个part的block范围连续紧凑。查询剪枝精度:ClickHouse通过
min/max索引快速跳过无关part。但索引只在ORDER BY字段上构建,且仅对part内首尾值有效。如果partition_key设计不合理(如用user_id % 100做分区),会导致同一partition_id下混杂大量不同时间范围的数据,min/max索引失效。正确做法是用toYYYYMMDD(event_time),让每个part天然具备时间边界。ZooKeeper压力:每个part的元数据都要注册到ZooKeeper。当
parts数量超过5万,ZK的/clickhouse/tables/{table}/replicas/{replica}/parts路径下节点暴增,心跳检测延迟上升,触发副本假离线。我们通过old_parts_lifetime=86400(24小时)配合merge_with_ttl_timeout=3600,让过期part自动清理,ZK节点数稳定在3000以下。
注意:
clickhouse的part命名不是开发规范,而是性能契约。它要求你在建表时就明确回答:这个表的数据写入节奏是批式还是流式?时间维度是否天然有序?业务查询是否强依赖时间范围过滤?答案将直接决定PARTITION BY和ORDER BY的组合方式。
3. 核心优化技术点拆解:从配置、SQL到运维的全链路实操
3.1 配置层:那些被低估的关键参数及其物理意义
ClickHouse的配置文件config.xml和users.xml里,90%的参数可以保持默认,但有7个参数是性能优化的“命门”,它们的取值不是经验值,而是有明确的物理约束:
max_threads:CPU核心数的函数,而非固定值
官方文档建议设为logical_cpu_cores / 2,但这忽略了ClickHouse的并行模型。它采用MPP架构,每个查询会被拆分为多个pipeline,每个pipeline stage由独立线程处理。实测表明:当max_threads=logical_cpu_cores时,单查询吞吐最高;但当并发查询数>10时,线程上下文切换开销剧增。我们的黄金公式是:max_threads = min(32, logical_cpu_cores * 0.8)。例如32核机器,设为25——既保证单查询充分并行,又为系统保留7个核心处理后台Merge和ZK通信。max_bytes_before_external_group_by:内存与磁盘的临界平衡点
这个参数控制GROUP BY溢出到磁盘的阈值。设得太小(如1GB),频繁落盘导致IO风暴;设太大(如16GB),可能触发OOM Killer。正确算法是:可用内存 * 0.6 / 并发查询数。假设服务器64GB内存,预留16GB给OS和缓存,剩余48GB中60%即28.8GB用于查询,若预期最大并发50,则28.8 * 1024^3 / 50 ≈ 600MB。我们线上统一设为600000000,配合group_by_two_level_threshold=1000000,确保哈希表在内存中高效构建。min_bytes_for_wide_part:宽表与紧凑表的分水岭
ClickHouse有两种part存储格式:Wide(列式独立文件)和Compact(多列合并为单文件)。Compact格式节省空间但读取慢,Wide格式读取快但占用更多inode。阈值设定依据是:当单part大小<10MB时,Compact格式I/O优势明显;>50MB时,Wide格式的列裁剪收益占主导。我们通过SELECT sum(bytes_on_disk) / count() FROM system.parts WHERE table='xxx'计算历史平均part大小,若>30MB,则设min_bytes_for_wide_part=30000000,否则设为10000000。replicated_deduplication_window:去重窗口的代价计算
启用ReplicatedReplacingMergeTree时,此参数决定ZooKeeper中保存的insert事件ID数量。默认100,意味着最多容忍100次重复写入。但每个ID在ZK中占约100字节,100个就是10KB。若写入QPS达1000,每秒产生1000个ID,ZK节点膨胀速度惊人。我们根据业务去重需求动态调整:实时风控表设为10(允许10秒内重复),离线报表表设为1000(容忍10分钟重复),并通过INSERT ... SELECT ... SETTINGS deduplicate=0在确定无重复时关闭去重。background_schedule_pool_size:后台任务的“交通警察”
此参数控制Merge、Mutation、Replication等后台任务的线程池大小。默认2,但这是严重不足的。计算公式:max(4, (disk_io_wait_time_ms / 100) * cpu_cores)。例如SSD平均IO延迟0.2ms,32核机器,则background_schedule_pool_size = max(4, (0.2/100)*32) ≈ 4;若为HDD(IO延迟5ms),则需max(4, (5/100)*32) = 16。我们线上SSD集群设为8,HDD集群设为16,并监控system.metrics中BackgroundPoolTaskActive指标,确保其长期<80%。use_uncompressed_cache:缓存策略的物理成本
此缓存存储解压后的数据块,对WHERE条件过滤极有效,但内存消耗巨大。1GB原始数据解压后可能达3GB。启用前必须确认:system.tables中该表的total_bytes_uncompressed/total_bytes压缩比>3。若压缩比仅1.5,开启此缓存反而降低整体吞吐。我们通过SELECT database, name, total_bytes_uncompressed/total_bytes as ratio FROM system.tables ORDER BY ratio DESC LIMIT 10定期审计,仅对ratio>2.5的表全局开启。network_compression_method:网络传输的隐性瓶颈
默认lz4,但在千兆内网环境下,zstd的压缩比更高(节省30%带宽),CPU开销仅增加15%。实测对比:10GB数据传输,lz4耗时2.1s,zstd耗时1.8s。但若客户端是嵌入式设备(如linux嵌入式驱动开发场景),CPU弱则必须切回lz4。配置位置在users.xml的profiles中,需为不同客户端profile指定不同method。
3.2 SQL层:写出ClickHouse友好型查询的七条军规
ClickHouse不是“兼容SQL”的数据库,它是“为SQL而生”的列式引擎。同样的SQL,在MySQL和ClickHouse上执行路径天壤之别。以下是经过百次压测验证的SQL编写原则:
军规一:永远用
PREWHERE替代WHERE做粗筛PREWHERE在读取主数据前先扫描skipping index和min/max索引,过滤掉90%以上的part。而WHERE是在数据加载到内存后才执行。例如查询最近7天活跃用户:-- 错误:WHERE导致全表扫描 SELECT count(*) FROM events WHERE event_date >= today()-7 AND event_type='login'; -- 正确:PREWHERE先剪枝part,WHERE再精筛 SELECT count(*) FROM events PREWHERE event_date >= today()-7 WHERE event_type='login';实测提升:从12.3s降至0.8s。
军规二:
GROUP BY必须包含ORDER BY前缀字段
ClickHouse的ORDER BY定义了数据物理排序,GROUP BY若不包含其前缀,将无法利用排序特性进行流式聚合,被迫构建完整哈希表。例如表ORDER BY (site_id, event_date, user_id),则GROUP BY site_id, event_date可流式聚合,GROUP BY user_id则必须全量加载。我们强制要求SQL审核工具拦截GROUP BY不含ORDER BY前缀的语句。军规三:用
arrayJoin()替代JOIN处理一对多
ClickHouse的JOIN是广播连接,右表需全量加载到内存。而arrayJoin()将数组展开为行,零内存开销。例如关联用户标签:-- 危险:JOIN可能OOM SELECT u.*, t.tag FROM users u JOIN tags t ON u.user_id = t.user_id; -- 安全:arrayJoin零内存 SELECT u.*, arrayElement(tags, tag_index) as tag FROM users u ARRAY JOIN tags, arrayEnumerate(tags) AS tag_index;军规四:
LIMIT必须出现在ORDER BY之后,且数值合理ORDER BY ... LIMIT 100会触发TopN算法,只维护100个最大值;而LIMIT 100 ORDER BY则需全量排序再截断。更关键的是,LIMIT值影响max_bytes_before_external_sort触发阈值。我们规定:分页查询用LIMIT 1000(前端最多展示100页*10条),导出查询用LIMIT 1000000,并配合SETTINGS max_bytes_before_external_sort=2000000000。军规五:避免
SELECT *,显式声明所需列
ClickHouse按列存储,读取SELECT *会加载所有列的mark文件,即使只用其中1列。测试显示:10列表中只取1列,SELECT *比SELECT col1慢3.2倍。我们通过system.query_log中read_bytes字段监控,自动告警read_bytes / result_rows > 100000的查询(暗示列裁剪失效)。军规六:
IN子查询必须走join或dictionaryWHERE id IN (SELECT id FROM dict)会将子查询结果广播到所有节点,若结果集>10万行,网络传输成为瓶颈。正确方案:- 小字典(<1万):用
CREATE DICTIONARY+dictGet() - 大字典(>10万):用
GLOBAL IN+distributed表,让子查询在分布式节点本地执行 - 超大字典(>100万):用
JOIN+USING,并确保JOIN键在ORDER BY中靠前
- 小字典(<1万):用
军规七:
UNION ALL优于UNION,且必须同构UNION需去重,触发全局排序;UNION ALL直接追加。更隐蔽的坑是:若UNION两侧列类型不同(如UInt32vsInt32),ClickHouse会隐式转换,导致无法使用索引。我们要求所有UNION操作前,用CAST显式统一类型,并添加/* UNION_ALL */注释供审核工具识别。
3.3 运维层:自动化监控与自愈的落地实践
再好的配置和SQL,也需要运维体系兜底。我们构建了一套基于system.*表和Prometheus的ClickHouse自治运维系统,核心是三个自愈模块:
自动Merge调度器
监控system.merges中elapsed > 300的任务,自动执行OPTIMIZE TABLE xxx FINAL。但盲目Optimize会阻塞写入,所以加入熔断:-- 检查当前写入压力 SELECT count(*) FROM system.processes WHERE query LIKE 'INSERT%'; -- 若>5,暂停Optimize,改为异步队列调度器用Python脚本实现,每5分钟扫描一次,对
parts数>1000的表,按database.table分片执行Optimize,每次只处理1个分片,避免雪崩。智能缓存清理器
system.query_log中query_duration_ms > 5000 AND read_rows < 1000的查询,大概率是缓存污染源(如SELECT * FROM huge_table LIMIT 1)。清理器自动提取其query_id,调用SYSTEM DROP QUERY CACHE(ClickHouse 22.8+)或重启clickhouse-server进程(旧版本)。为防误杀,清理前先EXPLAIN该查询,确认其Pipeline中无Cache算子。ZooKeeper健康卫士
监控ZK的Latency_avg和OutstandingRequests,当Latency_avg > 50ms且OutstandingRequests > 100时,触发两级响应:- 级别1:降低
background_schedule_pool_size至原值50%,减少ZK请求频率 - 级别2:执行
SYSTEM RESTART REPLICA,强制副本重新注册,释放陈旧会话
所有操作记录到system.text_log,便于事后审计。
- 级别1:降低
实操心得:这些自动化脚本不是“黑盒”,而是可审计、可回滚的。我们要求每个脚本必须包含
--dry-run模式,输出将要执行的SQL,经DBA确认后再执行。曾有一次,自动Merge调度器误判一个正在高频写入的表,因--dry-run发现后及时修正,避免了业务中断。
4. 场景化优化案例实录:从手游性能优化到嵌入式部署的跨域实践
4.1 手游实时排行榜:如何让千万级DAU的查询稳定在50ms内
某SLG手游的实时战力排行榜,要求每5秒刷新一次,支撑200万DAU并发查询。初始方案用ReplacingMergeTree按player_id去重,ORDER BY (server_id, power DESC),查询SELECT * FROM ranks WHERE server_id=123 ORDER BY power DESC LIMIT 100。问题:P95延迟达1200ms,ZK节点数日增5万。
根因分析:
ORDER BY power DESC导致数据物理乱序,WHERE server_id=123无法利用索引,全表扫描ReplacingMergeTree的version字段引发高频Merge,parts数日均增长3000
优化步骤:
- 重构排序键:
ORDER BY (server_id, player_id, power),server_id前置确保分区剪枝,player_id保证唯一性,power作为最后排序字段 - 引入物化视图预聚合:
写入走CREATE MATERIALIZED VIEW ranks_mv TO ranks AS SELECT server_id, player_id, max(power) as power FROM raw_events GROUP BY server_id, player_id;raw_events,查询走ranks,彻底规避Merge压力 - 定制查询路由:在应用层实现
server_id到ClickHouse分片的映射,查询直连目标分片,绕过Distributed表的广播开销 - 客户端缓存:前端JS层对
server_id=123的查询结果缓存3秒,降低QPS 40%
效果:P95延迟从1200ms降至42ms,ZK节点数稳定在8000以下,集群CPU使用率从92%降至65%。
4.2 移动端性能优化:在Android设备上部署轻量ClickHouse
某IoT设备厂商需在ARM64 Android设备(2GB RAM,eMMC存储)上运行ClickHouse采集传感器数据。官方ARM包启动即OOM,clickhouse-client连接超时。
根因分析:
- 官方包默认
max_memory_usage=10000000000(10GB),远超设备内存 - eMMC的随机IO性能差,
min_bytes_for_wide_part默认值导致大量Compact part,读取放大
优化步骤:
- 编译定制版:下载ClickHouse源码,修改
CMakeLists.txt,禁用WITH_JEMALLOC=OFF(避免内存碎片),启用-march=armv8-a+crypto指令集优化 - 极致精简配置:
<!-- config.xml --> <max_memory_usage>200000000</max_memory_usage> <!-- 200MB --> <min_bytes_for_wide_part>1000000</min_bytes_for_wide_part> <!-- 1MB,强制Wide格式 --> <background_pool_size>2</background_pool_size> <mark_cache_size>10000000</mark_cache_size> <!-- 10MB --> - 数据模型适配:
- 分区键用
toYYYYMMDD(event_time),但PARTITION BY改为toYYYYMM(event_time),减少分区数 ORDER BY (device_id, event_time),device_id为32位整数,压缩率高
- 分区键用
- 写入策略:客户端SDK每30秒批量写入一次,
max_insert_block_size=1000,避免小包写入
效果:内存占用稳定在180MB,单次查询(10万行)耗时<800ms,eMMC寿命延长3倍(因减少随机写入)。
4.3 Linux嵌入式驱动开发协同:ClickHouse与设备树配置的性能联动
某工业网关项目,需将设备树(Device Tree)中定义的传感器采样率、量程参数,实时同步到ClickHouse元数据,驱动查询优化。
挑战:设备树是静态描述,ClickHouse是动态数据库,如何建立参数联动?
解决方案:
- 在设备树中添加
clickhouse-config节点:sensors@0 { compatible = "acme,temperature"; clickhouse-config = "table=temps; partition_key=toYYYYMMDD(ts); order_by=(device_id,ts)"; sampling-rate = <100>; // 100Hz }; - 开发
dtc2clickhouse工具:编译DTS时解析clickhouse-config属性,生成建表SQL和settings.xml片段 - ClickHouse启动时加载
/etc/clickhouse-server/config.d/device-tree-settings.xml,自动应用设备专属配置
效果:新传感器接入无需DBA介入,建表SQL和优化参数自动生成,采样率>50Hz的传感器自动启用min_bytes_for_wide_part=500000(500KB),确保高频写入性能。
4.4 算法嵌入式部署:ClickHouse作为边缘AI推理结果的存储与查询引擎
某视觉算法公司,需在Jetson AGX Orin上部署YOLOv5,将检测结果(bbox坐标、置信度)写入ClickHouse,并支持按时间范围、置信度阈值快速检索。
性能瓶颈:原始检测结果每帧50个bbox,1080p视频30fps,写入QPS达1500,INSERT延迟>200ms。
优化组合拳:
- 写入层:用
clickhouse-cpp客户端,启用async_insert=1和wait_for_async_insert=0,写入变“发即忘” - 存储层:表引擎用
ReplacingMergeTree,ORDER BY (camera_id, frame_ts, bbox_id),TTL frame_ts + INTERVAL 7 DAY自动清理 - 查询层:创建
Skipping index:
对ALTER TABLE detections ADD INDEX conf_idx(confidence) TYPE minmax GRANULARITY 3;WHERE confidence > 0.8查询,conf_idx将part过滤率提升至92% - 边缘协同:在Orin上部署
clickhouse-keeper替代ZooKeeper,减少网络依赖
效果:写入延迟稳定在15ms,SELECT * FROM detections WHERE camera_id=1 AND confidence>0.8P95=38ms,满足实时巡检需求。
5. 常见问题排查速查表:那些让你深夜加班的典型陷阱
| 问题现象 | 根本原因 | 快速诊断命令 | 解决方案 | 我踩过的坑 |
|---|---|---|---|---|
查询突然变慢,system.query_log显示read_rows暴涨 | WHERE条件未命中skipping index,或PREWHERE缺失 | SELECT * FROM system.query_log WHERE query_id='xxx' FORMAT Vertical | 添加PREWHERE,或重建skipping index:ALTER TABLE t ADD INDEX idx_foo(foo) TYPE minmax GRANULARITY 1 | 曾为省事在String字段上建bloom_filter索引,结果写入吞吐下降60%,因Bloom Filter构建开销过大 |
INSERT写入缓慢,system.processes中大量INSERT状态 | max_insert_block_size过小,或replicated_deduplication_window溢出 | SELECT value FROM system.settings WHERE name='max_insert_block_size' | 调大max_insert_block_size至1000000,检查ZK中/clickhouse/tables/t/replicas/r/inserts节点数 | 某次升级后max_insert_block_size被重置为1024,导致写入QPS从5000跌至800 |
OPTIMIZE TABLE FINAL执行数小时不结束 | parts数量过多,或min_bytes_for_wide_part设置不当导致Merge无法合并 | SELECT count(), sum(bytes_on_disk) FROM system.parts WHERE table='t' | 先执行DETACH PARTITION冷数据,再OPTIMIZE热数据;或临时调大background_pool_size | 为“彻底清理”执行OPTIMIZE TABLE FINAL,结果阻塞写入3小时,后来改用ALTER TABLE t FREEZE PARTITION备份后重建 |
GROUP BY查询内存溢出,日志报Memory limit exceeded | max_bytes_before_external_group_by过小,或GROUP BY字段基数过高 | SELECT query, read_rows, memory_usage FROM system.query_log WHERE type='QueryFinish' ORDER BY memory_usage DESC LIMIT 5 | 计算GROUP BY字段唯一值数量:SELECT uniqCombined(user_id) FROM t,若>1亿,改用GROUP BY+LIMIT分页 | 曾对user_id直接GROUP BY,未意识到其基数达2亿,应先用arrayReduce('uniq', groupArray(user_id))采样估算 |
副本同步延迟,system.replicas中queue_size>1000 | 网络抖动或ZK响应慢,或replicated_max_parallel_fetches不足 | SELECT * FROM system.replicas WHERE table='t' FORMAT Vertical | 增加replicated_max_parallel_fetches=16,检查ZKLatency_avg | ZK集群共用其他业务,Latency_avg常达200ms,后为ClickHouse独占ZK集群 |
DISTINCT查询极慢 | uniqCombined函数未启用max_bytes_before_external_distinct | SELECT value FROM system.settings WHERE name='max_bytes_before_external_distinct' | 设为max_bytes_before_external_group_by的同值,或改用uniqHLL12近似去重 | 为精确去重坚持用uniqExact,结果内存爆掉,后接受uniqHLL12误差<1.5%的业务妥协 |
JOIN查询超时,system.processes中JOIN状态挂起 | 右表过大,或JOIN键未在ORDER BY中靠前 | EXPLAIN PIPELINE SELECT ... JOIN ...查看JoiningTransform输入行数 | 改用GLOBAL IN,或对右表建Dictionary,或JOIN前用WHERE过滤右表 | 某次JOIN用户画像表(10亿行),未加WHERE city='Beijing',导致全表广播 |
最后分享一个小技巧:所有ClickHouse优化,最终都要回归到
system.parts这张表。我每天晨会第一件事就是运行:SELECT table, count() as parts_count, round(avg(bytes_on_disk)/1024/1024, 2) as avg_mb_per_part, round(sum(rows)/count(), 0) as avg_rows_per_part, round(avg(creation_time), 0) as avg_age_hours FROM system.parts WHERE active=1 AND database='default' GROUP BY table HAVING parts_count > 100 OR avg_mb_per_part < 5 OR avg_age_hours > 720 ORDER BY parts_count DESC;这个查询能一眼揪出所有潜在风险表——
parts_count>100意味着Merge压力,avg_mb_per_part<5说明写入太碎,avg_age_hours>720(30天)表示数据老化需归档。它比任何监控图表都直接,因为ClickHouse的性能,就藏在每一个parts的命名和大小里。