1. 索引的基本概念
索引(Index) 是数据库中对表的一列或多列值进行排序的数据结构,它能显著提升数据检索效率。类似于书籍的目录,索引可以帮助数据库系统快速定位所需数据,而无需扫描整张表。 例如,若需按员工姓氏查找某人,直接使用索引比逐行扫描全表更高效。从技术角度看,索引是一种有序的数据结构,它存储了指向原始数据的指针(如行号或主键),并基于某个或多个列(称为搜索码)组织数据。搜索码可以是列的全部或部分组合,索引本质上就是这些搜索码及其对应数据位置的集合,用于加速查询。
2. 索引的作用与代价
2.1 索引的主要作用
- 加速数据检索
索引的核心功能是提升查询速度,使数据库系统能快速定位符合条件的记录,避免全表扫描。
- 保证数据唯一性
唯一索引(Unique Index) 可确保表中某列(或列组合)的值不重复,如用户ID、身份证号等关键字段。
- 优化表连接(JOIN)操作
在关联查询时,索引能加快不同表之间的匹配,尤其在实现参照完整性(Referential Integrity) 时至关重要。
- 提升排序与分组效率
当查询包含 ORDER BY 或 GROUP BY 时,索引可减少排序和分组计算的时间。
- 优化查询执行计划
数据库优化器可能利用索引选择更高效的查询路径,从而提升整体性能。
2.2 索引的代价
尽管索引能大幅提升查询效率,但其使用也伴随一定的开销:
- 存储空间占用
索引本身需要额外的磁盘空间存储,尤其是对大表或多列索引而言。
- 写入性能影响
当插入、更新或删除数据时,数据库不仅需要修改表数据,还必须同步调整相关索引,这会增加写操作的开销。
- 维护成本
索引需要随数据变化动态维护,数据量越大,索引维护的耗时越长。
聚簇索引(Clustered Index)对存储空间的要求更高,因为它直接影响数据的物理存储顺序。
- 管理复杂度
过多的索引可能导致数据库优化器选择不当的执行计划,反而降低性能,因此需合理设计索引策略。
3. StarRocks索引
StarRocks 提供了丰富的索引类型,主要分为以下两类:
- StarRocks 自动创建的索引,称为内置索引,包括前缀索引、Ordinal 索引、ZoneMap 索引。
- StarRocks 同时也支持用户手动创建索引,包括 Bitmap 索引和 Bloom filter 索引。
3.1 Ordinal 索引
底层存储数据时,StarRocks 实际上采用列式存储。每一列数据以 Data Page 为单位分块存储,每个 Data Page 大小一般为 64*1024 个字节(data_page_size = 64 * 1024)。每一个列 Data Page 会对应生成一条 Ordinal 索引项,记录 Data Page 的起始行号等信息。这样 Ordinal 索引提供了通过行号来查找列 Data Page 数据页的物理地址。其他索引查找数据时,最终都要通过 Ordinal 索引查找列 Data Page 的位置。
3.2 ZoneMap 索引
ZoneMap 索引存储了每块数据统计信息,统计信息包括 Min 最大值、Max 最小值、HasNull 空值、HasNotNull 不全为空的信息。在查询时,StarRocks 可以根据这些统计信息,快速判断这些数据块是否可以过滤掉,从而减少扫描数据量,提升查询速度。
3.3 前缀索引
数据写入时候自动生成前缀索引。具体来说,写入时数据按照指定的排序键排序,并且每写入 1024 行数据构成一个逻辑数据块(Data Block),在前缀索引表中存储一个索引项,内容为该逻辑数据块中第一行数据的排序列组成的前缀。 当查询的过滤条件命中前缀索引的前缀,则可以快速定位符合条件的数据,减少扫描的数据量,从而查询性能可以得到显著提升。
3.3.1 各类型表对前缀索引的支持
自 3.0 版本起,主键表支持使用 ORDER BY 定义排序键,自 3.3 版本起,明细表、聚合表和更新表支持使用 ORDER BY 定义排序键。
明细表中数据按照排序键 ORDER BY 排序,排序键可以为任意列的排列组合。
聚合表中数据先按照聚合键 AGGREGATE KEY 进行聚合后,再按照排序键 ORDER BY 排序。ORDER BY 和 AGGREGATE KEY 中的列需要保持一致,但是列的顺序不需要保持一致。
更新表中数据先按照唯一键 UNIQUE KEY 进行 REPLACE 后,再按照排序键 ORDER BY 排序。ORDER BY 和 UNIQUE KEY 中的列需要保持一致,但是列的顺序不需要保持一致。
主键表中数据先按照主键 PRIMARY KEY 进行 REPLACE 后,再按照排序键 ORDER BY 排序。
3.3.2 指定排序键的建表语句示例
CREATE TABLE user_access (
uid int,
name varchar(64),
age int,
phone varchar(16),
last_access datetime,
credits double
)
ORDER BY (uid, age, name);
前缀索引:uid(4字节)+age(4字节)+name(28字节),name被截断。设计时应根据查询情况定义排序键。
3.3.3 前缀索引注意事项:
- 前缀字段的数量不超过 3 个,前缀索引项的最大长度为 36 字节,超过部分会被截断。
- 前缀字段中 CHAR、VARCHAR、STRING 类型的列只能出现一次,并且处在末尾位置。
- 如果表中通过 ORDER BY 指定了排序键,就根据排序键构建前缀索引;如果没有通过 ORDER BY 指定排序键,就根据 Key 列构建前缀索引。
3.3.4 合理设计排序键
根据分析业务场景中查询和数据特点,选择合理的排序列和设计排序列的顺序,来组成前缀索引,能够显著提高查询性能。
-
排序列不宜过多, 一般为 3 个,建议不超过 4 个。排序列过多并不有助于提升查询性能,反而会导致数据导入时会增加排序的开销。
-
选择排序列和排序列的顺序,从以下两个方面按优先级展开:
(1)选择经常作为查询过滤条件的列为排序列。 如果存在多个排序列,则按照作为查询条件列的频率排列,最经常作为查询过滤条件的排序列放最前面。这样查询的过滤条件包含前缀索引的前缀,则查询性能可以得到显著提升。
(2)如果多个排序列作为查询过滤条件的频率差不多,则可以衡量各排序列的基数特点。 列的基数较高,则查询时能够过滤较多的数据。如果列的基数过低,比如布尔类型的列,则查询时其对于数据过滤效果不佳。 另外考虑存储压缩因素。如果一个低基数列和一个高基数列谁前谁后对于查询性能影响不大,则将低基数列在高基数列前时,排序后的低基数列的存储压缩率会高很多,因此建议低基数列放前。
建表时定义排序列的要求:
- 排序列的数据类型:主键表的排序列支持数值(包括整型、布尔)、字符串、时间日期类型。明细表、聚合表和更新表的排序列支持数值(包括整型、布尔、Decimal)、字符串、时间日期类型。
- 聚合表和更新表中,排序列必须定义在其他列之前。
自 3.0 版本起,支持修改主键表的排序键,自 3.3 版本起,支持修改明细表、聚合表和更新表的排序键。明细表和主键表中排序键可以为任意列的排序组合,聚合表和更新表中排序键必须包含所有 key 列,但是列的顺序无需与 key 列保持一致。
执行查询后,您可以通过 Query Profile 的 scan 节点中的详细指标查看前缀索引是否生效以及过滤效果,例如 ShortKeyFilterRows 等指标。