跳转到主要内容
本节将通过常见场景说明如何使用不同的性能分析和优化技术,例如 analyzer查询性能分析避免使用 Nullable 列,从而提升 ClickHouse 查询性能。

了解查询性能

考虑性能优化的最佳时机,是在首次将数据摄取到 ClickHouse 之前设计数据 schema的时候。  但说实话,很难预测你的数据会增长到什么规模,或者会执行哪些类型的查询。  如果你已经有一个现有部署,并且有几个查询希望加以改进,那么第一步就是了解这些查询的性能表现,以及为什么有些查询能在几毫秒内完成,而另一些则需要更长时间。 ClickHouse 提供了丰富的工具,帮助你了解查询是如何执行的,以及执行过程中消耗了哪些资源。  在本节中,我们将介绍这些工具及其使用方法。 

基本注意事项

为了理解查询性能,我们先来看看 ClickHouse 在执行查询时会发生什么。  下面的内容是刻意简化后的版本,也省略了一些细节;目的不是让你被各种细节淹没,而是帮助你快速建立对基本概念的认识。更多信息请参阅查询分析器。  从较高层面来看,ClickHouse 执行查询时大致会经历以下过程: 
  • 查询解析与分析
查询会先被解析和分析,然后生成一个通用的查询执行计划。 
  • 查询优化
查询执行计划会被优化,剔除不必要的数据,并基于查询计划构建查询管道。 
  • 查询管道执行
数据会被并行读取和处理。这个阶段中,ClickHouse 会实际执行过滤、聚合、排序等查询操作。 
  • 最终处理
结果在发送给客户端之前,会先进行合并、排序并格式化为最终结果。 实际上,这一过程中还会发生许多优化。我们会在本指南后面进一步讨论其中的一些内容;不过现在,这几个主要概念已经足以帮助我们理解 ClickHouse 执行查询时幕后发生了什么。  有了这一层面的理解之后,我们再来看看 ClickHouse 提供了哪些工具,以及如何利用它们来跟踪影响查询性能的指标。 

数据集

我们将通过一个真实示例来说明我们如何优化查询性能。  我们以 NYC Taxi 数据集为例,其中包含纽约市的出租车行程数据。首先,我们在不做任何优化的情况下摄取 NYC Taxi 数据集。 下面的命令用于创建表,并从 S3 存储桶插入数据。请注意,这里我们有意根据数据推断 schema,而这并不是优化后的做法。
让我们来看一下根据数据自动推断出的表 schema。

找出慢查询

查询日志

默认情况下,ClickHouse 会在查询日志中收集并记录每个已执行查询的信息。这些数据存储在 system.query_log 表中。  对于每个已执行的查询,ClickHouse 都会记录查询执行时间、读取行数等统计信息,以及 CPU、内存使用量或文件系统缓存命中次数等资源使用情况。  因此,排查慢查询时,查询日志是一个很好的切入点。你可以轻松找出执行时间较长的查询,并查看每个查询的资源使用信息。  下面我们来找出 NYC taxi 数据集中运行时间最长的前五个查询。
字段 query_duration_ms 表示该查询的执行耗时。查看查询日志中的结果,我们可以看到,第一个查询的运行耗时为 2967ms,还有优化空间。  你可能还想通过检查占用内存或 CPU 最多的查询,了解哪些查询给系统带来了较大压力。 
我们把找到的长时间运行的查询单独挑出来,再重复运行几次,以了解其响应时间。  此时,务必将 enable_filesystem_cache 设置为 0,以关闭文件系统缓存,从而提高结果的可复现性。
汇总如下,便于阅读。 下面我们更具体地看看这些查询分别实现了什么。 
  • 查询 1 计算平均速度超过 30 英里/小时的行程中,行驶距离的分布。
  • 查询 2 统计每周的行程数量和平均费用。 
  • 查询 3 计算数据集中每次行程的平均时长。
这些查询都不涉及特别复杂的处理,唯一的例外是第一个查询,因为它每次执行时都要动态计算行程时长。不过,这些查询每一个的执行时间都超过了 1 秒,而在 ClickHouse 的世界里,这已经算是非常长了。我们还可以注意到这些查询的内存占用:每个查询大约都用了 400 Mb 内存,这已经相当高了。此外,每个查询读取的行数看起来都一样 (即 3.2904 亿) 。下面我们快速确认一下这个表里到底有多少行。
该表包含 3.2904 亿行,因此每次查询都需要对整张表进行全表扫描。

Explain 语句

