vitessio/vitess · error

VT09015

VT09015

Error message

VT09015

What it means

VT09015: unsupported subquery in this projection context. During projection building (after a route/aggregation has been collapsed), the planner walks each projected column and panics with VT09015 if it finds a *sqlparser.Subquery, because subqueries cannot be evaluated inside this operator's projection on the chosen route.

Source

Thrown at go/vt/vtgate/planbuilder/operators/horizon_expanding.go:366

			proj.addUnexploredExpr(org, expr)
		} else {
			proj.addSubqueryExpr(ctx, org, newExpr, subqs...)
		}
	}
	proj.Source = sqc.getRootOperator(src, nil)
	return proj
}

func newStarProjection(src Operator, qp *QueryProjection) *Projection {
	cols := make([]sqlparser.SelectExpr, 0, len(qp.SelectExprs))

	for _, expr := range qp.SelectExprs {
		_ = sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
			_, isSubQ := node.(*sqlparser.Subquery)
			if !isSubQ {
				return true, nil
			}
			panic(vterrors.VT09015())
		}, expr.Col)
		cols = append(cols, expr.Col)
	}

	return &Projection{
		unaryOperator: newUnaryOp(src),
		Columns:       StarProjections(cols),
	}
}

View on GitHub (pinned to 01a25a7d17)

Solutions

  1. Rewrite the subquery as a JOIN against a derived table: `SELECT d.m FROM t JOIN (SELECT MAX(x) AS m FROM t2) d`
  2. Move the subquery result into a derived table and reference its column in the projection
  3. Split into two queries executed by the application
  4. Check Vitess release notes — subquery support expands over time; upgrade may help

Example fix

// before
SELECT a, (SELECT MAX(x) FROM t2) AS m FROM t GROUP BY a;
// after
SELECT t.a, d.m FROM t JOIN (SELECT MAX(x) AS m FROM t2) AS d ON TRUE GROUP BY t.a, d.m;
Defensive patterns

Strategy: validation

Validate before calling

// Reject scalar subqueries in SELECT list before sending to Vitess
func selectContainsSubquery(stmt sqlparser.Statement) bool {
  found := false
  _ = sqlparser.Walk(func(n sqlparser.SQLNode) (bool, error) {
    if _, ok := n.(*sqlparser.Subquery); ok { found = true; return false, nil }
    return true, nil
  }, stmt)
  return found
}

Type guard

_, isSubquery := node.(*sqlparser.Subquery) // isSubquery true => unsupported here

Try / catch

err := exec(q)
if err != nil && strings.Contains(err.Error(), "VT09015") {
  // rewrite query to use JOIN against derived table instead
}

Prevention

When it happens

Trigger: A SELECT list (of a collapsed/merged query, e.g. inside an aggregation or post-processing projection) contains a scalar subquery expression, e.g. `SELECT (SELECT MAX(x) FROM t2) FROM t GROUP BY a`.

Common situations: Queries ported from application code that relies on scalar subqueries in the SELECT list combined with GROUP BY or other constructs that force them into the internal projection operator.

Related errors


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