CONCEPT Cited by 1 source
Offset planning¶
Definition¶
Offset planning is a named pipeline stage in the Vitess query planner introduced as part of the "new model" rewrite (canonicalised in Andrés Taylor's 2023-06-01 post). It sits between Horizon Planning and Executable Plan emission — that is, after all tree rewrites have converged to a fixed point and before the planner hands the tree to the execution engine.
From Taylor's canonical pipeline diagram (Source: sources/2026-04-21-planetscale-optimizing-query-planning-in-vitess-a-step-by-step-approach): "the New Process also has one new step, 'Offset Planning', that is between Horizon Planning and Executable Plan."
Why it exists (inferred)¶
The article names offset planning as a distinct stage but does not deeply specify what it does. The plausible interpretation — consistent with the name and with the planner's observable output — is that offset planning resolves symbolic column references to positional row offsets that the execution engine needs to index into materialised result rows.
During horizon planning and earlier stages, operators carry symbolic
expressions like u.baz or ue.bar — column references bound to the
logical table + column in the SQL AST. The execution engine, however, deals
in positional row offsets — at runtime, rows are flat tuples (byte
buffers), and reading u.baz means reading bytes [start..end] at offset
N in the materialised row. Offset planning is the stage that computes
those offsets, given the plan's materialisation contract.
Evidence for this interpretation from the worked example's final plan:
- LHS query:
SELECT u.foo, u.uid, u.baz, weight_string(u.baz) FROM user AS u ORDER BY u.baz ASC— the planner has addedu.uid+u.baz+weight_string(u.baz)columns beyond what the user's SELECT asked for (u.foo), because the VTGate-side nested-loop join needsu.uidas the bind variable for the RHS query and the VTGate-side ordering needsu.baz+ its weight string for collation-correct sorting. These "extra columns needed to execute the plan" are exactly the kind of thing offset planning would compute. - The
:u_uidplaceholder in the RHS query (WHERE ue.uid = :u_uid) is bound to a specific offset in the LHS result row at runtime. Offset planning is the natural place to resolve which offset.
Why offset planning is a separate stage, not part of horizon planning¶
The separation buys two properties:
-
Horizon planning stays about operator-tree rewrites, not row layout. The horizon-planning loop operates on symbolic plans — "push the Projection under the Route", "split the aggregator into local + global", etc. — without knowing the execution engine's row format. This keeps horizon planning portable across execution-engine evolutions.
-
Offset planning runs once, post-convergence. Once the tree has stabilised under horizon planning's fixed-point loop, the set of columns each Route must emit is fixed; the set of columns each VTGate-side operator consumes is fixed. Offset planning can compute the full column-flow map in one pass, without having to re-run on each rewriter iteration.
Relationship to the new-model pipeline¶
Parse → Determining Join Order → Horizon Planning ⟲ → Offset Planning → Executable Plan
↑
(fixed-point loop)
(Source: this article's pipeline diagram, verbatim.)
- Parse — SQL → AST.
- Determining Join Order — decide the order in which tables are joined.
- Horizon Planning — iteratively expand + push down Horizon operators (and all other operators in the planner's vocabulary) to a fixed point.
- Offset Planning — resolve symbolic column references to positional offsets; add any extra columns required by VTGate-side operators that aren't in the user's SELECT.
- Executable Plan — emit the final plan the execution engine runs.
What offset planning is NOT¶
- Not a rewriter. It doesn't reshape the operator tree; it annotates the tree with layout information.
- Not a pushdown pass. Pushdown happens inside horizon planning's fixed-point loop; by offset-planning time, all pushdown decisions are final.
- Not column-pruning. Some planners have a column-pruning pass that removes unused columns from Routes; that's a horizon-planning-stage rewrite if it exists, not offset planning. Offset planning is the opposite — it may add columns that the execution engine needs.
Not-fully-specified-here caveat¶
Taylor's 2023-06-01 post names offset planning as a new step but doesn't unpack its mechanism. The interpretation above (symbolic-ref → positional- offset resolution + VTGate-side-operator column requirements) is the most parsimonious reading consistent with the names + the final plan shapes + the execution engine's known row-flat materialisation. Future Vitess disclosures (likely via a dedicated planner post or the Vitess internals docs) will either confirm or refine this reading. This page is a placeholder that records the name + placement and flags the mechanism as not-deeply-specified by this source.
Relationship to¶
- concepts/vtgate-query-planner — parent planner; offset planning is one of its pipeline stages.
- concepts/horizon-operator — the prior-stage operator; horizon expansion is done by the time offset planning runs.
- concepts/runnable-plan-at-every-step — offset planning respects the invariant: the post-offset-planning tree is also a runnable plan (the pre-offset tree is symbolic but conceptually runnable via an interpreter that resolves references lazily).
- concepts/fixed-point-tree-rewriting — runs before offset planning, not during.
Seen in¶
- sources/2026-04-21-planetscale-optimizing-query-planning-in-vitess-a-step-by-step-approach — canonical source for the name + pipeline placement. Andrés Taylor's 2023-06-01 post introduces offset planning as "one new step, 'Offset Planning', that is between Horizon Planning and Executable Plan."