既然我们已经找到了一些耗时较长的查询,接下来让我们了解它们的执行方式。为此,ClickHouse 支持 EXPLAIN 语句命令。这是一个非常实用的工具,无需实际执行查询,即可详细展示查询执行的各个阶段。对于不熟悉 ClickHouse 的用户来说,其输出内容可能较为繁杂,但它仍然是深入理解查询执行机制的必备工具。 文档中有一份详细的指南,介绍了 EXPLAIN 语句的用途及如何使用它分析查询执行过程。本文不再赘述该指南的内容,而是重点介绍几个有助于定位查询执行性能瓶颈的命令。 Explain indexes = 1 我们先使用 EXPLAIN indexes = 1 来检查查询计划。查询计划是一棵树形结构,展示了查询的执行方式。通过它,您可以了解查询中各子句的执行顺序。EXPLAIN 语句返回的查询计划需从下往上阅读。 我们来试用第一个耗时较长的查询。
输出结果一目了然。该查询首先从 nyc_taxi.trips_small_inferred 表中读取数据,然后应用 WHERE 子句,根据计算值对行进行过滤,过滤后的数据进入聚合阶段并完成分位数计算,最后对结果排序输出。 可以看到,此处没有使用任何主键——这是预期行为,因为我们在创建表时并未定义主键。因此,ClickHouse 对该表执行了全表扫描来完成此次查询。 EXPLAIN PIPELINE EXPLAIN Pipeline 展示了查询的具体执行策略。通过它,您可以了解 ClickHouse 实际上是如何执行我们之前查看的通用查询计划的。
在这里,我们可以注意到执行该查询所使用的线程数:59 个线程,说明并行化程度较高。这加快了查询速度——同样的查询在配置较低的机器上执行会耗费更长时间。并行运行的线程数量可以解释该查询内存占用较高的原因。 理想情况下,您应以相同的方式排查所有慢查询,从而识别不必要的复杂查询计划,并了解每个查询读取的行数及其资源消耗情况。

方法

在生产环境的部署中识别有问题的查询并不容易,因为在任意时刻,你的 ClickHouse 部署中很可能都有大量查询正在执行。  如果你知道是哪个用户、数据库或表存在问题,可以使用 system.query_logs 中的 usertablesdatabases 字段来缩小搜索范围。  一旦确定了要优化的查询,就可以开始着手优化。在这个阶段,开发者常犯的一个错误是同时改动多项内容,做一些临时性的实验,结果往往是得到好坏参半的结果;更重要的是,无法清楚理解究竟是什么让查询变快了。  查询优化需要有条理的方法。我说的不是高级基准测试,而是建立一个简单的流程,帮助你理解自己的改动会如何影响查询性能;这样做会很有帮助。  先从查询日志中找出慢查询,再单独分析可能的改进点。测试查询时,务必禁用文件系统缓存。 
ClickHouse 会利用缓存在不同阶段加速查询性能。这对查询性能是有益的,但在故障排查时,它可能会掩盖潜在的 I/O 瓶颈或不合理的表 schema。因此,我建议你在测试期间关闭文件系统缓存。请确保在生产环境中将其启用。
一旦识别出潜在的优化项,建议逐项实施,这样更容易跟踪它们对性能的影响。下图展示了总体方法。 最后,要注意异常值;查询偶尔运行缓慢是很常见的,可能是因为某个用户执行了一条高开销的临时查询,也可能是系统由于其他原因正处于高压状态。你可以按 normalized_query_hash 字段分组,以识别那些被定期执行的高开销查询。这些通常才是你真正需要重点调查的对象。

基础优化

现在我们已经有了可供测试的框架,可以开始着手优化了。 最好的起点是先看数据是如何存储的。对任何数据库来说,读取的数据越少,查询执行得就越快。  具体取决于你摄取数据的方式,你可能已经利用 ClickHouse 的功能,根据摄取的数据推断出表 schema。虽然这对快速上手非常方便,但如果你想优化查询性能,就需要检查数据 schema,使其尽可能契合你的使用场景。

Nullable

最佳实践文档中所述,应尽可能避免使用 Nullable 列。它们虽然会让数据摄取机制更灵活,因此很容易被频繁使用,但由于每次都需要处理一个额外的列,会对性能产生负面影响。 运行一条统计包含 NULL 值的行数的 SQL 查询,就能轻松找出你的表中哪些列实际上需要使用 Nullable。
只有 mta_taxpayment_type 这两列包含 NULL 值。其余字段不应使用 Nullable 类型的列。

低基数

对于 String 类型,一个很容易采用的优化方式是尽量使用 LowCardinality 数据类型。如低基数文档所述,ClickHouse 会对 LowCardinality 列应用字典编码,从而显著提升查询性能。  判断哪些列适合使用 LowCardinality 的一个简单经验法则是:任何唯一值少于 10,000 个的列,都是非常理想的候选项。 你可以使用以下 SQL 查询来找出唯一值较少的列。
由于这四列的基数较低,ratecode_idpickup_location_iddropoff_location_idvendor_id 都很适合使用 LowCardinality 字段类型。

