跳转到主要内容
本指南属于社区交流会经验总结系列的一部分。若想了解更多真实场景中的解决方案和洞察,可按具体问题浏览 如果你在使用 Materialized Views 时遇到问题,欢迎查看 Materialized Views 社区经验指南。 如果你正遇到慢查询问题,并希望查看更多示例,我们还提供了查询优化指南。

按基数从低到高排序

当低基数列排在前面时,ClickHouse 的主索引效果最佳,这样就能高效地跳过大块数据。而键中靠后的高基数列,则可在这些数据块内提供更细粒度的排序。应从唯一值较少的列开始 (如 status、category、country) ,以唯一值较多的列结束 (如 user_id、timestamp、session_id) 。 有关基数和主索引的更多信息,请参阅:

时间粒度至关重要

ORDER BY 子句中使用时间戳时,需要权衡基数与精度。微秒级精度的时间戳会带来非常高的基数 (几乎每一行对应一个唯一值) ,从而降低 ClickHouse 稀疏主索引的效果。经过舍入的时间戳基数较低,更有利于索引跳过,但会损失基于时间的查询精度。
runnable editable

关注单个查询,而不是平均值

在调试 ClickHouse 性能时,不要依赖平均查询时间或整体系统指标。相反,要找出具体某条查询为什么会变慢。系统的平均性能可能很好,但单个查询仍可能因为内存耗尽、过滤效果不佳或高基数操作而表现不佳。 正如 ClickHouse CTO Alexey 所说:“正确的做法,是问问自己为什么这个特定查询花了五秒钟才处理完……我不在乎中位数,也不在乎其他查询处理得有多快。我只关心我的查询” 当某条查询变慢时,不要只看平均值。要问:“为什么偏偏这条查询这么慢?”并检查实际的资源使用模式。

内存与行扫描

Sentry 是一个以开发者为中心的错误跟踪平台,每天处理来自 400 多万名开发者的数十亿条事件。他们的关键洞察是:“在这种特定情况下,真正决定内存占用的是分组键的基数”——拖垮性能的并不是行扫描,而是高基数聚合导致的内存耗尽。 当查询失败时,要先判断是内存问题 (分组过多) ,还是扫描问题 (行过多) 。 GROUP BY user_id, error_message, url_path 这样的查询,会为这三个值的每一种唯一组合创建一个独立的内存状态。随着用户数、错误类型和 URL 路径增多,你很容易生成数百万个聚合状态,而这些状态都必须同时保存在内存中。 对于极端情况,Sentry 会使用确定性采样。10% 的样本可将内存使用量降低 90%,同时对大多数聚合操作仍能保持约 5% 的精度:
这可确保每次查询中都会出现相同的用户,从而在不同时间段内获得一致的结果。关键在于:cityHash64() 对相同输入始终会生成相同的哈希值,因此 user_id = 12345 总会被哈希为同一个值,保证该用户要么始终出现在你的 10% 样本中,要么始终不会出现——不会在不同查询之间来回“跳变”。

Sentry 的位掩码优化

当按高基数列 (如 URL) 进行聚合时,每个唯一值都会在内存中创建单独的聚合状态,从而导致内存耗尽。Sentry 的解决方案是:不按实际的 URL 字符串分组,而是按可压缩为位掩码的布尔表达式分组。 如果你也遇到这种情况,可以在自己的表上试试下面这条查询:
你无需在内存中存储每个唯一的字符串,而是将关于这些字符串的查询结果以整数形式存储。这样一来,无论数据有多么多样,聚合状态都会受到限制且非常小。 Sentry 工程团队表示:“这些高负载查询的速度提升了 10 倍以上,而我们的 memory usage 降低了 100 倍 (更重要的是,它是有界的) 。我们最大的客户在搜索回放时不再遇到 error,如今我们也能够支持任意规模的客户,而不会耗尽内存。“

视频资源

延伸阅读
最后修改于 2026年7月1日