ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

TiDB 聚簇索引(Clustered Index)设计与实现:从 Handle 抽象到全链路改造

TiDB 聚簇索引(Clustered Index)设计与实现:从 Handle 抽象到全链路改造 TiDB 聚簇索引Clustered Index设计与实现从 Handle 抽象到全链路改造【免费下载链接】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 将主键与行数据存储位置绑定、省去二次回表的关键能力。本文以 TiDB 官方设计文档 docs/design/2020-05-08-cluster-index.md 为骨架结合当前仓库源码系统讲解聚簇索引的存储编码、读写路径、Planner/Coprocessor/Executor 改造、兼容性策略与落地路线。读完本文你将理解 TiDB 如何从仅支持单整数列主键聚簇演进到支持任意类型主键聚簇并能定位到对应的源码实现。背景为什么聚簇索引只能覆盖单整数列主键在设计文档提出的时间点2020-05-08TiDB 只支持单整数列主键的聚簇索引。其存储模型是表中每行有一个 row-key 条目存储整行数据所有二级索引条目指向该 row-key。如果主键是单整数列就直接把该列的值当作行 handlerow-key 的一部分实现主键即存储位置否则由系统内部为行分配一个自增 handle并额外建立一条指向 row-key 的索引。当时的 row key 格式t | {table_id} | _r | {handle} // component 1 | 8 | 2 | 8 // byte sizetable_id与handle均为 int64因此 row key 长度恒为 19 字节。非唯一索引 key 格式t | {table_id} | _i | {index_id} | {index_column_values} | encoded_handle // component 1 | 8 | 2 | 8 | size of the values | 9 // byte sizehandle 作为索引 key 的后缀保证同一索引值下不同行按 handle 有序排列。唯一索引 key 格式t | {table_id} | _i | {index_id} | {index_column_values} // component 1 | 8 | 2 | 8 | size of the values // byte size唯一索引的 handle 存放在索引条目的value中而不是 key 中。三种典型操作路径的差异On Write主键为单整数列时直接用主键列值作为 handle否则内部分配 handle 并额外写入一条指向 row key 的索引条目。On Point Select单整数列主键时直接用主键值构造 row key 做一次 TiKV 查找否则要先构造索引 key 读 handle再构造 row key 做第二次查找回表。On Range Scan单整数列主键时直接构造 row key 范围做一次扫描否则要扫描索引收集 handle 集合再对每行做 point lookup。可以看到非单整数列主键的表在写入和查询时都多了一次索引往返这正是聚簇索引要解决的问题。核心抽象Handle 接口与两种具体实现设计文档提出将 handle 抽象为接口从而把行的 ID从 int64 推广到任意主键列组合。当前源码中的定义位于 pkg/kv/key.go接口方法包括package kv // Handle is the ID of a row. type Handle interface { // IsInt returns if the handle type is int64. IsInt() bool // IntValue returns the int64 value if IsInt is true, it panics if IsInt returns false. IntValue() int64 // Next returns the minimum handle that is greater than this handle. Next() Handle // Equal returns if the handle equals to another handle, it panics if the types are different. Equal(h Handle) bool // Compare returns the comparison result of the two handles, it panics if the types are different. Compare(h Handle) int // Encoded returns the encoded bytes. Encoded() []byte // Len returns the length of the encoded bytes. Len() int // NumCols returns the number of columns of the handle. NumCols() int // EncodedCol returns the encoded column value at the given column index. EncodedCol(idx int) []byte // String implements the fmt.Stringer interface. String() string }接口提供两个具体类型IntHandletype IntHandle int64pkg/kv/key.go对应单整数列主键编码格式与旧实现完全一致零额外开销。CommonHandlepkg/kv/key.go对应其余所有主键类型内部保存encoded字节序列和colEndOffsets []uint16每个主键列在编码后的结束偏移借助EncodedCol(idx)可以切出第 idx 列主键的编码值。从源码看接口在演进中还补充了Copy、Data、MemUsage、ExtraMemSize等方法用于内存追踪与深拷贝说明该抽象经受住了后续内存控制等特性的复用。一个容易被忽略的工程细节handle 在很多地方被当作 map key 使用。如果直接用编码字节作 keyIntHandle 每次都会产生内存分配。设计文档给出的方案是自定义HandleMap类型内部用两个 map 分别存放 int64 与字符串 key从而避免 IntHandle 的分配开销。主键类型与 handle 类型的对应关系单列整数主键 → IntHandle其他任意主键 → CommonHandle。整数 handle 的 row key 与索引 key 格式保持不变改造只需关注 CommonHandle 路径。编码方案CommonHandle 的三类存储格式行 key 格式CommonHandle row keyt | {table_id} | _r | {common_handle} // component 1 | 8 | 2 | len(common_handle) // byte sizecommon_handle与索引 key 中的index_column_values编码完全一致因此 row key 长度不再固定为 19 字节而是随主键内容变化。非唯一索引 key 格式t | {table_id} | _i | {index_id} | {idx_col_vals} | {common_handle} // component 1 | 8 | 2 | 8 | len(idx_col_vals) | len(common_handle) // byte size非唯一索引的 value 保持原样common_handle 作为 key 后缀延续了同一索引值下按 handle 排序的性质。唯一索引 value 格式{tailLen} | {common_handle_flag} | {common_handle_len} | {common_handle} // component 1 | 1 | 2 | len(common_handle) // byte size唯一索引的 key 保持不变common_handle 被编码进 value。tailLen用于描述 handle 之后是否还有附加数据如 TTL 等common_handle_flag用于区分 handle 类型使旧版本 TiKV 也能安全跳过未知 handle。设计文档强调编码的关键原则是让 common_handle 与索引列值使用同一套列值编码codec这样 Coprocessor 解码索引列时可以复用同一逻辑也保证了行 key 与索引 key 之间的字典序一致性。读写路径的收益为什么能省掉一次往返聚簇索引的价值集中体现在三类操作上操作非聚簇旧聚簇CommonHandle写入分配内部 handle 额外索引条目直接用主键值做 handle主键不再冗余编码进行值点查索引查 handle → 回表查行用主键值直接构造 row key一次点查范围扫描扫索引收 handle → 多次 point lookup直接扫 row key 范围单次扫描设计文档给出的量化动机是TPC-C 负载下把多列主键压缩成单整数主键后性能提升约 33%——这 33% 正是聚簇索引期望在多列主键表上追回的开销。需要说明的是这是设计阶段的实验数据不同负载下的实际收益取决于主键长度与回表频率。另外值得注意的是聚簇索引表在写入时不需要把主键列编码进行 value见下文 Insert 改造进一步压缩了行存储。Planner把主键访问当作表路径而非索引路径设计文档对优化器提出了两点关键改造主键索引路径视为表路径对 CommonHandle 表主键索引的访问路径应走 TableScan 计划而非 IndexLookUp 计划——因为主键本身就是行位置不需要索引 → 回表两步。同时要仔细审查所有隐式假设 TableScan 使用 int64 handle 的逻辑并可能针对聚簇表调整代价模型。IndexScan 输出 Schema 按需包含主键列若查询用到任一主键列或执行器处于 IndexLookUp 中索引扫描的 schema 为{index columns},{primary key columns}且 CommonHandle 表不再额外输出 handle 列若只用索引列schema 仅为{index columns}。文档示例create table t (a int, b int, c int, d int, e int, primary key (a, b), index c_d (c, d));c_d索引扫描的 schema 应为c, d, a, b或c, d取决于查询是否引用主键列。若查询不引用列e则c_d成为覆盖索引可直接构建 IndexScan 计划而无需 IndexLookUp。Coprocessor解码与采样改造TiKV Coprocessor 侧需要配合的改动对应 docs/design/2020-05-08-cluster-index.md 的 Coprocessor 一节IndexScan若扫描 schema 不含主键列逻辑不变若含主键列则需把 common handle 解码为主键列值编码后的列值可通过 Handle 的EncodedCol方法直接切出无需重新编码。Analyze Column多列主键时CMSketch 需要插入主键的前缀列值这决定了旧版SHOW STATS_BUCKETS/ 多列统计的收集口径。Fast Analyzehandle 不再是整数无法按给定位置随机生成采样 key因此需要新增一种 Coprocessor 请求类型在扫描过程中直接采样 key/value 对并返回统计结果。Executor受影响的四个主要组件设计文档明确列出需要改造的执行器Admin Executorsadmin check/recover table/index严重依赖 handle 是 int64 的假设需要大改以支持 CommonHandle。PointGet / BatchPointGet需要把 common handle 解码到 chunk 中。Insert不再需要把主键列编码进行 value主键信息已经蕴含在 row key 中。Update若任一主键列发生变化需要删旧行 插新行因为主键即存储位置主键变更等于行搬家。SplitTableRegion需要重新设计——旧逻辑根据 region 的 start/end key 取中间 key而 common handle 无法像整数那样简单取中值。这些改动在当前仓库中已有大量落点例如 pkg/ddl/executor.go、pkg/ddl/index_cop.go 中随处可见IsCommonHandle分支判断。兼容性与系统变量如何安全上线设计文档给出的兼容策略是向后兼容、按表启用、升级需显式开启具体由两处机制承载表结构标记在model.TableInfo中新增字段。当前定义见 pkg/meta/model/table.go// PKIsHandle is true when PK is clustered and a single integer column. PKIsHandle bool json:pk_is_handle // IsCommonHandle is true when PK is clustered and not a single integer column. IsCommonHandle bool json:is_common_handle // CommonHandleVersion is the version of the clustered index. // 0 for the clustered index created 5.0.0 RC. // 1 for the clustered index created 5.0.0 RC. CommonHandleVersion uint16 json:common_handle_version配套提供了HasClusteredIndex()方法pkg/meta/model/table.go统一判断两种聚簇形态。所有 CommonHandle 相关逻辑都必须先检查IsCommonHandle再进入对应代码块。全局系统变量tidb_enable_clustered_index定义于 pkg/sessionctx/variable/sysvar.go支持OFF/ON/INT_ONLY三个取值INT_ONLY仅为兼容旧版本保留会输出弃用警告会话默认值见 pkg/sessionctx/variable/session.go。启用语义新集群bootstrap 阶段直接写入tidb_enable_clustered_index 1默认开启聚簇索引旧集群升级需要用户显式执行SET GLOBAL tidb_enable_clustered_index 1后才生效避免升级瞬间改变既有表的存储布局建表时机CREATE TABLE时若全局变量为1则在TableInfo上设置IsCommonHandle true当前 DDL 建表路径见 pkg/ddl/create_table.go 中相关分支。实施路线与后续工作设计文档给出的落地顺序如下implement the Handle interface for IntHandle and CommonHandle. | refactor int64 handle to the Handle interface in all tidb packages. | - implement the codec. | |- support codec in tikv. | | | - support coprocessor indexScan | | | - support coprocessor fastAnalyze | | | - support coprocessor analyzeColumn | - support common handle in planner. | |- support common handle in executors. | |- support common handle in ddl. | - support common handle in other packages thats need minor change.即先落地 Handle 接口抽象 → 全量重构 int64 handle 为接口 → 实现 codec 与 TiKV 侧支持 → 再逐层改造 planner / executors / ddl。从当前仓库看该路线已基本完成Handle 接口还演进出了Copy、Data、MemUsage等额外能力CommonHandleVersion字段记录了 5.0.0 RC 前后两代聚簇索引编码的差异。设计文档最后列出的 Open Issues 是TiFlash 与 CDCTiCDC也需要同步更新以支持聚簇索引——即列存副本与增量日志同步必须理解 CommonHandle 编码否则主键变更deleteinsert在复制链路上会产生错误。小结聚簇索引的本质是主键值 行存储位置。TiDB 通过把 handle 抽象为Handle接口用IntHandle保住单整数列主键的零开销路径用CommonHandle覆盖所有其他主键类型并以common_handle 与索引列共用同一套列值编码为支点撬动了从 TiKV codec、Coprocessor、Planner 到 Executor 的全链路改造再通过IsCommonHandle表标记与tidb_enable_clustered_index全局变量实现了向后兼容的灰度上线。对于以多列主键为主业务模型的用户这一特性直接消除了索引回表带来的读写放大是理解 TiDB 行存储与主键语义的关键一课。【免费下载链接】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),仅供参考
返回列表