在分布式数据库里建全局二级索引,本质上是用空间和写入时间,去换查询时间。这个代价具体有多大,值不值得,取决于你的数据分布、查询模式和索引实现方式。我们直接拆开看维护成本的具体构成和查询收益的真实来源,不绕弯子。
全局二级索引的维护成本到底发生在哪里全局二级索引和主键索引最大的区别在于,索引数据本身不跟主键数据存放在同一个物理分片上。这意味着每一次涉及索引列的写入操作,都会触发一次跨节点的分布式事务。具体来说,INSERT 操作需要在目标分片写入主键数据,同时根据索引列的值计算索引分片,把索引记录写入对应的索引表。UPDATE 操作如果修改了索引列,需要先删除旧索引记录,再插入新索引记录。DELETE 操作则要同时删除主键数据和对应的索引记录。这些操作都需要走两阶段提交或者 Percolator 这类分布式事务协议,网络往返次数至少增加一倍。
以 TiDB 为例,它的全局索引采用异步在线变更的方式,但在日常写入时,索引维护是同步的。一条简单的 INSERT 语句,如果没有全局二级索引,可能只需要一次 Raft 日志复制;加上全局二级索引后,变成了至少两次 Raft 日志复制,分别针对主键分片和索引分片。实测数据显示,在 8 节点集群上,带 3 个全局二级索引的表的写入 TPS,相比不带索引时下降 40% 到 60%。这个比例会随着索引数量线性增长。
存储成本也容易被低估。全局二级索引表本身占用空间,通常包含索引列、主键列以及一些内部元数据。如果索引列较多或者索引列本身长度较大,索引表的存储量很容易达到主键表的 50% 到 80%。更隐蔽的成本在于 MVCC 多版本带来的空间放大。每次更新索引列,旧版本不会立即清理,导致索引表膨胀速度比主键表更快。实际运维中,带全局二级索引的表,其垃圾回收和 Compaction 压力明显更大。
查询收益的真实边界全局二级索引的查询收益,主要体现在两类场景。第一类是点查,根据索引列精确匹配一行或多行数据。没有全局索引时,这类查询需要在所有分片上执行全表扫描,然后聚合结果,延迟和资源消耗随分片数线性增长。有了全局索引,查询可以直接定位到索引分片,回表拿到完整数据,延迟从秒级降到毫秒级。第二类是范围查询,比如按时间范围、按用户ID区间检索。全局索引保证了索引数据在索引列上有序分布,范围扫描可以顺序读取,不需要跨分片排序合并。
但收益不是无条件的。如果查询最终需要回表取大量数据,而回表操作本身又是随机读,那么索引带来的收益会大打折扣。比如一条 SQL 通过索引过滤出 10 万行,然后回表 10 万次,每次回表都可能跨节点,总延迟反而可能比全表扫描更高。优化器在这类场景下的行数估算一旦出错,就会选错执行计划。实际调优中,我们经常通过强制走全表扫描来对比性能,发现当结果集超过总数据量的 5% 到 10% 时,全局索引的优势开始消失。
另一个容易被忽略的收益是减少分布式 SQL 的中间结果集大小。不带索引时,各分片需要把全量数据发给聚合节点做排序和过滤,网络传输量和内存占用都很高。全局索引让过滤和排序在索引侧完成,聚合节点只需要处理最终结果集,这对大并发场景下的集群稳定性有显著帮助。
索引实现方式对成本和收益的影响不同分布式数据库的全局二级索引实现差异很大,直接决定了维护成本和查询收益的平衡点。以 Spanner 和 CockroachDB 为代表的系统,采用索引表和主键表物理分离的设计,索引数据按索引列分片,索引表中存储索引列和主键列的映射。写入时需要原子更新两张表,依赖 TrueTime 或 HLC 混合逻辑时钟实现外部一致性。这类系统的写入成本较高,但查询时索引扫描效率高,适合读多写少的场景。
以 YugabyteDB 为代表的系统,提供了覆盖索引的选项,允许将常用列直接包含在索引中,避免回表。这种设计进一步提升了查询收益,但写入成本也相应增加,因为索引表变大了。实际使用中,如果能把高频查询的列全部包含在索引里,查询性能可以做到与主键查询持平,代价是写入放大更严重。
还有一些系统如 OceanBase,支持局部索引和全局索引的混合使用。局部索引的索引数据和主键数据在同一个分片内,维护成本低,但查询时需要在所有分片上执行索引扫描,适合查询条件中总是包含分片键的场景。全局索引则相反。实际建表时,需要根据查询模式严格区分:如果查询条件总是带分片键,局部索引足够;如果查询条件不带分片键,才需要考虑全局索引。
索引选择性的决定性作用选择性是指索引列不同值的数量占总行数的比例。选择性越高,索引的查询收益越大。如果一个列只有男女两个值,建全局索引几乎没有任何正面作用,因为索引过滤不掉多少数据,反而每次写入都要维护索引。反过来,用户ID、订单号这类高基数列,选择性接近 1,索引收益最大。
但高选择性不代表一定要建全局索引。如果这个高基数列本身就是分片键,那么主键索引已经天然具备了全局索引的能力,不需要额外建。真正需要全局索引的场景,是那些高选择性但又不是分片键的列。比如电商系统按买家ID分片,但经常需要按卖家ID查询订单。这时在卖家ID上建全局二级索引,收益远大于成本。
写入热点与索引倾斜全局二级索引本身也是分片的,索引分片键就是索引列。如果索引列的值分布不均匀,索引表就会出现热点分片。比如按城市建全局索引,北京上海的数据量远大于其他城市,导致对应索引分片的写入压力集中。这种热点问题比主键表的热点更难处理,因为主键表可以通过选择合适的分片键来打散数据,而索引表的分片键由索引列决定,没有调整空间。
解决索引热点的方法有限。一种是在索引列前面加一个额外的哈希列做分片,但这样会丧失索引列上的范围扫描能力。另一种是使用分区索引,按时间等维度将索引拆成多个物理分区,但增加了查询时的分区裁剪逻辑。实际中,如果索引列存在严重的数据倾斜,需要重新评估建索引的必要性,或者考虑用其他查询方案替代。
在线变更索引的实际代价在生产环境给大表添加全局二级索引,是一个高风险操作。以 TiDB 的在线 DDL 为例,添加索引的过程分为多个阶段:先创建索引元数据,然后逐批回填索引数据,最后切换索引状态为可用。回填过程中,系统需要扫描全表数据,生成索引记录写入索引表。这个操作本身会消耗大量 IO 和 CPU,同时与正常业务写入产生竞争。回填期间,新的写入操作需要同时维护索引,写入放大效应叠加,容易导致集群负载飙升。
实际经验是,在业务低峰期执行索引添加操作,并且严格控制回填的并发度和速度。即使如此,一张 1 亿行的表添加全局二级索引,回填时间通常在数小时级别。期间如果发生节点故障或网络分区,回填任务可能失败回滚,需要重试。索引删除操作相对简单,但删除后索引占用的空间不会立即释放,需要等待 GC 周期,期间存储成本依然存在。
查询改写与索引利用有了全局二级索引,不代表查询就一定能用上。优化器需要根据统计信息判断是否走索引。如果统计信息过期,优化器可能选择全表扫描。实际运维中,定期更新统计信息是保证索引收益的前提。此外,SQL 写法也直接影响索引利用。比如对索引列做函数操作、隐式类型转换、前缀模糊匹配等,都会导致索引失效。
一个常见的优化手段是使用覆盖索引改写查询。假设有一张订单表,经常执行 SELECT order_id, amount, status FROM orders WHERE seller_id = ? AND create_time > ? 这样的查询。如果在 seller_id 和 create_time 上建联合索引,并且将 order_id, amount, status 包含在索引中,查询就不需要回表,性能可以提升一个数量级。代价是索引表体积增大,写入开销增加。这需要根据具体业务读写比例做权衡。
成本与收益的量化评估方法在决定是否建全局二级索引之前,建议做一次量化的成本收益评估。收益侧,统计目标查询的当前平均延迟和 P99 延迟,以及单位时间内的执行次数。成本侧,估算索引带来的写入延迟增加比例和存储空间增长量。具体方法是在测试环境建一个同结构同数据量的表,分别测试带索引和不带索引的写入 TPS,以及目标查询的延迟变化。
如果目标查询是核心业务链路,比如用户登录后展示订单列表,延迟从 500 毫秒降到 10 毫秒,即使写入 TPS 下降 30%,整体收益也是正的。如果目标查询是后台报表,每天只跑几次,延迟从 10 秒降到 1 秒,但写入 TPS 下降 40%,这个代价就不划算。评估时一定要结合业务优先级和调用频率,不要只看绝对性能数字。
另外,存储成本的评估要考虑数据增长趋势。如果业务数据量以每月 20% 的速度增长,索引表的存储开销也会同步增长,而且 Compaction 开销会非线性增加。需要把未来 6 到 12 个月的存储和计算资源预算纳入考量。
替代方案与混合策略全局二级索引不是解决跨分片查询的唯一手段。对于延迟不敏感的查询,可以考虑使用异步物化视图,通过定时任务预计算查询结果,查询时直接读物化视图。对于需要实时但查询模式固定的场景,可以在应用层做双写,同时维护一个按另一种维度分片的副本表。对于搜索场景,可以把数据同步到 Elasticsearch 这类搜索引擎中,由搜索引擎承担多维查询的职责。
实际架构中,经常采用混合策略。核心在线交易链路使用全局二级索引保证低延迟;离线报表和分析查询走列存副本或数据仓库;全文搜索走搜索引擎;监控和告警查询走时序数据库。把查询需求分散到不同的专用系统中,可以避免在分布式数据库上建过多索引,导致写入性能恶化。
全局二级索引是分布式数据库提供的一把利器,但它不是免费的午餐。维护成本真实存在,查询收益有条件限制。用好的关键,在于对业务查询模式的精确理解,对数据分布特征的准确把握,以及对成本收益的持续量化跟踪。建索引之前先问自己三个问题:这个查询能容忍多大延迟?写入性能下降多少可以接受?数据分布是否均匀?三个问题都有明确答案之后,再做决定。
