练习表 2:Sort key 设计(ORDER BY)
从查询负载出发为每张 NYC 出租车表推导 ORDER BY,每个答案都有即时反馈。
预计耗时: 20–25 分钟 参考资料: MergeTree 引擎,ORDER BY 设计一节
概念
ClickHouse 的 ORDER BY 子句不是装饰。它定义了主索引,一个
稀疏的、块级别的索引,让 ClickHouse 在计算 WHERE 子句时能跳过无关的数据块。它还决定了
part 内部数据的物理排序次序,而这会影响压缩率。
错误的 ORDER BY = 慢查询 + 浪费存储。 以 UUID 开头的 ORDER BY 意味着任何分析查询都无法 跳过数据块(UUID 是随机的;不存在可排序的前缀)。 以日期开头的 ORDER BY 意味着按日期过滤的查询能跳过表中的大部分数据。
ORDER BY 设计的三条规则
规则 1:从查询过滤条件推导,而不是从源 schema 推导。 看你最频繁的那些查询中的 WHERE、
GROUP BY 和 JOIN 列。被过滤最多的
列大概应该出现在 ORDER BY 中(前提是基数不要太高)。
源表的 primary key(如果有的话)通常无关紧要。
规则 2:低基数在前,高基数在后。 ClickHouse 的主索引
每约 8192 行(一个 granule)有一个条目。低基数列(例如
toStartOfMonth(date) = 4 年内约 48 个不同值)把大量行聚集在一起,
索引可以跳过整个 granule。高基数列(例如 trip_id = 5000 万个
不同值)每行都唯一,把它放在最前面意味着索引什么都跳不了。这个次序是默认做法,不是对规则 1 的
推翻:一个被查询按范围过滤、能排除掉大部分行的列,仍然可以领先于一个只会被按等值过滤的
更低基数列。
规则 3:对 ReplacingMergeTree,以唯一行标识符结尾。
去重键是完整的 ORDER BY 元组。如果 ORDER BY 中缺少 trip_id,
两次拥有相同 pickup_at、且没有更多列区分的不同行程就会被当成
重复行。把 trip_id 放在最后,既保证唯一性又不损害索引性能。
练习:查询负载分析
在设计 sort key 之前,先弄清楚这些查询实际过滤的是哪些列。 NYC 出租车实验有 7 个代表性查询;对每一个,挑出能排除最多行的 过滤列。
练习:基数估算
对每个候选 ORDER BY 列,估算它在这份 4 年、5000 万行的
数据集上的基数。下表大部分是参考数据,两个待填单元格是 pickup_at
自己的估算不同值数量和基数等级。四年大约是 1.26 亿
秒(而分钟数只有约 210 万),生产者按
墙上时钟为每次行程打时间戳,而 PICKUP_AT 存储为 DateTime64(3, 'UTC'),先算出 5000 万次行程
能占据其中多少个槽位,再去选区间。
练习:sort key 设计
利用你的查询负载分析和基数估算,为
trips_raw、fact_trips 和 agg_hourly_zone_trips 各提出一个 ORDER BY。提醒:
- 低基数在前 → 能跳过最多数据块。
- 出现在多个查询的
WHERE/GROUP BY中的列 → 把它们包含进来。 - 对 ReplacingMergeTree 表 → 以唯一行标识符结尾。
- 不要包含从不用于过滤的列。
每张表都填完之后,再作答推理与反思问题。
Loading worksheet...
转录到 migration-plan.md
填完这份练习表之后,把你的 ORDER BY 决策抄到
migration-plan.md 的第 4 节,并勾选:
- [ ] Sort key design: completed