Skip to content
JZLeetCode
Go back

System Design - How TiDB's Selection Executor Filters Chunks

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

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.

References

  1. TiDB source: PhysicalSelection and its conditions, commit 328460a.
  2. TiDB source: executorBuilder.buildSelection.
  3. TiDB source: SelectionExec.Open, Next, and unBatchedNext.
  4. TiDB source: vectorized/row-based filter dispatch.
  5. TiDB docs: root versus coprocessor tasks.
Share this post on:

Previous Post
LeetCode 1696 Jump Game VI
Next Post
System Design - How TLS Certificate Revocation Reaches a Browser