news 2026/9/10 2:06:10

TiDB 不可见索引(Invisible Index)设计与实现解析:基于 2020-03-12-invisible-index 设计文档的深度指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TiDB 不可见索引(Invisible Index)设计与实现解析:基于 2020-03-12-invisible-index 设计文档的深度指南

TiDB 不可见索引(Invisible Index)设计与实现解析:基于 2020-03-12-invisible-index 设计文档的深度指南

【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidb

TiDB 的不可见索引(Invisible Index)允许你创建或调整一个对优化器"隐藏"但仍持续维护的索引,从而在不执行破坏性 DROP 操作的前提下,安全评估删除索引对查询性能的影响。本指南以仓库中的设计文档为主体骨架,结合当前 TiDB 源码实现,完整讲解不可见索引的语法、行为约束、底层执行链路(从ALTER INDEX到 schema 变更 Job)以及优化器侧的过滤逻辑,帮助你掌握这一"零风险索引下线演练"能力的原理与正确用法。

一、不可见索引要解决什么问题

索引对数据库读写性能的影响举足轻重:是否存在索引、优化器是否选对了索引,很大程度上决定了数据库读写的性能表现。在某些场景下,我们希望评估"删除某个索引"对数据库读写性能的影响。过去 TiDB 的做法是借助 Index Hint 让优化器忽略指定索引,虽然能达到目的,但要求逐条修改所有 SQL 语句,在大规模业务中并不可行(见设计文档的 Background 一节)。

不可见索引正是为此而生:它为索引增加一个"可见/不可见"的选项,不可见的索引不会被优化器使用,但依然会在 DML 操作中持续维护。对查询而言,不可见索引的效果等价于通过 Index Hint 忽略该索引,却无需改动任何 SQL。更关键的是,删除再重建一个大表的索引代价高昂,而将索引在 VISIBLE 与 INVISIBLE 之间切换是快速的原位(in-place)操作——这让它成为"以安全方式下线索引"的理想工具。

二、核心语法与行为契约

设计文档给出了完整的语法支持,当前 TiDB 已全部实现。

2.1 建索引时指定可见性

INVISIBLE/VISIBLE作为索引选项(index_option)的一部分,可在创建索引时设置,也可在建表时直接设置:

CREATE [...] INDEX index_name [index_type] ON tbl_name (key_part,...) [index_option] index_option: {VISIBLE | INVISIBLE}

