docs/agents/ddl/06-add-index.md
This doc focuses on ActionAddIndex (and closely related ActionAddPrimaryKey). Add index is special because it’s a reorg/backfill DDL: it needs a schema state machine + background data backfill while DML continues.
SQL → DDL module:
pkg/executor/ddl.go (type DDLExec, (*DDLExec).Next)pkg/ddl/executor.go:CreateIndex, pkg/ddl/executor.go:createIndexJob creation (what’s persisted):
pkg/ddl/executor.go:buildAddIndexJobWithoutTypeAndArgsjob.Version = model.GetJobVerInUse() (inside createIndex)job.Type = model.ActionAddIndex (inside createIndex)pkg/ddl/reorg_util.go:initJobReorgMetaFromVariablesmodel.ModifyIndexArgs with OpType = model.OpAddIndex (inside createIndex)Add index is a “normal” DDL action in terms of SQL surface, but the job args carry a lot of complexity:
pkg/ddl/executor.go:checkCreateGlobalIndexpkg/ddl/executor.go:CheckAndBuildIndexConditionStringpkg/ddl/index.go:onCreateIndex (restores ConditionExprString on the built IndexInfo)pkg/ddl/executor.go:checkIndexNameAndColumnspkg/ddl/index.go:moveAndUpdateHiddenColumnsToPublic (done when moving StateNone → StateDeleteOnly)SPLIT clause on index option):
pkg/ddl/executor.go:buildIndexPresplitOptpkg/ddl/index_presplit.go:preSplitIndexRegionsOwner scheduling + worker:
pkg/ddl/job_scheduler.go (routes mysql.tidb_ddl_job.reorg=1 jobs to reorgWorkerPool)pkg/ddl/job_worker.go (addIdxWorker)pkg/ddl/index.go:onCreateIndexThe reorg routing flag is decided at submission time:
pkg/ddl/job_submitter.go:insertDDLJobs2Table (jobW.MayNeedReorg() → mysql.tidb_ddl_job.reorg)The add-index job drives an index state and a corresponding job schema state. The core transitions happen in pkg/ddl/index.go:onCreateIndex:
StateNone → StateDeleteOnlyStateDeleteOnly → StateWriteOnlyStateWriteOnly → StateWriteReorganization (reorg/backfill)StateWriteReorganization → StatePublicThe job records the corresponding “schema state” on job.SchemaState (e.g. StateDeleteOnly, StateWriteOnly, StateWriteReorganization) so other parts of the framework can reason about compatibility windows.
At each boundary, the worker updates table metadata and schema version so all nodes observe the same compatibility window before the next step.
StateWriteReorganization is not “just backfill”. In pkg/ddl/index.go:onCreateIndex, the worker:
pkg/ddl/index.go:doReorgWorkForCreateIndexANALYZE (based on job vars and job context):
job.ReorgMeta.AnalyzeState (see pkg/ddl/index.go:onCreateIndex)pkg/ddl/index.go:startAnalyzeAndWaitOnly after analyze is done/skipped/timeout/failed does the worker finalize the index to StatePublic and finish the job.
Before the job is submitted, add index initializes job.ReorgMeta from global/session variables:
pkg/ddl/reorg_util.go:initJobReorgMetaFromVariablestidb_ddl_enable_fast_reorg (vardef.EnableFastReorg)tidb_enable_dist_task (vardef.EnableDistTask) — requires fast reorg; otherwise the job rejects (ErrUnsupportedDistTask)job.ReorgMeta also carries persisted reorg parameters (concurrency, batch size, max write speed) and (for dist-task) target scope / max node count.
The reorg/backfill implementation picks a reorg type once and persists it in job.ReorgMeta.ReorgTp:
pkg/ddl/index.go:pickBackfillTypeHigh level:
ReorgTypeTxn (traditional transactional backfill)ReorgTypeIngestReorgTypeTxnMerge (still a fast-reorg pipeline, without Lightning)The reorg loop itself is coordinated from pkg/ddl/index.go:doReorgWorkForCreateIndex.
Fast-reorg pipelines use a backfill-merge state machine persisted on the index (IndexInfo.BackfillState):
pkg/meta/model/reorg.go:BackfillStatepkg/ddl/index.go:doReorgWorkForCreateIndexStates:
BackfillStateRunning: backfill is running; writes/deletes are redirected to (or maintained in) a temporary index.BackfillStateReadyToMerge: temp index is ready; publish this state so all TiDB nodes start copying writes/deletes for merge safety.BackfillStateMerging: merge temp index records back to the origin index.BackfillStateInapplicable: exit the backfill-merge process (and prevent double-write on the temp path).This state machine exists to keep correctness under: cross-node schema propagation, online writes during backfill, retries, and owner transfer.
For resumability (owner transfer/retry), progress must be persisted. Add index uses:
mysql.tidb_ddl_job (job_meta, reorg, processing)mysql.tidb_ddl_reorg (range + element + reorg meta)
pkg/ddl/job_scheduler.go:getDDLReorgHandle, pkg/ddl/job_scheduler.go:initDDLReorgHandle, pkg/ddl/job_scheduler.go:deleteDDLReorgHandleIf you change what’s stored for reorg/backfill, ensure it’s:
Add index must be safe under partial failure and retries:
pkg/ddl/index.go (e.g. convertAddIdxJob2RollbackJob call sites).When adding new fast-reorg/ingest behavior, also verify rollback paths don’t leak temp index artifacts and that BackfillState transitions remain monotonic and persisted.
SQL:
ADMIN SHOW DDL JOBSADMIN SHOW DDL JOB QUERIESADMIN CANCEL DDL JOBS <job_id>Tables:
mysql.tidb_ddl_job (queue)mysql.tidb_ddl_history (history)mysql.tidb_ddl_reorg (reorg handle/progress)Code:
pkg/ddl/executor.go:createIndexpkg/ddl/index.go:onCreateIndexpkg/ddl/index.go:doReorgWorkForCreateIndexpkg/ddl/ingest/* (see also docs/design/2022-06-07-adding-index-acceleration.md)BackfillState not persisted → reorg restarts or double-writes after retry.pickBackfillType + checks in onCreateIndex / initForReorgIndexes).