建一张30万行、按月分区、按region+uid排序的表,用3种不同的WHERE条件对比read_rows,看ORDER BY前缀、非索引列、分区裁剪各自的效果。
按月分区,按region+uid排序,插入横跨3个月的30万行随机数据。
建表CREATE TABLE demo.orders (d Date, region String, uid UInt64, amount Float64) ENGINE=MergeTree PARTITION BY toYYYYMM(d) ORDER BY (region, uid) 插入30万行,日期随机分布在2026-01-15 / 2026-02-20 / 2026-03-10 SELECT partition, count() parts, sum(rows) FROM system.parts WHERE table='orders' → 202601 1 part 99432行 → 202602 1 part 100548行 → 202603 1 part 100020行
这张表同时用到了3个概念:PARTITION BY 决定数据物理上分成几组文件(这里是3个月各一组),ORDER BY 决定每个分区内部数据按什么顺序排列并建立稀疏索引。它们各自解决不同的问题,测一遍才知道差在哪。
同一张表,一条查询过滤 ORDER BY 的第一列(region),一条过滤不在 ORDER BY 里的列(amount),对比 system.query_log 里的 read_rows。
结果SELECT count() FROM demo.orders WHERE region='bj' → 匹配75226行,read_rows只有24577 SELECT count() FROM demo.orders WHERE amount > 999 → 匹配313行,read_rows是300000(全表扫描)
过滤 amount 这种不在 ORDER BY 里的列,ClickHouse 没有任何索引信息可用,把30万行全部扫了一遍才能找出符合条件的313行。过滤 region(ORDER BY 的第一列)则完全不同:因为数据在每个分区内部是按 region 物理排序的,同一个 region 的行必然连续挨在一起,稀疏索引可以直接定位到这段连续区间的起止位置,读取量(24577)甚至比最终匹配的行数(75226)还要少——因为大部分落在区间内部的行,ClickHouse 靠索引直接确认"肯定符合条件",不需要真的去读它们,只有区间边界那一小部分颗粒需要读取来确认精确边界。
再测一条过滤日期的查询,日期只用于 PARTITION BY,不在 ORDER BY 里。
结果SELECT count() FROM demo.orders WHERE d = '2026-02-20' → read_rows正好是100548,等于2月这一个分区的全部行数
这条查询只读了 100548 行,一行不多一行不少,正好是 202602 这个分区的总行数——ClickHouse 靠 PARTITION BY toYYYYMM(d) 直接跳过了 1 月和 3 月这两个分区(总共约20万行),但因为 d 不在 ORDER BY (region, uid) 里,进了 2 月这个分区之后就没有更细的索引可用了,只能把这个分区内部100548行全部扫一遍。
每一步都是本文上面查询system.query_log得到的原始read_rows。
system.parts:核对每个分区的part数与行数system.query_log:对比不同WHERE条件下的read_rows