表聚簇
一个经过深思熟虑的排序键是 StarRocks 中最高效的物理设计技巧。 本指南解释了排序键的底层工作原理、它解锁的系统性优势,以及为您的工作负载选择有效键的具体方法。
示例
假设您运行一个遥测系统,该系统每天接收数十亿行数据,每行数据都标有 device_id
和 ts
(时间戳)。 在事实表上定义 ORDER BY (device_id, ts)
确保
- 对
device_id
的点查询以毫秒为单位返回。 - 仪表板过滤每个设备的最近时间窗口,从而剪除大部分数据。
- 诸如
GROUP BY device_id
之类的聚合受益于流式聚合。 - 由于每个设备附近的多个时间戳运行,压缩得到改善。
这个简单的两列排序键 ORDER BY (device_id, ts)
可减少 I/O、节省 CPU 并在数十亿行数据中实现更稳定的查询性能。
CREATE TABLE telemetry (
device_id VARCHAR,
ts DATETIME,
value DOUBLE
)
ENGINE=OLAP
PRIMARY KEY(device_id, ts)
PARTITION BY RANGE (date_trunc('day', ts))
DISTRIBUTED BY HASH(device_id) BUCKETS 16
ORDER BY (device_id, ts);
深入了解优势
-
大量 I/O 消除 - 段和页面剪枝
工作原理
每个段和 64 KB 页面存储所有列的最小值/最大值。 如果谓词超出该范围,StarRocks 会跳过整个块,而不会触及磁盘。
示例
SELECT count(*)
FROM events
WHERE tenant_id = 42
AND ts BETWEEN '2025-05-01' AND '2025-05-07';使用
ORDER BY (tenant_id, ts)
只考虑第一个键等于 42 的段,并且在这些段中只考虑 ts 窗口与这 7 天重叠的页面。 一个 1000 亿行的表可能扫描不到 10 亿行,从而将分钟变成秒。
-
毫秒级点 查找 - 稀疏前缀索引
工作原理
稀疏前缀索引存储每个约 1K 个排序键值。 二进制搜索落在正确的页面上,然后单个磁盘读取(通常已缓存)返回该行。
示例
SELECT *
FROM orders
WHERE order_id = 982347234;使用
ORDER BY (order_id)
,探测需要在 500 亿行的表中进行大约 50 个键比较 - 即使在冷数据缓存上,延迟也低于 10 毫秒。
-
更快的排序聚合
工作原理
当排序键与 GROUP BY 子句对齐时,StarRocks 在扫描时执行流式聚合 - 无需排序或哈希表。
此排序聚合计划按排序键顺序扫描行并动态发出组,从而利用 CPU 缓存局部性并跳过中间物化。
示例
SELECT device_id, COUNT(*)
FROM telemetry
WHERE ts BETWEEN '2025-01-01' AND '2025-01-31'
GROUP BY device_id;如果表是
ORDER BY (device_id, ts)
,则引擎会在行流入时对行进行分组 - 无需构建哈希表或重新排序。 对于像 device_id 这样高基数的键,这可以显著减少 CPU 和内存使用。对于大组基数,具有排序输入的流式聚合通常将吞吐量提高 2-3 倍,超过哈希聚合。
-
更高的 压缩 & 更热的 缓存
工作原理
排序的数据显示小增量或长运行,从而加速字典、RLE 和参考帧编码。 紧凑的页面按顺序流经 CPU 缓存。
示例
按 (device_id, ts) 排序的遥测表比未排序的相同数据实现了 1.8 倍更好的压缩 (LZ4) 和 25% 更低的 CPU/扫描。
-
对于主要键表,更快的合并写入
工作原理
在 upsert 期间,引擎仅重写其键范围与批次重叠的切片,而不是整个平板。
示例
UPDATE balances
SET amount = amount + 100
WHERE account_id = 123;使用
ORDER BY (account_id)
,与未排序时相比,只有不到 1% 的段数据被触及 - 在更新密集型工作负载上产生 2-4 倍更高的写入吞吐量。
排序键如何工作
排序键的影响从写入行的那一刻开始,并持续到每次读取时优化。 本节将介绍该生命周期 - 写入路径 ➜ 存储层次结构 ➜ 段内部结构 ➜ 读取路径 - 以显示每一层如何复合排序值的。
- 写入路径
- 提取:行落在 MemTable 中,按声明的排序键排序,然后刷新为包含一个或多个有序段的新 Rowset。
- 压缩:后台累积/基本作业将许多小 Rowset 合并为更大的 Rowset,回收删除并降低段计数而不重新排序,因为每个源 Rowset 已经共享相同的顺序。
- 复制:每个平板(拥有 Rowset 的分片)都会同步复制到对等后端节点,从而保证排序顺序在副本之间保持一致。
- 存储层次结构
对象 | 它是什么 | 为什么它对排序键很重要 |
---|---|---|
分区 | 表的粗粒度逻辑切片(例如,日期或 tenant_id)。 | 启用计划时分区剪枝并隔离生命周期操作(TTL、批量加载)。 |
平板 | 分区内的哈希/随机桶,在后端节点之间独立复制。 | 行按排序键物理排序的单元;所有分区内剪枝都从这里开始。 |
MemTable | 内存写入缓冲区(约 96 MB),在刷新到磁盘之前按声明的键排序。 | 保证每个磁盘段都已经排序 - 以后不需要外部排序。 |
Rowset | 刷新、流式加载或压缩周期产生的一个或多个段的不可变捆绑包。 | 仅附加设计允许 StarRocks 并发提取,同时读取器保持无锁。 |
段 | Rowset 内的自包含列文件(约 512 MB),携带数据页面加上剪枝索引。 | 段级区域映射和前缀索引依赖于 MemTable 阶段建立的顺序。 |
- 段文件内部
每个段都是自描述的。 从上到下,您会发现
- 列数据页面 64 KB 块编码(字典、RLE、Delta)和压缩(LZ4 默认)。
- 序号索引 将行序号映射到页面偏移量,因此引擎可以直接跳转到页面 n。
- 区域映射索引 每个页面和整个段的最小值、最大值和 has_null - 剪枝的第一道防线。
- 短键(前缀)索引 稀疏二进制搜索表,包含每大约 1K 行的排序键的前 36 个字节 - 实现毫秒级点/范围查找。
- 页脚和魔数 每个索引的偏移量和用于完整性的校验和;允许 StarRocks 仅对尾部进行内存映射以发现其余部分。
由于页面已经按键排序,因此这些索引非常小,但效果却非常有效。
-
读取路径
- 分区剪枝(计划时) 如果 WHERE 子句约束分区键(例如
dt BETWEEN '2025-05-01' AND '2025-05-07'
),则优化器仅打开匹配的分区目录。 - 平板剪枝(计划时) 当相等性过滤器包含哈希分布列时,StarRocks 会计算目标平板 ID 并仅计划这些平板。
- 前缀索引查找 前导排序列上的稀疏短键索引锁定确切的段或页面。
- 区域映射剪枝 每个段和 64 KB 页面的 min/max 元数据丢弃错过谓词窗口的块。
- 向量化扫描和后期物化 幸存的列页面按顺序流经 CPU 缓存;仅物化引用的行和列,从而保持内存紧张。
由于数据在每次刷新时都按键顺序提交,因此每个读取时剪枝层都建立在它之前的层上,从而在数十亿行的表上实现亚秒级扫描。
- 分区剪枝(计划时) 如果 WHERE 子句约束分区键(例如
如何选择有效的排序键
-
从工作负载智能开始
首先分析前 N 个查询模式
- 相等谓词(
=
/IN
)。 几乎总是按相等性过滤的列是理想的前导候选列。 - 范围谓词。 时间戳和数字范围通常在排序键中跟随相等列。
- 聚合键。 如果范围列也出现在
GROUP BY
子句中,则将其放在键中较早的位置(在选择性过滤器之后)可以启用排序聚合。 - 连接/分组依据键。 如果连接或分组键很常见,请考虑将其尽早放置
衡量列基数:高基数列(数百万个不同的值)剪枝效果最佳。
- 相等谓词(
-
启发法和经验法则
- 排序规则:(高选择性相等列)→(主范围列)→(集群辅助列)。
- 基数排序:将低基数列放在高基数列之前可以增强数据压缩。
- 宽度:保持 3-5 列。 非常宽的键会减慢提取速度并使 36 字节前缀索引限制溢出。
- 字符串列:长的前导字符串列可能占用前缀索引中 36 字节限制的大部分或全部,从而阻止排序键中的后续列被有效索引。
这降低了前缀索引的剪枝能力并降低了点查询性能。
-
与其他设计技巧协调
- 分区:选择比前导排序列更粗的分区键(例如,
PARTITION BY date
,ORDER BY (tenant_id, ts)
)。 这样,分区剪枝首先删除整个日期范围,然后排序剪枝在内部清理。 - 分桶:对分桶和聚簇使用相同的列具有不同的目的。 分桶确保数据在集群中均匀分布,而排序则实现有效的 I/O 消除。
- 表类型:主要键表默认使用主键作为排序键,但它们也可以指定其他列来优化物理顺序并增强剪枝。 聚合表和重复表应遵循上面讨论的分析谓词驱动的排序键策略。
- 分区:选择比前导排序列更粗的分区键(例如,
- 参考模板
场景 | 分区 | 排序键 | 理由 |
---|---|---|---|
B2C 订单 | date_trunc('day', order_ts) | (user_id, order_ts) | 大多数查询首先按用户过滤,然后按最近的时间范围过滤。 |
物联网遥测 | date_trunc('day', ts) | (device_id, ts) | 设备范围的时间序列读取占主导地位。 |
SaaS 多租户 | tenant_id | (dt, event_id) | 通过分区进行租户隔离;按天对排序集群进行仪表板处理。 |
维度查找 | 无 | (dim_id) | 小表,纯点查找 - 单列就足够了。 |
结论
一个精心设计的排序键用小的、可预测的提取开销换取了扫描延迟、存储效率和 CPU 利用率的显着提高。 通过将您的选择建立在工作负载现实的基础上、尊重基数并使用 EXPLAIN
进行验证,即使数据和用户数量增长 10 倍及以上,您也可以保持 StarRocks 的正常运行。