尧图精选

TiDB 表分区(Table Partition)设计与实现解析

🕒 发布时间:2026/9/10 16:59:38 📁 来源:尧图网络
TiDB 表分区Table Partition设计与实现解析【免费下载链接】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表分区是 MySQL 用户广泛使用的一项核心能力本文基于 TiDB 仓库中的设计提案文档 docs/design/2018-10-19-table-partition.md系统梳理 TiDB 分区表从设计取舍、兼容性策略到读写路径、DDL 操作与限制的完整脉络并结合当前仓库源码pkg/table/tables/partition.go、pkg/meta/model/table.go、pkg/planner/core/rule/rule_partition_processor.go 等还原其底层实现原理。读完本文你将理解 TiDB 分区表如何存储、如何读写、如何裁剪、如何演进并能直接对照源码验证每一个关键结论。背景TiDB 为什么要支持表分区MySQL 提供成熟的表分区Table Partition能力。在 TiDB 尚未支持分区时社区大量场景无法在 TiDB 上落地设计提案中归纳了三类典型收益按范围删除旧数据对于按时间增长的数据可以通过DROP PARTITION直接摘除过期分区避免昂贵的全表DELETE缓解热点、提升写入性能通过PARTITION BY HASH将写入打散到多个分区避免单点写热点加速查询利用分区裁剪partition pruning查询只需扫描命中的分区比全表扫描更快。设计目标与取舍提案明确了分步实现、问题驱动的路线先实现 Range 分区再实现 Hash 分区当前仓库中已进一步演进为 Range、Hash、Key、List 等多种类型见下文演进现状暂不支持子分区subpartition暂不支持涉及数据搬移的 REORGANIZE PARTITION新功能以解决问题为导向初期不必覆盖 MySQL 的全部行为语法必须保持 MySQL 兼容——这是 TiDB 从诞生起的承诺。对于包含未实现特性的分区表TiDB 解析 SQL 后忽略其分区属性将其当作普通表处理。这一解析但降级为普通表的策略是 TiDB 分区功能能够渐进式落地、不阻塞用户建表的关键设计。兼容性与升级策略PartitionInfo.Enable标志分区功能引入前后存在三阶段兼容问题新 TiDB 运行在旧集群数据上旧 TiDB 运行在新集群数据上功能部分实现期间如只实现了 Range、尚未实现 Hash的升级过程。提案给出的方案是在TableInfo中引入并持久化一个PartitionInfo.Enable标志新 TiDB 运行在旧集群上时检查到该标志为false仍按普通表处理行为与旧版本一致旧 TiDB 无法运行在包含分区表数据的集群上因此升级本身不兼容但若新 TiDB 从未创建过分区表即功能未被使用集群仍可降级回旧版本在 Range 已实现而 Hash 未实现的阶段CREATE TABLE ... PARTITION BY HASH ...不会把Enable置为true而CREATE TABLE ... PARTITION BY RANGE ...会置为true。结论只有当持久化的PartitionInfo.Enable为true且代码能够处理分区表时分区功能才真正生效。这一标志在今天的源码中依然存在并发挥着同样的作用。在 pkg/meta/model/table.go 的PartitionInfo结构体中type PartitionInfo struct { Type ast.PartitionType json:type Expr string json:expr Columns []ast.CIStr json:columns // User may already create table with partition but table partition is not // yet supported back then. When Enable is true, write/read need use tid // rather than pid. Enable bool json:enable ... Definitions []PartitionDefinition json:definitions Num uint64 json:num }注释明确说明Enable为true时读写需使用分区 IDpid而非表 IDtid为false时则按老逻辑处理。该结构后续还演进出了AddingDefinitions、DroppingDefinitions分区 ADD/DROP/TRUNCATE 中间态、DDLAction、DDLState等字段用于支撑在线 DDL 状态机这也是后续版本支持 REORGANIZE、EXCHANGE 等复杂操作的基础。实现原理分区表如何存储TiDB 存在两级映射SQL 数据 → 逻辑 key 区间 → 物理存储TiKV。普通表在编码时以table id row id作为 key、行数据作为 value逻辑 key 区间再被切分为 Region 分布到 TiKV。分区作用于第一级映射分区 ID 被视作与表 ID 等价分区表的一行数据使用partition id row id作为编码后的 key且分区 ID 在集群范围内唯一。表 ID 与分区 ID 的对应关系维护在TableInfo中。插入分区表的新行若不属于任何分区则写入操作失败。NULL值的行为参照 MySQL 文档例如 Range 分区中NULL会被归入最小的分区等规则。源码中这一分区 ≈ 表的建模体现得十分直接。在 pkg/table/tables/partition.go// Both partition and partitionedTable implement the table.Table interface. var _ table.PhysicalTable partition{} var _ table.Table partitionedTable{} // partition is a feature from MySQL: ... // A partition table may contain many partitions, each partition has a unique partition // id. The underlying representation of a partition and a normal table (a table with no // partitions) is basically the same. type partition struct { TableCommon table *partitionedTable } type partitionedTable struct { TableCommon partitionExpr *PartitionExpr partitions map[int64]*partition ... }注释与设计文档一一对应每个分区拥有唯一的 partition id分区与普通表在底层表示上基本一致。partition内嵌了TableCommon与普通表共享的行/索引编码逻辑因此对 TiKV 而言一个分区就是一张独立的表。测试辅助函数也印证了这一编码方式pkg/table/tables/partition.go 中PartitionRecordKey(pid, handle)直接用分区 ID 生成记录前缀并编码记录 key。读取路径UnionAll 展开与分区裁剪逻辑展开DataSource → UnionAll提案以 Range 分区表为例说明读取等价关系CREATE TABLE t (id INT) PARTITION BY RANGE (id) (PARTITION p1 VALUES LESS THAN (10), PARTITION p2 VALUES LESS THAN (20), PARTITION p3 VALUES LESS THAN (30));查询SELECT * FROM t等价于SELECT * FROM (UNION ALL SELECT * FROM p1 WHERE id 10 SELECT * FROM p2 WHERE id 20 SELECT * FROM p3 WHERE id 30);在逻辑优化阶段DataSource算子被改写为UnionAll算子每个分区在物理优化阶段生成各自的TableReader。这一实现存在两个已知缺点分区数量很多时会产生大量 readerEXPLAIN结果对用户不友好UnionAll算子无法保持有序性若下游算子需要有序结果如IndexReader需要额外引入Sort算子。当前源码中的PartitionProcessor规则正是这一设计的直接继承。在 pkg/planner/core/rule/rule_partition_processor.go 中其文件头注释完整保留了上述等价改写示例并说明PartitionProcessor 之所以存在是因为在谓词下推之后更容易做分区裁剪其Optimize通过rewriteDataSource完成改写。同时注释注明它服务于静态分区裁剪模式static partition prune mode说明后续 TiDB 又演进出了动态裁剪模式以缓解 reader 过多的问题。若使用分区选择partition selection语法如SELECT * FROM t PARTITION (p1)优化器应将表 ID 转换为分区 ID并关闭分区裁剪。分区裁剪Partition Pruning分区裁剪在谓词下推之后、逻辑优化阶段执行Range 分区基于分区列上的范围过滤条件进行裁剪Hash 分区当过滤条件形如key const时可以进行裁剪。结合上文裁剪先于DataSource改写执行谓词下推 → 裁剪 → UnionAll 展开只有被判定命中的分区才会进入最终的读取计划。对应测试可在 pkg/planner/core/casetest/partition/partition_pruner_test.go 中找到覆盖各类分区类型的裁剪用例。写入路径locatePartition AddRecord所有写操作最终都会调用table.AddRecord之类的接口方法。因此分区表的写实现就是在PartitionedTable结构体上实现该接口PartitionedTable实现table.Table接口并重载AddRecord方法同时提供一个locatePartition方法用于决定一行数据应插入到哪个分区每个分区各自维护独立的索引数据插入操作必须保证数据与索引的一致性。当前源码完整继承了这一设计。在 pkg/table/tables/partition.go 中func (t *partitionedTable) AddRecord(ctx table.MutateContext, txn kv.Transaction, r []types.Datum, opts ...table.AddRecordOption) (recordID kv.Handle, err error) { return partitionedTableAddRecord(ctx, txn, t, r, nil, opts) } func partitionedTableAddRecord(ctx table.MutateContext, txn kv.Transaction, t *partitionedTable, r []types.Datum, partitionSelection map[int64]struct{}, opts []table.AddRecordOption) (recordID kv.Handle, err error) { opt : table.NewAddRecordOpt(opts...) pid, err : t.locatePartition(ctx.GetExprCtx().GetEvalCtx(), r) if err ! nil { return nil, errors.Trace(err) } ... tbl : t.getPartition(pid) recordID, err tbl.addRecord(ctx, txn, r, opt) ... }写入流程清晰可见先locatePartition定位分区再委派给该分区的addRecord写入。若指定了partitionSelection对应 SQL 中的分区选择还会校验定位到的分区是否在允许集合内否则返回ErrRowDoesNotMatchGivenPartitionSet——这正是插入的行不属于任何分区则操作失败以及分区选择语义的落地。locatePartition的底层分发逻辑在locatePartitionCommon中pkg/table/tables/partition.go按分区类型switchRange 分区走locateRangePartition/locateRangeColumnPartitionHash 走locateHashPartitionKey 走LocateKeyPartitionList 走locateListPartition。可以推断随着分区类型从最初Range、Hash两类扩展到 Key、List定位逻辑也被逐步泛化到同一个方法中但定位 → 委派写入的整体架构与提案一致。而GetPartitionByRowpkg/table/tables/partition.go则是locatePartition的只读复用读路径同样通过行数据定位物理分区。DDL 操作与分区管理DROP / TRUNCATE / ADD提案阶段DROP PARTITION、TRUNCATE PARTITION、ADD PARTITION三种操作作用于 Range 分区DROP PARTITION与DROP TABLE类似区别在于使用分区 ID操作完成后需更新TableInfo。特别提醒若要删除表中最后一个分区应使用DROP TABLE而非DROP PARTITIONTRUNCATE PARTITION清空分区内的全部数据和索引但保留分区本身。对应的 DDL 实现位于 pkg/ddl/partition.go。例如在 ADD PARTITION 时会对分区数量做上限检查pkg/ddl/partition.go// The last loop still not reach the max value, return error. if i mysql.PartitionCountLimit-1 { return errors.Trace(dbterror.ErrTooManyPartitions) } if len(tbInfo.Partition.Definitions)len(partDefs) mysql.PartitionCountLimit { return errors.Trace(dbterror.ErrTooManyPartitions) }而checkAddPartitionTooManyPartitionspkg/ddl/partition.go在物理阶段再次校验新增分区数与上述逻辑构成双重防线。分区管理语句提案时期下列分区管理语句可以解析但暂时忽略即不报错也不执行实际语义为后续能力逐步补齐预留语法入口ALTER TABLE ... REBUILD PARTITION ... ALTER TABLE ... OPTIMIZE PARTITION ... ALTER TABLE ... ANALYZE PARTITION ... ALTER TABLE ... REPAIR PARTITION ... ALTER TABLE ... CHECK PARTITION ... ALTER TABLE ... DROP/TRUNCATE PARTITION ALTER TABLE ... ADD PARTITION SHOW CREATE TABLE SHOW TABLE STATUS INFORMATION_SCHEMA.PARTITIONS 表需要说明的是这是 2018 年提案的初始范围。当前仓库中部分语句已具备完整实现例如SHOW CREATE TABLE会结合PartitionInfo.IsEmptyColumns等字段还原建表语句ANALYZE TABLE t PARTITION a ...的分区级分析语法也已在 pkg/parser/parser_test.go 中有对应解析测试分区 REORGANIZE、EXCHANGE 等涉及数据搬移的操作也已在后续演进中实现可见 pkg/ddl/tests/partition/exchange_partition_test.go 与 pkg/ddl/tests/partition/reorg_partition_test.go。提案规划的是最小可行路径而非最终能力的边界。限制Limitations提案明确引用 MySQL 的分区限制文档并列出 TiDB 当时的约束分区键不能是整型列之外的列或最终解析为整型或NULL的表达式分区键不能是子查询即使子查询解析为整型分区数量上限MySQL 为 8192TiDB 当时为 1024分区表达式中允许使用的函数需参考 MySQL 的分区函数限制列表。关于分区数上限当前仓库已对齐 MySQL 标准在 pkg/parser/mysql/const.go 中// PartitionCountLimit is limit of the number of partitions in a table. // Reference linking https://dev.mysql.com/doc/refman/5.7/en/partitioning-limitations.html. PartitionCountLimit 8192即当前版本 TiDB 的单表分区数上限为8192与 MySQL 一致作为对比提案当时规划为 1024属于早期保守取值。超出该限制会抛出dbterror.ErrTooManyPartitions相关测试用例如 pkg/ddl/tests/partition/db_partition_test.go 中PARTITION BY HASH(store_id) PARTITIONS 102400000000的用例验证了这一约束的生效。演进现状从提案到当前实现作为 2018 年的设计提案其核心架构经受住了时间的检验并在当前仓库中持续演进维度提案初始范围当前仓库状态分区类型先 Range、后 HashRange / Hash / Key / ListlocatePartitionCommon中四类分派齐全分区数上限10248192pkg/parser/mysql/const.go元数据PartitionInfo.Enable标志增加AddingDefinitions/DroppingDefinitions、DDLAction/DDLState等在线 DDL 中间态字段pkg/meta/model/table.go分区裁剪逻辑优化阶段执行静态裁剪PartitionProcessor 动态裁剪并存DDL 操作DROP/TRUNCATE/ADD进一步支持 REORGANIZE、EXCHANGE、ADD/REMOVE PARTITIONING 等无论功能如何扩展分区在存储层等价于独立表、在读写层通过定位函数分发到具体分区这一从提案确立的两层模型始终未变——这正是该设计文档最具价值的核心结论也使其成为理解 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),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →