ClickHouse中表排序键对与查询性能至关重要。本文介绍当使用非排序键作为查询条件时如何提升查询性能。通过对非排序键定义投影,然后物化排序结果,利用空间换时间策略,提升查询性能。
当创建MergeTree表时,需要指定列的顺序作为排序键。排序键的顺序对查询性能影响很大,因为排序键决定需要计算的数据在磁盘上排列在一起的紧密程度。
当选择排序键时,应该遵循下面几个规则:
当查询过滤条件有多个列时,我们如何定义排序键,下面通过示例进行说明。
CREATE TABLE deleteme
(
`product_id` UInt64,
`client_id` UInt64
)
ENGINE = MergeTree
PARTITION BY product_id % 10
ORDER BY (product_id, client_id) AS
SELECT number % 100 product_id, number % 100 client_id
FROM numbers(100000000)
整个表占用空间较小,仅~7MB:
SELECT formatReadableSize(total_bytes)
FROM system.tables
WHERE name = 'deleteme'
FORMAT Vertical
Row 1:
──────
formatReadableSize(total_bytes): 7.64 MiB
运行带product_id的查询条件,仅需要~1M行,因为表第一个排序键为product_id:
SELECT *
FROM deleteme
WHERE product_id = 10
FORMAT `Null`
0 rows in set. Elapsed: 0.014 sec. Processed 1.03 million rows, 16.52 MB (72.40 million rows/s., 1.16 GB/s.)
但如果过滤条件使用client_id(第二个排序键),ClickHouse查询时间增加仅3倍,由于相同的client_id在product_id排序的磁盘上不同块中重复存在,ClickHouse需要读取和处理更多的数据来运行查询:
SELECT *
FROM deleteme
WHERE client_id = 10
FORMAT `Null`
0 rows in set. Elapsed: 0.048 sec. Processed 2.97 million rows, 31.98 MB (61.42 million rows/s., 661.52 MB/s.)
下面通过定义投影解决上面查询性能问题。投影概念很简单,即对client_id定义排序并重新排序,从而提升对client_id的查询性能。请看示例:
ALTER TABLE deleteme
ADD PROJECTION deleteme_by_client_id
(
SELECT *
ORDER BY client_id
)
ALTER TABLE deleteme
MATERIALIZE PROJECTION deleteme_by_client_id
PROJECTION 定义了数据按client_id进行排序,当新的部分需要合并时,会以client_id为顺序组织数据。在查询时ClickHouse会透明地使用按Client_id排序的部分。
现在再次执行上节中的查询,可以看到仅读取~1M行数据:
SELECT *
FROM deleteme
WHERE client_id = 10
FORMAT `Null`
Query id: 51a55fec-d526-480b-870b-424a0c6471d3
0 rows in set. Elapsed: 0.052 sec. Processed 1.25 million rows, 18.28 MB (24.17 million rows/s., 352.53 MB/s.)
为了确认projection对性能有提升作用,我们可以检查query_log:
SELECT projections
FROM system.query_log
WHERE (event_time > (now() - toIntervalMinute(5))) AND (query_id = '51a55fec-d526-480b-870b-424a0c6471d3')
LIMIT 1
FORMAT Vertical
Row 1:
──────
projections: ['default.deleteme.deleteme_by_client_id']
当然采用投影会重复存储数据,但在一定场景中可以接受空间和时间的平衡,特别针对较小的表。
在命令行中执行查询,ClickHouse会自动生成查询性能统计。对于相同的查询会有多种写法,每种方式的查询性能可能有差异。通过分析性能统计,可能会发现最佳的查询方案。
我们可以使用 FORMAT Null
子句执行查询,则仅返回查询性能统计。上面的示例我们就使用了该功能。
SELECT *
FROM system.query_log
WHERE event_time > (now() - toIntervalMinute(10))
FORMAT `Null`
Query id: 7a125064-5422-471c-a170-e18601b2d631
Ok.
0 rows in set. Elapsed: 0.019 sec. Processed 49.86 thousand rows, 1.81 MB (2.61 million rows/s., 94.45 MB/s.)
我们也可以使用FORMAT Vertical子句,是的查询结果按列方式排列,对于返回数据行较少,但数据列时查看数据比较方便。
SELECT * FROM table_with_a_lot_of_columns FORMAT Vertical
Row 1:
────────
type: QueryFinish
event_date: 2022-09-22
event_time: 2022-09-22 09:29:58
event_time_microseconds: 2022-09-22 09:29:58.298699
query_start_time: 2022-09-22 09:29:58
query_start_time_microseconds: 2022-09-22 09:29:58.296902
query_duration_ms: 1
read_rows: 0
read_bytes: 0
written_rows: 60
written_bytes: 8879
result_rows: 0
result_bytes: 0
memory_usage: 4325156
current_database: public
本文介绍了排序键对查询的作用,并通过示例对比使用projection提升查询性能,最后也提及如何在命令行下查询性能统计信息。参考文档:https://www.tinybird.co/clickhouse/knowledge-base/improve-performance-inverted-index