了解查询性能
基本注意事项
- 查询解析与分析
- 查询优化
- 查询管道执行
- 最终处理
数据集
找出慢查询
查询日志
system.query_log 表中。
对于每个已执行的查询,ClickHouse 都会记录查询执行时间、读取行数等统计信息,以及 CPU、内存使用量或文件系统缓存命中次数等资源使用情况。
因此,排查慢查询时,查询日志是一个很好的切入点。你可以轻松找出执行时间较长的查询,并查看每个查询的资源使用信息。
下面我们来找出 NYC taxi 数据集中运行时间最长的前五个查询。
query_duration_ms 表示该查询的执行耗时。查看查询日志中的结果,我们可以看到,第一个查询的运行耗时为 2967ms,还有优化空间。
你可能还想通过检查占用内存或 CPU 最多的查询,了解哪些查询给系统带来了较大压力。
enable_filesystem_cache 设置为 0,以关闭文件系统缓存,从而提高结果的可复现性。
下面我们更具体地看看这些查询分别实现了什么。
- 查询 1 计算平均速度超过 30 英里/小时的行程中,行驶距离的分布。
- 查询 2 统计每周的行程数量和平均费用。
- 查询 3 计算数据集中每次行程的平均时长。
Explain 语句
nyc_taxi.trips_small_inferred 表中读取数据,然后应用 WHERE 子句,根据计算值对行进行过滤,过滤后的数据进入聚合阶段并完成分位数计算,最后对结果排序输出。
可以看到,此处没有使用任何主键——这是预期行为,因为我们在创建表时并未定义主键。因此,ClickHouse 对该表执行了全表扫描来完成此次查询。
EXPLAIN PIPELINE
EXPLAIN Pipeline 展示了查询的具体执行策略。通过它,您可以了解 ClickHouse 实际上是如何执行我们之前查看的通用查询计划的。
方法
system.query_logs 中的 user、tables 或 databases 字段来缩小搜索范围。
一旦确定了要优化的查询,就可以开始着手优化。在这个阶段,开发者常犯的一个错误是同时改动多项内容,做一些临时性的实验,结果往往是得到好坏参半的结果;更重要的是,无法清楚理解究竟是什么让查询变快了。
查询优化需要有条理的方法。我说的不是高级基准测试,而是建立一个简单的流程,帮助你理解自己的改动会如何影响查询性能;这样做会很有帮助。
先从查询日志中找出慢查询,再单独分析可能的改进点。测试查询时,务必禁用文件系统缓存。
ClickHouse 会利用缓存在不同阶段加速查询性能。这对查询性能是有益的,但在故障排查时,它可能会掩盖潜在的 I/O 瓶颈或不合理的表 schema。因此,我建议你在测试期间关闭文件系统缓存。请确保在生产环境中将其启用。一旦识别出潜在的优化项,建议逐项实施,这样更容易跟踪它们对性能的影响。下图展示了总体方法。 最后,要注意异常值;查询偶尔运行缓慢是很常见的,可能是因为某个用户执行了一条高开销的临时查询,也可能是系统由于其他原因正处于高压状态。你可以按
normalized_query_hash 字段分组,以识别那些被定期执行的高开销查询。这些通常才是你真正需要重点调查的对象。
基础优化
Nullable
mta_tax 和 payment_type 这两列包含 NULL 值。其余字段不应使用 Nullable 类型的列。
低基数
ratecode_id、pickup_location_id、dropoff_location_id 和 vendor_id 都很适合使用 LowCardinality 字段类型。
优化数据类型
应用优化
可以看到,查询时间和内存占用都有所改善。得益于数据 schema 的优化,表示这些数据所需的总数据量减少了,从而降低了内存消耗并缩短了处理时间。
接下来检查这些表的大小,看看差异。
主键的重要性
在 ClickHouse 中,粒度是查询执行期间读取数据的最小单位。每个粒度最多包含固定数量的行,由 index_granularity 决定,默认值为 8192 行。粒度会按主键顺序连续存储。
选择一组合适的主键对性能至关重要。实际上,将相同的数据存储在不同的表中,并使用不同的主键组合来加速特定的一组查询,是一种很常见的做法。
ClickHouse 支持的其他选项 (例如 Projection 或 materialized view) 也允许你在同一份数据上使用不同的主键组合。本系列博客的第二部分将更详细地介绍这一点。
选择主键
- 使用大多数查询中会用作过滤器的字段
- 优先选择基数较低的列
- 考虑在主键中加入时间相关的组件,因为在带有 timestamp 的数据集中按时间过滤非常常见。
passenger_count、pickup_datetime 和 dropoff_datetime。
passenger_count 的基数较低 (只有 24 个唯一值) ,而且会在慢查询中用到。我们还加入了 timestamp 字段 (pickup_datetime 和 dropoff_datetime) ,因为它们也经常作为过滤条件使用。
创建一个包含这些主键的新表,并重新摄取数据。
| 查询 1 | |||
|---|---|---|---|
| 第 1 次运行 | 第 2 次运行 | 第 3 次运行 | |
| 耗时 | 1.699 sec | 1.353 sec | 0.765 sec |
| 处理的行数 | 329.04 million | 329.04 million | 329.04 million |
| 峰值内存占用 | 440.24 MiB | 337.12 MiB | 444.19 MiB |
| 查询 2 | |||
|---|---|---|---|
| 第 1 次运行 | 第 2 次运行 | 第 3 次运行 | |
| 耗时 | 1.419 sec | 1.171 sec | 0.248 sec |
| 处理的行数 | 329.04 million | 329.04 million | 41.46 million |
| 峰值内存占用 | 546.75 MiB | 531.09 MiB | 173.50 MiB |
| 查询 3 | |||
|---|---|---|---|
| 运行 1 | 运行 2 | 运行 3 | |
| 耗时 | 1.414 sec | 1.188 sec | 0.431 sec |
| 处理的行数 | 329.04 million | 329.04 million | 276.99 million |
| 峰值内存占用 | 451.53 MiB | 265.05 MiB | 197.38 MiB |