什么是 Doris 的前缀索引(Short Key Index)?如何合理排列建表语句中的 Key 列以最大化前缀索引的过滤效果?
在 Apache Doris 中,前缀索引(Short Key Index) 是其核心的查询加速机制之一。理解它的原理并合理设计建表语句,能够极大地提升查询性能。
以下是关于 Doris 前缀索引的详细解释以及如何优化 Key 列排列的指南:
一、 什么是 Doris 的前缀索引?
1. 基本概念
Doris 的数据在底层存储时,是严格按照建表语句中指定的 Key 列(Aggregate Key、Unique Key 或 Duplicate Key)进行字典序排序存储的。
前缀索引是一种稀疏索引。Doris 会在排好序的数据上,每隔一定的数据行(默认 1024 行构成一个 Data Block)提取该 Block 第一行的前几个列的值作为索引项。查询时,Doris 会先通过二分查找匹配前缀索引,快速定位到数据可能存在的 Block,从而跳过大量无关数据(Block 级别裁剪)。
2. 前缀索引的生成规则(核心!)
Doris 并不会把所有的 Key 列都放入前缀索引,而是有严格的截断规则:
- 36 字节限制:前缀索引最多包含前 36 个字节的数据。
- VARCHAR 截断规则:当遇到
VARCHAR或STRING类型的列时,前缀索引会包含该列,但会立即在该列截断。即排在VARCHAR之后的列,无论是否达到 36 字节,都不会进入前缀索引。
举个例子:
假设建表 Key 列如下:
user_id(INT, 4 字节)age(SMALLINT, 2 字节)message(VARCHAR(50))join_date(DATE, 3 字节)
前缀索引包含的内容是:user_id + age + message(前30个字节)。
原因:4 + 2 + 30 = 36 字节。遇到 message 是 VARCHAR,索引在此截断。后面的 join_date 即使是 Key 列,也无法享受前缀索引加速。
二、 如何合理排列 Key 列以最大化过滤效果?
为了让查询尽可能多地命中前缀索引,你需要遵循以下几个核心原则:
1. 遵循“最左前缀匹配”原则
前缀索引的查询类似于 MySQL 的联合索引,必须从左到右连续匹配。
- 原则:将查询中最频繁作为过滤条件(WHERE 子句)的列放在最前面。
- 场景:如果你的表大部分查询都是基于
date和shop_id,那么这两个列必须放在前面。如果查询条件跳过了第一列(例如WHERE shop_id = 1,但表的第一列是date),则无法使用前缀索引。
2. 规避 VARCHAR 截断陷阱
由于 VARCHAR 会截断前缀索引,错误的放置会导致后续列的索引失效。
- 原则:将定长类型(INT, BIGINT, DATE, DATETIME 等)放在前面,将 VARCHAR 放到 Key 列的最后面。
- 反面教材:
CREATE TABLE ... DUPLICATE KEY (user_name VARCHAR, age INT, date DATE)。这里前缀索引只有user_name,后面的age和date完全没用上。 - 正面教材:
CREATE TABLE ... DUPLICATE KEY (date DATE, age INT, user_name VARCHAR)。这样三个列都能进入前缀索引(只要总大小不超过 36 字节)。
3. 区分度(基数)与业务逻辑的平衡
- 原则:在满足前两点的基础上,将区分度高(Cardinality 大)且经常用于等值查询的列尽量靠前。
- 特例(时间列):在日志分析、事实表场景中,
DATE或DATETIME列虽然区分度不如user_id高,但几乎所有的查询都会带上时间范围(过滤掉 90% 的历史数据)。因此,通常建议把时间列放在第一列,接着放区分度高的维度列(如user_id,device_id)。
4. 充分利用 36 字节
- 原则:不要在前面放过大的定长字段(比如 DECIMAL 或长 CHAR),尽量让 36 字节能包含更多的维度列。
- 常见类型字节数:
TINYINT(1),SMALLINT(2),INT(4),BIGINT(8),DATE(3),DATETIME(8)。 - 如果你把
INT,INT,DATE放前面,总共才 11 字节,前缀索引可以轻松覆盖这三个列。
三、 综合实战案例
假设我们要建一张用户行为日志表,字段包括:log_time (DATETIME), user_id (BIGINT), event_type (INT), device_model (VARCHAR), ip (VARCHAR)。
❌ 糟糕的排列(反例):
CREATE TABLE log_table (
device_model VARCHAR(50),
user_id BIGINT,
log_time DATETIME,
event_type INT,
ip VARCHAR(20)
)
DUPLICATE KEY(device_model, user_id, log_time, event_type)
-- 结果:前缀索引只包含 device_model。
-- 灾难:查询 WHERE log_time > '2023-01-01' 时,必须全表扫描。
✅ 优秀的排列(正例):
CREATE TABLE log_table (
log_time DATETIME, -- 8 字节,几乎所有查询必带
event_type INT, -- 4 字节,经常用于过滤事件
user_id BIGINT, -- 8 字节,精确查找
device_model VARCHAR(50), -- 截断点,放最后
ip VARCHAR(20)
)
DUPLICATE KEY(log_time, event_type, user_id, device_model)
-- 结果:前缀索引包含了 log_time(8) + event_type(4) + user_id(8) = 20 字节。
-- 剩余 16 字节分配给 device_model 的前缀。
-- 优势:基于时间范围、事件类型、用户的查询,都能完美命中前缀索引,实现极速响应。
四、 补充优化手段(当一个前缀索引不够用时)
在实际业务中,查询条件可能是多变的。如果查询条件无法命中当前的前缀索引,可以通过以下方式弥补:
- Rollup / 倒排索引:如果除了按时间+用户查询,还需要频繁按
device_model查询,且不带时间条件,可以创建一个以device_model为第一列的 Rollup 表,或者在device_model上建立倒排索引(Inverted Index)。 - Bloom Filter 索引:对于高基数(高区分度)且经常进行等值查询(
=或IN)的列(如user_id),如果它排在前缀索引后面,可以为其单独添加 Bloom Filter 索引以加速。