尧图精选

TiDB 非事务性 DML(BATCH DML)设计解析:语法、分片原理、执行流程与错误处理

🕒 发布时间:2026/9/10 4:42:12 📁 来源:尧图网络
TiDB 非事务性 DMLBATCH DML设计解析语法、分片原理、执行流程与错误处理【免费下载链接】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 设计文档 2022-03-25-non-transactional-dml.md 为主体结合仓库中 nontransactional.go 等源码与集成测试深入剖析非事务性语句Non-transactional DML的引入动机、BATCH ON ... LIMIT ...语法、语句分片sharding机制、会话级执行链路、错误处理策略及其约束边界。读完本文你将理解 TiDB 如何通过把一条超大 DELETE/UPDATE/INSERT 拆成一批带区间条件的普通语句来规避大事务限制并能据此规划批量数据清理与归档的实战方案。为什么需要非事务性 DML单条大语句的三重困境在 TiDB 中用户经常需要用一条语句完成大批量删除或更新bulk delete/update例如清理过期数据、归档历史记录。但当前 TiDB 无法很好地满足该需求原因来自三个方面见设计文档 Motivation or Background性能问题单条超大语句如无 WHERE 的全表 DELETE会长时间占用事务与资源造成明显的性能瓶颈事务大小限制一条语句对应的底层 KV 事务过大容易触及 TiDB 单事务大小限制而失败周边工具兼容性CDC、binlog 等同步工具对超大事务支持不佳大事务会放大复制延迟甚至导致同步中断。设计文档还明确对比了已废弃的batch-DML即tidb_batch_delete/tidb_batch_insert这类开关把一条 DML 在内部拆成小批量执行的历史方案batch-DML 在拆分执行时容易造成数据与索引不一致的风险而非事务性 DML 拆分出的每一条 SQL 都是语义完全正常的独立语句天然规避了该风险。基本思路把大语句翻译成互斥且穷尽的小语句序列设计文档 Introduction 给出了一个直观例子假设表t上分片列a的最小值为 1、最大值为 2000那么BATCH ON a LIMIT 1000 DELETE FROM t可被等价转换为两条顺序执行的小语句DELETE FROM t WHERE a BETWEEN 1 AND 1000 DELETE FROM t WHERE a BETWEEN 1001 AND 2000这种把一条 DML 转换为一系列互不重叠mutually exclusive且覆盖完整collectively exhaustive的小语句串行执行的语法就是非事务性 DML 的核心。它不提供原子性一旦中途失败无法整体回滚通常也不提供隔离性保证因此被称为非事务性。语法与用户接口两种书写形式设计文档定义了如下语法完整形态见 pkg/parser/ast/dml.go 中NonTransactionalDMLStmtAST 节点的ShardColumn、Limit、DryRun、DMLStmt字段完整形式BATCH ON column_name LIMIT batch_size DML由用户显式指定分片列shard column。例如BATCH ON id LIMIT 1000 DELETE FROM t WHERE created_at 2024-01-01;简短形式BATCH LIMIT batch_size DML省略分片列由系统从主键列中自动选择。若无法自动选出合适列例如聚簇索引由多列组成则会返回错误要求用户显式指定分片列。支持语句范围的演进设计文档撰写时说明第一阶段只支持DELETEUPDATE与INSERT INTO SELECT也值得考虑。而从当前仓库的落地实现看范围已经扩大——nontransactional.go 的 checkConstraint 中分别处理了DELETE删除单表或多表关联中符合条件的行UPDATEINSERT ... SELECT要求数据来源必须是 SELECT纯 VALUES 插入会被拒绝报Non-transactional insert supports insert select stmt only因 TiDB 解析器将REPLACE表达为带IsReplace标志的InsertStmtREPLACE INTO ... SELECT同样出现在集成测试中见 nontransactional.test。DRY RUN先看拆出来的 SQL再决定执行设计文档指出出于性能考虑分片列必须建有索引。为让用户在真正执行前确认拆分结果语法提供两种 dry run 形式AST 中的DryRun常量见 dml.go取值NoDryRun/DryRunQuery/DryRunSplitDmlBATCH ON column_name LIMIT batch_size DRY RUN QUERY DML BATCH ON column_name LIMIT batch_size DRY RUN DMLDRY RUN QUERY输出将被执行的分片键扫描 SELECT 语句。之所以返回查询语句而非查询计划是因为无法优雅地把一条 SQL 和一份计划同时放进一个结果集设计文档原话。DRY RUN输出语句将如何被拆分但只展示第一条与最后一条拆分语句作为示意。实现层面dry run 结果由 buildDryRunResults 构造DRY RUN的结果列名为split statement examplesDRY RUN QUERY的结果列名为query statement。输出与执行反馈用户通过三种途径获得反馈SQL 返回值、日志、进程信息process info。全部成功返回汇总信息。实际实现中返回两张列number of jobs与job status行为all succeeded见 buildExecuteResults。集成测试的期望结果也印证了这一点nontransactional.resultnumber of jobs job status 3 all succeeded被中止如KILL TIDB返回上下文取消context cancellation错误并把失败 job 的详细信息输出到日志。进度可视化process info 中的info字段不只描述当前正在执行的 SQL还描述全部 job 的整体进度慢日志与 statement summary 也会在 SQL 文本中携带进度。每条拆分语句实际以 SQL 注释形式携带/* job 11/41 */这样的前缀其中 11 是当前 job ID41 是 job 总数——慢查询里看到的就是形如/* job 11/41 */DELETE FROM test.t WHERE id BETWEEN xxx AND yyy;的实现细节见 doOneJob通过stmt.DMLStmt.SetText(...)把/* job %v/%v */ %s设置为语句文本。每条拆分语句还会以 INFO 级别记录自己的执行明细日志。分片Sharding的底层工作原理拆分发生在会话层而非编译层不同于绝大多数语句非事务性 DML 在**会话层session level**被处理它被当作简单计划SimplePlan不做常规编译优化。在 driver_tidb.go 的 ExecuteStmt 中可以看到分派逻辑——遇到*ast.NonTransactionalDMLStmt时不再走常规的Session.ExecuteStmt而是调用session.HandleNonTransactionalDMLif s, ok : stmt.(*ast.NonTransactionalDMLStmt); ok { rs, err session.HandleNonTransactionalDML(ctx, s, tc.Session) } else { rs, err tc.Session.ExecuteStmt(ctx, stmt) }HandleNonTransactionalDML入口实现是整条主链路先做预处理与约束检查再构造扫描分片键的 SELECT随后分批构建 job、逐条以ExecuteStmt执行拆分后的语句。如何找到 split keys为找到拆分边界split keys系统执行一条 SELECT 读取用户指定的分片列。设计文档以BATCH ON a LIMIT 1000 DELETE FROM t WHERE b 1000为例对应的扫描语句形如SELECT a FROM t WHERE b 1000 ORDER BY IF(ISNULL(a),0,1), a实际生成逻辑在 buildSelectSQL把原 WHERE 条件没有 WHERE 时写TRUE回填进SELECT并追加ORDER BY IF(ISNULL(col),0,1),col保证 NULL 值排在前面从而让 NULL 落在第一个 job 的范围内。处理流程如下结果集可能很大但不需要全量装载结果按行累积直到行数达到batchSize因拆分使用BETWEEN子句任意两个 batch 之间必须没有交集互斥——只有每个 batch 的首尾两个元素被保留下来构成一个 job一个 job 就代表分片列上的一个区间[start, end]。分批构造 job 的代码在 buildShardJobs其中有若干细节值得注意扫描阶段会临时清除SelectLimit否则sql_select_limit会截断分片键全集并把MaxExecutionTime置 0执行后恢复原值内存方面使用独立的LabelForNonTransactionalDMLtracker 挂到会话内存跟踪器上上限借用MemQuotaQuery设计上可接受最多约 2 倍配额的开销代码中以 TODO 注明未来应选择更合适的配额job 列表构建完成后若DryRun DryRunSplitDml则只对第一个与最后一个 job生成 SQL 返回其余跳过见 runJobs 中的continue逻辑。为什么串行执行理论上 job 之间相互独立、可以并行但并行需要多个会话用户会话必须与客户端连接一对一绑定无法拆借内部会话不适合承载这类语句。因此设计上出于可维护性考量只用当前用户会话串行执行所有 job。这也意味着非事务性 DML 的核心收益是突破单事务大小限制而不是获得比单条语句更优的性能设计文档原话。拆分语句的生成BETWEEN 与 NULL 边界每个拆分语句通过把区间嵌入原 DML 的 WHERE 子句生成使用BETWEEN操作符唯一的例外是NULL 边界——此时BETWEEN会被替换为IS NULL或形如x IS NULL OR x a的条件。对应实现位于 doOneJob构造规则可以归纳为三种起止都非 NULL生成WHERE (col BETWEEN start AND end)start 为 NULL、end 非 NULL生成WHERE (col end) OR (col IS NULL)起止都为 NULL生成WHERE col IS NULL。随后把该条件与原 WHERE 条件做逻辑与构成完整拆分 SQL。例如原语句BATCH ON a LIMIT 1000 DELETE FROM t WHERE b 1000配合 job{start:1, end:1000}生成DELETE FROM t WHERE (a BETWEEN 1 AND 1000) AND b 1000最终 SQL 通过 ASTRestore重新序列化文本开启反引号、二元操作符两侧空格等格式标志再交给会话执行。注意拼接时通过SetWhereExpr动态改写的是 AST 的 WHERE 条件节点因此不会破坏原有表结构、索引等元信息——这正是与历史 batch-DML 最大的区别。错误处理与约束系统变量 tidb_nontransactional_ignore_error非事务性语句显然无法整体回滚。当某个拆分语句失败时系统变量tidb_nontransactional_ignore_error决定后续行为设计文档定义取值为0默认取消所有后续 job并立即返回错误取值为1继续执行直到所有 job 完成最终返回全部失败 job 的详情与错误信息。该变量定义在 tidb_vars.go是 Global 与 Session 双作用域布尔变量默认关闭DefTiDBBatchDMLIgnoreError false声明于 sysvar.go会话侧对应标志位NonTransactionalIgnoreErrorsession.go。执行循环中的判定见 runJobs当 job 失败且未开启 ignore 时立即抛出标准错误ErrNonTransactionalJobFailure错误码8143见 errcode.go消息模板位于 errname.go包含 job id、job 总数、区间与 SQL。若语句被用户中止如KILL则上报当前进度并返回已收集到的错误runJobs顶部通过select检查ctx.Done()按有无失败 job分别记录日志并返回ctx.Err()。第一个 job 失败即整体终止错误处理存在一条例外如果第一个 job 就失败会中止整个过程并返回错误。设计文档给出两点理由如果所有 job 注定无法成功例如缺乏权限最早失败是最优选择——避免无意义地空跑全部 job其语义上等价于整体回滚。对应代码见 runJobsif i 0 jobs[i].err ! nil时返回带Early return: error occurred in the first job. All jobs are canceled注解的错误。对 DML 的硬性约束为让语义足够简单、避免误用设计文档与实现共同施加了下列约束可对照 checkConstraint 与checkReadClauses/checkTableRef两个辅助函数仅支持 WHERE 子句ORDER BY与LIMIT会被忽略且不生效——实现上直接返回Non-transactional statements dont support limit / order by错误而不是静默忽略注意设计文档原文说忽略实现更严格地选择了报错仅限 auto-commit要求autocommit1且不在显式事务内否则报错不能与 batch-DML 混用当tidb_enable_batch_dml开启且设置了批量删除/插入时拒绝执行不能在弱一致读weak read consistency下运行不能在设置了tidb_snapshot时运行分片列必须建有索引实现会在 Schema 信息中检查该列是否是整数主键PKIsHandle或是某公开且非不可见索引的首列见 selectShardColumnByGivenName否则报shard column %s is not indexedUPDATE/INSERT 不能更新分片列本身checkUpdateShardColumn会检查所有赋值目标含多表场景下的别名解析命中即报shard column cannot be updated防止拆分区间在更新过程中失效多表语句仅允许带条件地从最左表取分片涉及多表 JOIN 时必须完整限定dbname.tablename.colname否则报错selectShardColumn中可见实现会实时记录三类 metricNonTransactionalDeleteCount/NonTransactionalUpdateCount/NonTransactionalInsertCount。分片列的自动选择规则短形式BATCH LIMIT ...的自动选列逻辑在 selectShardColumnAutomatically若表是整数主键PKIsHandle选主键列若表是聚簇索引common handle且主键为单列选该列若聚簇索引含多列无法自动选择返回错误要求用户显式指定否则退化为_tidb_rowidExtraHandleName。另外执行环境上还做了一层保护见 HandleNonTransactionalDML 开头非事务性 DML 是写操作临时清除ReadStaleness、关闭 BulkDML 模式并在语句标签上冠以NTDML-前缀以与其它 DML 区分函数返回前统一恢复原值。会话执行状态与隔离注意点因为非事务性 DML 是在当前用户会话中串行执行多条自动提交语句实操时需要留意两点会话语义执行期间会话上下文始终活跃可通过SHOW PROCESSLIST看到带/* job x/y */注释的当前拆分语句用于观察进度它不同于一个大事务每一条拆分语句独立提交因此一旦进程中途被杀已完成的 job 生效、未完成的 job 未执行——这正是设计文档场景测试中所说的用户中止后可根据错误或日志信息重试剩余部分。测试设计与仓库中的验证设计文档规划了四层测试仓库当前已落地与之对应的大量用例功能测试Functional Tests验证非事务性 DELETE 不多删不少删覆盖 int、varchar含新旧 collation、timestamp、double、decimal 等多种分片列类型以及不同 DELETE 写法DELETE FROM t与DELETE t FROM、表别名、列别名、含/不含 WHERE、含子查询的复杂 WHERE 等。集成测试主体在 tests/integrationtest/t/session/nontransactional.test对应的期望结果集在 nontransactional.result两者均超过 1600 行用例/结果覆盖正常与各种报错路径。错误传播测试第一个 job 注入错误即整体返回非首个 job 注入错误时收集全部错误并完成剩余 job未授予权限时语句失败。单元测试见 pkg/session/test/nontransactionaltest/nontransactional_test.go其中TestNonTransactionalDMLErrorMessage、TestNonTransactionalDMLSharding、TestNonTransactionalWithCheckConstraint、TestNonTransactionalDMLWorkWithForeignKey、TestNonTransactionalMetrics、TestNonTransactionalDmlIgnoreMaxExecutionTime分别覆盖错误消息、分片正确性、约束检查、外键共存、指标与执行超时豁免等场景。场景测试Scenario Tests模拟单条 DELETE 无法删除的海量数据场景——先 dry run 查看拆分结果再执行、中途中止后按错误/日志信息续跑、无错误时结果等价于单条 DELETE。兼容性测试Compatibility Tests理论上不影响周边工具但仍设计了对 BR 备份恢复、TiCDC 同步、tidb-binlog 同步的简单验证。基准测试Benchmark Tests分别针对唯一索引分片、_tidb_rowid/整型主键分片、聚簇索引分片三种形态与非事务性 DELETE 的普通单条 DELETE 对比。影响、风险与备选方案设计文档明确提示的风险在于任何影响排序稳定性的特性如 collation 变更、排序规则不稳定都可能使拆分过程出错进而导致 DELETE 漏删或多删。因此该特性对分片列扫描排序的正确性高度敏感这也是实现中把 NULL 显式排到首位、并按 collation 使用对应 collator 做比较buildShardJobs 中collate.GetCollator(shardColumnCollate)的原因。在方案调研部分设计文档对比了两个方向CockroachDB并未提供类似机制而是通过官方文档教用户自己写脚本手动拆分 SQL另一种备选是增强大事务能力但 CDC 等工具对大事务支持不佳当时不具备可行性。尚未解决的问题设计文档保留的开放式问题集中在用户界面体验上语法、错误消息与 dry run 结果的呈现方式仍有改进空间。从当前仓库实现看语法层面的DRY RUN QUERY/DRY RUN返回值已相对清晰但错误消息的聚合呈现大量 job 失败时先记日志、返回值截断到 500 字符内再提示more in logs见 buildExecuteResults等取舍仍值得后续迭代优化。小结非事务性 DML 用拆分区间 串行自动提交的思路在保留普通 SQL 完整语义的前提下绕开了 TiDB 单事务大小与工具兼容性的双重限制为海量数据清理/归档提供了一条可预测、可观测、可续跑的执行路径。使用时请牢记三条纪律分片列必须有索引、只在 auto-commit 下使用、理解其非原子性并在任务设计上支持断点重试。想进一步阅读设计原文可查看 docs/design/2022-03-25-non-transactional-dml.md想了解实现细节可深入 pkg/session/nontransactional.go 及其测试用例。【免费下载链接】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),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →