未使用 PREWHERE 优化的查询处理
① 该查询包含对
town 列的过滤条件,而该列是表主键的一部分,因此也属于主索引的一部分。
② 为加快查询速度,ClickHouse 会将表的主索引加载到内存中。
③ 它会扫描索引条目,以识别 town 列中哪些粒度可能包含与谓词匹配的行。
④ 这些可能相关的粒度会被加载到内存中,同时还会加载查询所需其他列中位置对齐的粒度。
⑤ 然后在查询执行过程中应用其余过滤条件。
如你所见,如果不使用 PREWHERE,在执行过滤之前,所有可能相关的列都会先被加载,即使实际上只有少数行真正匹配。
PREWHERE 如何提高查询效率
① 查询包含对
town 列的过滤条件,而该列是表主键的一部分,因此也属于主索引的一部分。
② 与不使用 PREWHERE 子句时类似,为了加速查询,ClickHouse 会将主索引加载到内存中,
③ 然后扫描索引条目,以识别 town 列中哪些粒度可能包含与谓词匹配的行。
现在,由于使用了 PREWHERE 子句,下一步就有所不同了:ClickHouse 不会一开始就读取所有相关列,而是按列逐步过滤数据,只加载真正需要的内容。这会大幅减少 I/O,尤其是在宽表中。
在每一步中,它只会加载至少包含一行“保留下来”的粒度——也就是与上一个过滤条件匹配的粒度。因此,对每个过滤条件而言,需要加载和评估的粒度数量都会单调递减:
步骤 1:按 town 过滤ClickHouse 会先 ① 读取
town 列中选定的粒度,并检查其中哪些粒度实际包含与 London 匹配的行,以此开始 PREWHERE 处理。
在本例中,所有选定的粒度都匹配,因此 ② 接下来会选择下一个过滤列 date 中与之按位置对齐的对应粒度进行处理:
步骤 2:按 date 过滤
接着,ClickHouse 会 ① 读取选定的
date 列粒度,以评估过滤条件 date > '2024-12-31'。
在这种情况下,3 个粒度中有 2 个包含匹配的行,因此 ② 只会选择它们在下一个过滤列 price 中按位置对齐的粒度继续处理:
步骤 3:按 price 过滤
最后,ClickHouse 会 ① 读取
price 列中选定的两个粒度,以评估最后一个过滤条件 price > 10_000。
两个粒度中只有一个包含匹配的行,因此 ② 只需要加载 SELECT 列 street 中与其按位置对齐的粒度,以便进一步处理:
到最后一步时,加载的列粒度已经缩减到最小范围,也就是仅包含匹配行的那些粒度。这样可以降低内存占用、减少磁盘 I/O,并加快查询执行速度。
PREWHERE 减少的是读取的数据,而不是处理的行数请注意,在使用 PREWHERE 和不使用 PREWHERE 的两个查询版本中,ClickHouse 处理的行数是相同的。不过,应用 PREWHERE 优化后,并不需要为每一行都加载所有列值。
PREWHERE 优化会自动生效
optimize_move_to_prewhere 时 (默认为 true) ,ClickHouse 会自动将过滤条件从 WHERE 移到 PREWHERE,并优先选择那些最能减少读取量的条件。
其原理在于,较小的列扫描起来更快,而在处理较大的列时,大多数粒度往往已经被过滤掉。由于所有列的行数都相同,列的大小主要取决于其数据类型。例如,UInt8 列通常会比 String 列小得多。
自 23.2 版本起,ClickHouse 默认采用这一策略,按未压缩大小升序对 PREWHERE 过滤列进行排序,以执行多阶段处理。
自 23.11 版本起,可选的列统计信息还能进一步优化这一过程:根据实际数据的选择性而不只是列大小,来决定过滤器的处理顺序。
如何衡量 PREWHERE 的影响
optimize_move_to_prewhere setting 的情况下,查询的性能表现。
我们先在禁用 optimize_move_to_prewhere 设置的情况下运行该查询:
optimize_move_to_prewhere 设置的情况下运行该查询。 (请注意,这项设置是可选的,因为它默认已启用) :
关键要点
- PREWHERE 可避免读取后续会被过滤掉的列数据,从而节省 I/O 和内存。
- 启用
optimize_move_to_prewhere(默认开启) 后,它会自动生效。 - 过滤顺序很重要:较小且选择性高的列应放在前面。
- 使用
EXPLAIN和日志来验证是否应用了 PREWHERE,并了解其效果。 - 对宽表以及带有高选择性过滤器的大范围扫描而言,PREWHERE 的效果最为显著。