Table of contents
Open Table of contents
Four rows arrive; one survives
Suppose a query needs to keep scores from 20 through 39:
SELECT score FROM attempts WHERE score >= 20 AND score < 40;
For illustration, its child executor hands TiDB the rows [NULL, 18, 25, 44]. The predicate keeps only 25: NULL does not satisfy a WHERE filter. Where does that decision actually happen? This walkthrough follows a selection that remains in the TiDB server’s root task, using pingcap/tidb commit 328460a. It is a source snapshot, not a promise that every version or SQL statement has the same plan.
Many simple predicates instead run in TiKV’s coprocessor. EXPLAIN distinguishes a Selection under cop[tikv] from one under root; a coprocessor selection does not use the TiDB-server SelectionExec studied here. See the execution-plan task overview before applying this walkthrough to a real query.
Four code units, one batch of rows
PhysicalSelection (Conditions)
|
v
executorBuilder.buildSelection
| creates
v
SelectionExec (TiDB root)
^ |
| child Chunk | evaluates predicates
| v
child executor expression.VectorizedFilter
| selected[] mask
v
output Chunk for caller
PhysicalSelection carries the planned Conditions. executorBuilder.buildSelection builds its child first, then creates SelectionExec with those conditions, the child executor, and an evaluation context. It does not eagerly materialize all filtered rows.
SelectionExec keeps a reusable child-result Chunk, a row iterator, and a selected boolean slice. A Chunk is a batch of rows held in columns; its IsFull checks whether it has reached its requested row count. The fourth unit, expression.VectorizedFilter, evaluates the conditions on the child chunk and returns the mask that tells the executor which rows to append.
Follow one Next call
During Open, the executor sets up its child chunk, iterator, and memory tracker. It also checks expression.Vectorizable(filters) to decide whether the batched path is safe; expressions with user-variable get/set side effects, for example, can require row-by-row evaluation. The check lives in chunk_executor.go.
On the usual batched path, SelectionExec.Next first empties the caller’s result chunk. When it has no unconsumed child rows, it calls the child’s Next for another chunk, asks VectorizedFilter for a mask, and walks the child rows. It skips false entries and appends true entries to the caller’s chunk:
child chunk: [ NULL, 18, 25, 44 ]
score >= 20 AND <40 [ false, false, true, false ]
caller receives: [ 25 ]
If the output chunk becomes full, Next returns without throwing away its position in the child chunk; the following call resumes there. If a child batch contains no matches, the loop can request another batch before returning any rows. If the child returns an empty chunk, it has no more rows to offer. This is demand-driven filtering, not a promise to stop reading storage as soon as one row matches.
There is a subtle distinction between batching rows and vectorizing expressions. Vectorizable(filters) chooses between the batched loop and unBatchedNext, which evaluates individual rows and returns after a selected row to preserve side-effect ordering. Even on the batched loop, VectorizedFilterConsiderNull can evaluate expressions row by row if vectorization is disabled or the expressions do not support it. A batched executor is therefore not proof that every predicate runs in vectorized code.
Two knobs worth recognizing
tidb_max_chunk_sizecontrols the session’s maximum chunk size.Nextresets its output chunk with that cap, andChunk.IsFulldetermines when to return a filled batch. Raising it can increase per-query memory usage; it cannot make a non-matching row pass the filter.tidb_enable_vectorized_expressionselects vectorized expression evaluation where the expressions support it. The session value is captured innewSelectionExecutorContextand used by the filter helper, not by the optimizer to guarantee a rootSelection.
Both variables are registered in TiDB’s system-variable source. Before tuning, check the version-specific system-variable reference and the actual EXPLAIN plan.
The mental model
The physical plan names the predicate. The builder wires that predicate to a child. SelectionExec pulls a child chunk, computes which rows pass, and returns only those rows while preserving its place across calls. Expression evaluation may be vectorized or row-based, and simple filters may run in TiKV instead. To understand where filtering costs occur, first locate the Selection task in EXPLAIN; then trace the appropriate executor rather than assuming that every WHERE clause invokes this one.