具体到当前仓库,建索引路径位于 pkg/ddl/index.go,其中对indexOption.Visibility == ast.IndexVisibilityInvisible的分支处理(见pkg/ddl/index.go#L440附近)将不可见标记写入索引元数据。

2.2 通过 ALTER INDEX 切换可见性

通过如下 DDL 可以随时将索引切换为不可见或恢复可见:

ALTER TABLE table_name ALTER INDEX index_name { INVISIBLE | VISIBLE };

这是不可见索引最有价值的操作——它让"下线演练"可以随时开始、随时回滚,而无需重建索引。

2.3 三个必须遵守的行为约束

设计文档明确了三条配套行为,均已落地:

  1. 元数据可查:不可见信息必须体现在INFORMATION_SCHEMA.STATISTICS表和SHOW INDEX输出中(新增IS_VISIBLE/VISIBLE列);
  2. Hint 冲突报错:当不可见索引被用于索引 Hint 时,需要报错;
  3. 主键不可隐藏:主键索引不能被设置为不可见。

此外还有一条容易被忽略的隐含规则(来自设计文档):没有显式主键的表,如果存在建于 NOT NULL 列上的 UNIQUE 索引,则第一个这样的唯一索引在行约束上等价于隐式主键,同样不能被设置为不可见。

三、从设计到实现:源码级解读

设计文档的 Implementation 一节列出了六项实现任务,下面逐项对照当前仓库源码验证其落地情况。

3.1 元数据存储:IS_VISIBLE / VISIBLE 列

INFORMATION_SCHEMA.STATISTICS表新增了IS_VISIBLE列,其列定义(TypeVarchar,长度 3)位于 pkg/infoschema/tables.go;SHOW INDEX输出同样提供可见性信息(pkg/infoschema/tables.go)。同时SHOW CREATE TABLE也会展示不可见索引信息,以便完整还原表结构。

3.2 ALTER INDEX 的完整执行链路

ALTER TABLE ... ALTER INDEX ... INVISIBLE/VISIBLE的实际执行路径如下:

  • 入口:pkg/ddl/executor.go 的AlterIndexVisibilityast.IndexVisibilityInvisible映射为invisible=true,先做前置校验,再构造一个类型为model.ActionAlterIndexVisibility的 DDL Job 提交给 DDL 框架;
  • 校验:pkg/ddl/index.go 的validateAlterIndexVisibility检查索引是否存在且处于StatePublic状态,若目标状态与当前状态一致则直接跳过(幂等),并禁止对列式索引(Columnar Index)设置 INVISIBLE;
  • Job 处理onAlterIndexVisibility(pkg/ddl/index.go)与setIndexVisibility(pkg/ddl/index.go)在 schema 变更过程中改写索引的可见性标记;该 Job 类型在 pkg/ddl/job_worker.go 中被分发处理。

从源码结构看,这个操作走的是标准的在线 DDL(online DDL)异步 Job 机制,因此不会阻塞读写,这与设计文档"快速的原位操作"的定位一致。

3.3 主键与隐式主键保护

当尝试把聚簇索引(clustered index)主键设置为不可见时,TiDB 直接拒绝:

  • pkg/ddl/create_table.go:在建表/建主键路径上,若constr.Option.Visibility == ast.IndexVisibilityInvisible且表具有聚簇索引,则返回dbterror.ErrPKIndexCantBeInvisible
  • 该错误对应 MySQL 兼容错误码ERROR 3522 (HY000): A primary key index cannot be invisible,错误文案定义在 pkg/errno/errname.go。

ALTER INDEX路径则通过validateAlterIndexVisibility等逻辑配合元数据校验,从两个入口共同保障主键/隐式主键索引不会被隐藏。

3.4 优化器如何"看不见"不可见索引

设计文档提出:不可见索引不能供优化器使用(除非开关打开),但对查询而言其效果等价于 Index Hint 忽略。当前实现中,优化器在选择访问路径时对不可见索引做了显式过滤:

  • pkg/planner/core/planbuilder.go:读取会话变量OptimizerUseInvisibleIndexes,遍历表索引时若!optimizerUseInvisibleIndexes && index.Invisible则直接continue("Filter out invisible index, because they are not visible for optimizer");
  • pkg/planner/core/point_get_plan.go 与#L617:Point Get / Batch Point Get 计划同样要求索引可见且StatePublic,不可见索引不会被用于点查加速。

值得注意的是,与 MySQL 不同,TiDB 在 SQL Hint 中显式使用不可见索引时会抛出错误(设计文档 Compatibility 一节描述为 "Unresolved name" 错误),而不是静默允许。这一点在使用 Hint 时需要格外留意。

四、系统开关:tidb_opt_use_invisible_indexes

设计文档提出在optimizer_switch中新增use_invisible_indexes开关(文档中写作use_invite_indexes,为笔误),用于控制 INVISIBLE 选项是否生效。TiDB 实际实现为独立系统变量:

  • 变量定义于 pkg/sessionctx/vardef/tidb_vars.go:TiDBOptUseInvisibleIndexes = "tidb_opt_use_invisible_indexes"
  • 注册于 pkg/sessionctx/variable/setvar_affect.go。

开关默认关闭,即优化器不使用不可见索引。开启后优化器会重新考虑这些索引:

-- 关闭(默认):不可见索引不被优化器使用 set session tidb_opt_use_invisible_indexes=off; -- 开启:优化器可以继续使用不可见索引 set session tidb_opt_use_invisible_indexes=on;

这个开关给了运维一个额外的"逃生舱":即使索引已设置为 INVISIBLE,仍可在会话或全局层面临时恢复其参与优化,用于应急或对比验证。

五、为什么不用 WriteOnly 状态实现:设计取舍

设计文档 Rationale 一节讨论了另一种备选方案:用 DDL 状态机中的WriteOnly状态来表示"不可见"。该方案被否决,原因有三:

  1. 需要改动 schema 变更的核心逻辑,实现复杂度高;
  2. 无法区分"正处于 WriteOnly 迁移中的索引"与"被标记为不可见的索引",语义互相污染;
  3. 处理开关(是否使用不可见索引)非常麻烦。

当前采用"索引选项 + 优化器过滤"的方案,将可见性作为独立的元数据维度,与 schema 状态机解耦,既保持了 DDL 框架的简单性,也让可见性切换成为可随时执行、可随时回滚的轻量操作。

六、兼容性与迁移

这是一个全新特性,与旧版本 TiDB 完全兼容,不影响任何数据迁移。语法与功能基本兼容 MySQL,唯一明确的差异点是:

当在 SQL Hint 中使用不可见索引且use_invisible_indexes = false时,MySQL 允许使用该不可见索引;而 TiDB不允许,会抛出Unresolved name错误。

也就是说,在把 MySQL 业务迁移到 TiDB 时,如果业务 Hint 中引用了被标记为不可见的索引,需要提前清理或调整相关 Hint。

七、测试与验证

设计文档的 Testing Plan 包括单元测试与借鉴 MySQL 不可见索引相关测试用例。当前仓库中的测试可以帮你快速验证上述所有行为:

  • pkg/planner/core/casetest/index/index_test.go:TestInvisibleIndexCREATE TABLE t1 ( a INT, KEY( a ) INVISIBLE )建表,插入 10 行后先验证EXPLAINTableFullScan(索引不可见、不被使用);再set session tidb_opt_use_invisible_indexes=on验证执行计划切换为IndexFullScan——这一组用例直观演示了开关对执行计划的影响;
  • pkg/ddl/db_change_test.go:TestAlterIndexVisibility覆盖ALTER INDEX ... VISIBLE/INVISIBLE的 DDL 行为;
  • tests/integrationtest/r/ddl/db_integration.result 与 tests/integrationtest/r/ddl/primary_key_handle.result:记录了集成测试中主键不可见报错等场景的预期输出。

八、实战建议:零风险的索引下线流程

结合设计文档的初衷与当前实现,推荐如下"不可见索引演练"流程:

  1. 切换为不可见ALTER TABLE t ALTER INDEX idx_a INVISIBLE;——索引停止被优化器使用,但 DML 仍持续维护,索引数据保持新鲜;
  2. 观察与评估:在业务低峰期观察读写性能、慢查询与执行计划变化,此阶段随时可以回滚;
  3. 回滚或确认:若索引仍然必要,执行ALTER TABLE t ALTER INDEX idx_a VISIBLE;立即恢复;若确认无用,再执行DROP INDEX idx_a ON t;——此时才执行真正破坏性的操作,且由于索引始终被维护,回滚(重建索引)仍需成本,因此"先 INVISIBLE 观察"是唯一的低风险路径。

需要注意的边界条件:主键索引(含隐式主键等价约束的 NOT NULL 唯一索引)不能被设置为不可见;列式索引同样不支持 INVISIBLE;SQL Hint 中显式引用不可见索引会报错而非静默忽略。

总结

不可见索引是 TiDB 提供的一种"可逆的索引下线"能力:以索引元数据上的一个可见性标记为核心,结合优化器访问路径过滤、独立的tidb_opt_use_invisible_indexes系统开关,以及一套与 MySQL 高度兼容的语法与行为约束,实现了在不动 SQL、不重建索引的前提下安全评估索引价值的目标。其底层实现(ActionAlterIndexVisibilityDDL Job + 优化器过滤)与设计文档中的方案完全对应,相关源码与测试均可在本仓库的 pkg/ddl、pkg/planner/core 与 pkg/infoschema 中查阅。

【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidb

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 2:04:29

数据积木:从标准化到可复用的数据体系建设方法论

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 2:02:42

KMP算法详解:从暴力匹配到高效字符串匹配的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 2:01:53

PHP Telegram Bot测试驱动开发:编写高质量机器人代码的完整流程

PHP Telegram Bot测试驱动开发:编写高质量机器人代码的完整流程 想要构建稳定可靠的Telegram机器人吗?测试驱动开发(TDD)是确保PHP Telegram Bot代码质量的关键方法。通过先写测试再写实现,你可以创建出更健壮、更易维…

作者头像 李华
网站建设 2026/9/10 2:00:39

CANN/ge图编译缓存功能

图编译缓存 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、TensorFlow 前端…

作者头像 李华
网站建设 2026/9/10 2:00:21

H.264视频编码核心原理与工程实践全解析

1. H.264在数字图像处理里的真实位置做数字图像处理的人,十有八九会绕到视频编码这一步。H.264这个格式,我在实际项目里用了快十年,从最早的嵌入式监控到后来的云端转码,几乎每个环节都跟它打过交道。可以说,H.264是当…

作者头像 李华
网站建设 2026/9/10 2:00:17

CANN/ge:指定Graph输入输出的内部格式

指定Graph输入输出的内部格式 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorc…

作者头像 李华