如何移除TiDB中的表分区原创
分区表在数据量增长过快时可能成为瓶颈——查询优化器需要遍历过多分区元数据,分区裁剪失效导致全表扫描。当分区策略不再适合业务(如从按天分区的日志表转为冷归档),移除分区并转为普通表配合索引是常见优化路径。TiDB 支持 ALTER TABLE ... REMOVE PARTITIONING 单行 DDL,绝大多数场景无需手动搬迁数据。
版本说明
官方分区文档描述了 ALTER TABLE ... REMOVE PARTITIONING 的行为,但未标注该语法的引入版本。使用前请在自己的版本上用语句预览或测试集群确认可用性。
⚠️ 数据丢失风险:REMOVE PARTITIONING 在 v7.5.3 之前存在导致数据丢失的 bug(pingcap/tidb#53385 (opens new window)),v7.5.3 起修复。v7.5.3 以下版本严禁使用方案一,请改用方案二的拷贝搬迁法。
REMOVE PARTITIONING 属于 Reorg-Data 类 DDL,执行时会复制全表数据并重建索引。本文内容基于文档与源码分析,未在 TiDB 实例上实测,生产环境操作前请在测试集群验证。
# 识别分区表
在移除分区前,需确认哪些表使用了分区。TiDB 的 INFORMATION_SCHEMA.TABLES 提供了 CREATE_OPTIONS 字段标识分区状态:
SELECT
TABLE_SCHEMA,
TABLE_NAME,
CREATE_OPTIONS
FROM INFORMATION_SCHEMA.TABLES
WHERE CREATE_OPTIONS LIKE '%partitioned%';
2
3
4
5
6
预期输出示例:
+--------------+----------------+----------------+
| TABLE_SCHEMA | TABLE_NAME | CREATE_OPTIONS |
+--------------+----------------+----------------+
| test_db | user_activity | partitioned |
| test_db | order_data | partitioned |
+--------------+----------------+----------------+
2
3
4
5
6
补充确认分区类型(Range、Hash、List 等):
SELECT
TABLE_NAME,
PARTITION_NAME,
PARTITION_METHOD,
PARTITION_EXPRESSION
FROM INFORMATION_SCHEMA.PARTITIONS
WHERE TABLE_SCHEMA = 'test_db'
AND TABLE_NAME = 'user_activity';
2
3
4
5
6
7
8
预期输出示例:
+---------------+----------------+------------------+--------------------------------+
| TABLE_NAME | PARTITION_NAME | PARTITION_METHOD | PARTITION_EXPRESSION |
+---------------+----------------+------------------+--------------------------------+
| user_activity | p0 | RANGE | to_days(`activity_date`) |
| user_activity | p1 | RANGE | to_days(`activity_date`) |
| user_activity | p2 | RANGE | to_days(`activity_date`) |
+---------------+----------------+------------------+--------------------------------+
2
3
4
5
6
7
记录分区数量和行数分布,作为移除后的对比基准:
SELECT
PARTITION_NAME,
TABLE_ROWS
FROM INFORMATION_SCHEMA.PARTITIONS
WHERE TABLE_SCHEMA = 'test_db'
AND TABLE_NAME = 'user_activity'
ORDER BY PARTITION_NAME;
2
3
4
5
6
7
# 方案一:直接 DDL 移除分区(推荐,仅限 v7.5.3+)
版本分界
v7.5.3 以下版本严禁使用此方案。REMOVE PARTITIONING 存在 pingcap/tidb#53385 (opens new window) 数据丢失 bug,v7.5.3 起修复。低版本请改用方案二的拷贝搬迁法。
官方文档提供单行 DDL 指令原地移除分区,内部流程为:复制全表数据到新的非分区表结构→重建索引→原子切换元数据。
# 执行步骤
步骤 1:确认表结构(记录以备回滚参考)
SHOW CREATE TABLE test_db.user_activity\G
预期输出关键部分:
CREATE TABLE `user_activity` (
`id` bigint(20) NOT NULL,
`activity_date` date NOT NULL,
`user_id` bigint(20) DEFAULT NULL,
`action` varchar(64) DEFAULT NULL,
PRIMARY KEY (`id`,`activity_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
PARTITION BY RANGE (to_days(`activity_date`))
( PARTITION p0 VALUES LESS THAN (738950),
PARTITION p1 VALUES LESS THAN (738981),
PARTITION p2 VALUES LESS THAN MAXVALUE)
2
3
4
5
6
7
8
9
10
11
步骤 2:执行 REMOVE PARTITIONING
ALTER TABLE test_db.user_activity REMOVE PARTITIONING;
为什么要分两步而非原六步:旧教程推荐的 "创建新表→REMOVE PARTITIONING→加索引→迁数据→DROP 原表→重命名" 流程是错误冗余的。REMOVE PARTITIONING 本身就是原子操作,内部已包含数据复制和索引重建。额外手动搬迁不仅浪费时间,还引入 DROP 后 RENAME 失败导致数据丢失的风险。
步骤 3:添加替代索引
移除分区后,原本依赖分区裁剪的查询需要索引支撑。根据业务查询模式添加索引:
ALTER TABLE test_db.user_activity ADD INDEX idx_activity_date (activity_date);
ALTER TABLE test_db.user_activity ADD INDEX idx_user_id (user_id);
2
索引列选择原则:
- 若原查询多用
WHERE activity_date BETWEEN ...,优先在activity_date建索引 - 若原查询多用
WHERE user_id = ...,在user_id建索引 - 复合索引顺序遵循最左前缀原则
# 验证移除结果
确认 SHOW CREATE TABLE 输出中 PARTITION BY 子句已消失:
SHOW CREATE TABLE test_db.user_activity\G
预期输出(关键差异):
CREATE TABLE `user_activity` (
`id` bigint(20) NOT NULL,
`activity_date` date NOT NULL,
`user_id` bigint(20) DEFAULT NULL,
`action` varchar(64) DEFAULT NULL,
PRIMARY KEY (`id`,`activity_date`),
KEY `idx_activity_date` (`activity_date`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
2
3
4
5
6
7
8
9
确认行数一致:
SELECT COUNT(*) FROM test_db.user_activity;
对比移除前各分区行数之和,应完全相等。
确认执行计划使用索引而非全表扫描:
EXPLAIN SELECT * FROM test_db.user_activity WHERE activity_date = '2024-01-15';
预期输出应包含 IndexRangeScan 或 IndexLookUp,而非 TableFullScan。
# 方案二:拷贝搬迁法(降级为备选)
直接 DDL 虽好,但在特定场景下拷贝搬迁法仍有价值:
适用场景:
- 超大表 DDL 阻塞风险:TiDB 的 Reorg-Data DDL 会复制全表数据,若表超过 100GB,DDL 执行期间可能阻塞同表的其他写入(视 tidb_ddl_reorg_batch_size 和系统负载而定)
- 灰度验证需求:需要在新结构上跑业务 SQL 验证性能,确认无误后再切换
- 版本低于 v7.5.3:受 #53385 (opens new window) 数据丢失 bug 影响,方案一不可用;更早的版本还可能完全不支持该语法(官方未标注引入版本,需自行确认)
# 执行步骤
步骤 1:创建结构一致的新表
CREATE TABLE test_db.user_activity_new LIKE test_db.user_activity;
步骤 2:新表移除分区
ALTER TABLE test_db.user_activity_new REMOVE PARTITIONING;
步骤 3:添加索引
ALTER TABLE test_db.user_activity_new ADD INDEX idx_activity_date (activity_date);
步骤 4:分批迁移数据(关键:避免事务超限)
TiDB 默认事务大小限制为 1GB(txn-total-size-limit),超大表 INSERT ... SELECT 可能超限。改用分批迁移:
-- 假设 activity_date 有连续分布,按日期分批
INSERT INTO test_db.user_activity_new
SELECT * FROM test_db.user_activity
WHERE activity_date >= '2024-01-01' AND activity_date < '2024-02-01';
INSERT INTO test_db.user_activity_new
SELECT * FROM test_db.user_activity
WHERE activity_date >= '2024-02-01' AND activity_date < '2024-03-01';
2
3
4
5
6
7
8
每批执行后验证:
SELECT COUNT(*) FROM test_db.user_activity_new;
步骤 5:原子切换(RENAME,非 DROP 先)
正确的切换顺序:先重命名原表保留,再重命名新表顶替,DROP 放在最后且手动执行。
-- 在同一事务中执行
RENAME TABLE
test_db.user_activity TO test_db.user_activity_backup,
test_db.user_activity_new TO test_db.user_activity;
2
3
4
RENAME 操作是原子的,不会出现表名空白期。
步骤 6:业务验证后清理
确认业务读写正常、数据完整后,手动删除备份表:
DROP TABLE test_db.user_activity_backup;
# 拷贝搬迁法的代价
| 代价项 | 说明 |
|---|---|
| 双倍存储 | 迁移期间新老表同时存在,需确保磁盘充足 |
| 事务限流 | 大批量 INSERT 需手动切分,避免 txn-total-size-limit |
| 应用层适配 | 若表名切换需配合应用配置变更,比原地 DDL 复杂 |
# 坑与边界
# 坑一:v7.5.3 以下 REMOVE PARTITIONING 存在数据丢失 bug
REMOVE PARTITIONING 在 v7.5.3 之前存在致命 bug(pingcap/tidb#53385 (opens new window)),执行后可能导致数据丢失。官方 v7.5.3 release notes 明确说明:"Fix the issue that executing ALTER TABLE ... REMOVE PARTITIONING might cause data loss"。
影响范围:所有 v7.5.3 以下版本,无论表大小、分区类型。
解药:
- v7.5.3 及以上:可使用方案一
- v7.5.3 以下:严禁使用方案一,改用方案二的拷贝搬迁法
验证当前版本:
SELECT tidb_version()\G
# 坑二:REMOVE PARTITIONING 期间写入阻塞
REMOVE PARTITIONING 属于 Reorg-Data 类 DDL(与添加索引、修改列类型同级)。执行期间,涉及该表的写入可能被阻塞,阻塞时长与数据量成正比。
缓解方案:
- 选择业务低峰期执行
- TiDB v6.2.0+ 支持
tidb_ddl_enable_fast_reorg,开启后可加速数据重组 - 超大表优先考虑方案二的拷贝搬迁法,将风险分散到可控的分批 INSERT
# 坑三:原表有外键或触发器
TiDB 支持外键(v6.6.0+ 实验性),若分区表被外键引用,REMOVE PARTITIONING 会报错。
检查外键依赖:
SELECT
TABLE_NAME,
CONSTRAINT_NAME,
REFERENCED_TABLE_NAME
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS
WHERE REFERENCED_TABLE_NAME = 'user_activity';
2
3
4
5
6
若有外键,需先在外键 referencing 表上执行 DROP FOREIGN KEY,或改用方案二并同步更新外键指向。
# 坑四:分区键是主键的一部分
若主键定义为 PRIMARY KEY (id, activity_date),且 activity_date 是分区键,移除分区后主键不变,但数据物理存储布局改变。这对正确性无影响,但 EXPLAIN 中的分区裁剪会消失,必须依赖索引保证性能。
# 坑五:TiFlash 副本同步延迟
若表有 TiFlash 副本,REMOVE PARTITIONING 后 TiFlash 需重新同步数据。通过以下命令检查同步进度:
SELECT * FROM information_schema.tiflash_replica
WHERE TABLE_SCHEMA = 'test_db' AND TABLE_NAME = 'user_activity';
2
确认 AVAILABLE 列为 1 后再将查询路由到 TiFlash。
# 可复用要点
v7.5.3+ 才可用方案一:
ALTER TABLE t REMOVE PARTITIONING在 v7.5.3 之前存在数据丢失 bug(#53385),v7.5.3 以下必须改用方案二。拷贝搬迁法保留给超大表或低版本:表大于 100GB、需要灰度验证,或版本低于 v7.5.3 时,用分批 INSERT + RENAME 切换替代原地 DDL。
事务大小限制是分批迁移的真正原因:TiDB 的
txn-total-size-limit默认 1GB,超大表迁移必须按业务维度(如日期)切分批次。验证务必覆盖元数据和执行计划:
SHOW CREATE TABLE确认分区语法消失、COUNT(*)确认行数一致、EXPLAIN确认查询走索引。TiFlash 副本需额外关注同步状态:分区结构变更会触发 TiFlash 重新加载,确认
AVAILABLE=1后再切换分析负载。
🤖 Agent 可直接解析的元数据块(点击展开)
{
"_meta": {
"doc_version": "2025-04-03",
"article_id": "tidb-remove-partition",
"profile_context": "any",
"estimated_setup_time": "30min",
"version_requirement": "TiDB >= v7.5.3 for REMOVE PARTITIONING (data loss bug #53385 fixed in v7.5.3)"
},
"quick_start": {
"identify_partitioned_tables": "SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE CREATE_OPTIONS LIKE '%partitioned%';",
"remove_partitioning": "ALTER TABLE db.table REMOVE PARTITIONING;",
"add_index": "ALTER TABLE db.table ADD INDEX idx_col (col);",
"verify_structure": "SHOW CREATE TABLE db.table;",
"verify_count": "SELECT COUNT(*) FROM db.table;",
"verify_plan": "EXPLAIN SELECT * FROM db.table WHERE col = 'value';"
},
"safety_rules": [
"REMOVE PARTITIONING 属于 Reorg-Data DDL,执行期间可能阻塞写入,选择低峰期",
"超大表(>100GB)优先考虑拷贝搬迁法,分散风险",
"拷贝搬迁法必须按 txn-total-size-limit 分批 INSERT,避免事务超限",
"RENAME 切换顺序:原表→备份名,新表→原表名;DROP 放在最后手动执行"
],
"verification": {
"partition_removed": "SHOW CREATE TABLE 输出中无 PARTITION BY 子句",
"row_count_match": "移除后 COUNT(*) 等于移除前各分区行数之和",
"index_used": "EXPLAIN 显示 IndexRangeScan 而非 TableFullScan",
"tiflash_ready": "SELECT AVAILABLE FROM information_schema.tiflash_replica WHERE TABLE_NAME='table'"
},
"troubleshooting": {
"ddl_blocked": "检查 tidb_ddl_reorg_batch_size 和系统负载,必要时开启 tidb_ddl_enable_fast_reorg",
"txn_too_large": "分批 INSERT,按业务日期或 ID 范围切分",
"foreign_key_error": "先在外键 referencing 表执行 DROP FOREIGN KEY"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34