vitessio/vitess · error · VitessError
VT03006
VT03006
Error message
VT03006: column count does not match value count with the row
What it means
VT03006 is raised by insertSelectPlan in go/vt/vtgate/planbuilder/operators/insert.go:415 when an `INSERT ... SELECT` statement's SELECT produces a different number of columns than the INSERT's column list (adjusted for auto-increment handling, per the columnMismatch check). Each row the SELECT yields must supply exactly one value per insert column.
Source
Thrown at go/vt/vtgate/planbuilder/operators/insert.go:415
route.Source = insertRowsPlan(ctx, insOp, insStmt, rows)
case sqlparser.TableStatement:
op = insertSelectPlan(ctx, insOp, route, insStmt, rows)
}
if insStmt.Comments != nil {
op = newLockAndComment(op, insStmt.Comments, sqlparser.NoLock)
}
return op
}
func insertSelectPlan(
ctx *plancontext.PlanningContext,
insOp *Insert,
routeOp *Route,
ins *sqlparser.Insert,
sel sqlparser.TableStatement,
) *InsertSelection {
if columnMismatch(insOp.AutoIncrement, ins, sel) {
panic(vterrors.VT03006())
}
selOp, err := PlanQuery(ctx, sel)
if err != nil {
panic(err)
}
// output of the select plan will be used to insert rows into the table.
insertSelect := &InsertSelection{
binaryOperator: newBinaryOp(newLockAndComment(selOp, nil, sqlparser.ShareModeLock), routeOp),
}
// When the table we are inserting into also appears in the select, the
// streamed select holds shared locks (it runs with "lock in share mode")
// on ranges the insert writes to, which can block or deadlock the insert.
// This flag makes us read the full select result first, so the locks are
// released before the rows are inserted.
insertTbl := insOp.tableTarget()View on GitHub (pinned to 01a25a7d17)
Solutions
- Make the SELECT expression list match the INSERT column list count exactly
- Remove the auto-increment column from the INSERT column list and let Vitess generate it
- Run the SELECT alone and compare its column count against the INSERT's column list
Example fix
// before INSERT INTO t (id, name) SELECT pk, first, last FROM other; // after INSERT INTO t (id, name) SELECT pk, CONCAT(first, ' ', last) FROM other;
Defensive patterns
Strategy: validation
Validate before calling
// Verify SELECT projection count matches INSERT column count before sending
function checkInsertSelect(insertCols, selectProjections) {
if (insertCols.length !== selectProjections.length) {
throw new Error(`column count ${insertCols.length} != select count ${selectProjections.length}`);
}
} Type guard
function countsMatch(insertCols, selectCols) {
return Array.isArray(insertCols) && Array.isArray(selectCols) && insertCols.length === selectCols.length;
} Try / catch
try {
await vtgate.execute(insertSelectSql, args);
} catch (e) {
if (String(e).includes('VT03006')) {
console.error('Align the INSERT column list with the SELECT projection list');
}
throw e;
} Prevention
- Run the SELECT standalone and count its output columns during development
- Never list the auto-increment column in INSERT..SELECT column lists
- Review INSERT..SELECT column lists whenever either side of the query changes
When it happens
Trigger: Running INSERT INTO t (a, b) SELECT x, y, z FROM other — the select expression count differs from the insert column count; also triggered when auto-increment modification changes expectations but the select still mismatches.
Common situations: Hand-written INSERT..SELECT copy queries where the SELECT list was edited independently of the column list; adding an auto-inc column to the insert list without adjusting the SELECT; refactored ETL queries.
Related errors
- non-constant regexp
- error running vtexplain: %w
- in-memory row count exceeded allowed limit of %d
- err
- VT09015
AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01).
Data as JSON: /api/errors/1a7ba23490cdd8f1.
Report an issue: GitHub.