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

  1. Make the SELECT expression list match the INSERT column list count exactly
  2. Remove the auto-increment column from the INSERT column list and let Vitess generate it
  3. 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

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


AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01). Data as JSON: /api/errors/1a7ba23490cdd8f1. Report an issue: GitHub.