优化数据类型

ClickHouse 支持大量数据类型。请务必根据你的使用场景选择尽可能小且合适的数据类型,以优化性能并减少磁盘存储占用。  对于数值类型,你可以查看数据集中的最小值和最大值,以确认当前的精度值是否符合数据集的实际情况。 
对于日期,应选择与你的数据集相匹配且最适合你计划执行的查询的精度。

应用优化

创建一个新表来使用优化后的 schema,并重新摄取数据。
我们使用新表再次运行这些查询,检查是否有所改进。  可以看到,查询时间和内存占用都有所改善。得益于数据 schema 的优化,表示这些数据所需的总数据量减少了,从而降低了内存消耗并缩短了处理时间。  接下来检查这些表的大小,看看差异。 
新表明显比之前的表更小。可以看到,该表占用的磁盘空间减少了约 34% (7.38 GiB 对比 4.89 GiB) 。

主键的重要性

ClickHouse 中的主键与大多数传统数据库系统中的主键工作方式不同。在那些系统中,主键用于保证唯一性和数据完整性。任何试图插入重复主键值的操作都会被拒绝,通常还会创建 B-tree 或基于哈希的索引来加快查找。  在 ClickHouse 中,主键的作用不同;它既不保证唯一性,也无助于维护数据完整性。相反,它的设计目的是优化查询性能。主键定义了数据在磁盘上的存储顺序,并以稀疏索引的形式实现,存储指向每个粒度首行的指针。
在 ClickHouse 中,粒度是查询执行期间读取数据的最小单位。每个粒度最多包含固定数量的行,由 index_granularity 决定,默认值为 8192 行。粒度会按主键顺序连续存储。 
选择一组合适的主键对性能至关重要。实际上,将相同的数据存储在不同的表中,并使用不同的主键组合来加速特定的一组查询,是一种很常见的做法。  ClickHouse 支持的其他选项 (例如 Projection 或 materialized view) 也允许你在同一份数据上使用不同的主键组合。本系列博客的第二部分将更详细地介绍这一点。 

选择主键

选择合适的一组主键是个复杂的话题,可能需要在多种方案之间权衡,并通过实验找出最佳组合。  这里我们先遵循几个简单原则: 
  • 使用大多数查询中会用作过滤器的字段
  • 优先选择基数较低的列 
  • 考虑在主键中加入时间相关的组件,因为在带有 timestamp 的数据集中按时间过滤非常常见。 
在这个示例中,我们将尝试以下主键:passenger_countpickup_datetimedropoff_datetime。  passenger_count 的基数较低 (只有 24 个唯一值) ,而且会在慢查询中用到。我们还加入了 timestamp 字段 (pickup_datetimedropoff_datetime) ,因为它们也经常作为过滤条件使用。 创建一个包含这些主键的新表,并重新摄取数据。
然后,我们再次运行查询。我们汇总了这三次实验的结果,以查看执行时间、处理行数和内存占用方面的改进。 
查询 1
第 1 次运行第 2 次运行第 3 次运行
耗时1.699 sec1.353 sec0.765 sec
处理的行数329.04 million329.04 million329.04 million
峰值内存占用440.24 MiB337.12 MiB444.19 MiB
查询 2
第 1 次运行第 2 次运行第 3 次运行
耗时1.419 sec1.171 sec0.248 sec
处理的行数329.04 million329.04 million41.46 million
峰值内存占用546.75 MiB531.09 MiB173.50 MiB
查询 3
运行 1运行 2运行 3
耗时1.414 sec1.188 sec0.431 sec
处理的行数329.04 million329.04 million276.99 million
峰值内存占用451.53 MiB265.05 MiB197.38 MiB
可以看到,执行时间和内存占用均有显著改善。  查询 2 从主键中受益最大。我们来看看其生成的查询计划与之前有何不同。
得益于主键,系统只需选择表中的部分粒度。这一点本身就能大幅提升查询性能,因为 ClickHouse 需要处理的数据明显更少。

后续步骤

希望本指南能帮助你更好地了解如何使用 ClickHouse 排查慢查询,并进一步提升查询速度。若想深入了解这一主题,你可以继续阅读查询分析器性能分析的相关内容,更清楚地了解 ClickHouse 究竟是如何执行查询的。 随着你对 ClickHouse 特性的进一步熟悉,建议你阅读分区键数据跳过索引的相关内容,了解更多可用于加速查询的高级技术。
最后修改于 2026年6月12日