Skip to content
JZLeetCode
Go back

System Design - How TiDB's Projection Executor Runs Expressions

Table of contents

Open Table of contents

A calculation between storage and the client

Suppose a shop asks for the price after an eight-percent markup:

SELECT product_id, price * 1.08 AS marked_up_price
FROM products;

Some operator must turn a child row (17, 50) into (17, 54). In a TiDB execution plan, a Projection produces output columns by evaluating expressions over the rows its child supplies. We will follow a Projection that stays in the TiDB server’s root task, not one pushed into TiKV or TiFlash. Run EXPLAIN for an actual query: both the operators and their task locations depend on the optimizer’s decisions. PingCAP’s execution-plan guide shows how to read that task column.

This walkthrough is anchored to pingcap/tidb commit 88d75cb, rather than assuming today’s source matches every deployed TiDB release. The focus is four Go types: ProjectionExec, projectionInputFetcher, projectionWorker, and expression.EvaluatorSuite.

Meet the four pieces

       physical plan: PhysicalProjection (expressions)
                            |
                            v
                executorBuilder.buildProjection
                            |
                            v
       parent <---- ProjectionExec.Next <---- results
                            |                    ^
                            |                    |
                            v                    |
               projectionInputFetcher ----> projectionWorker(s)
                     |                          |
                     v                          v
                 child.Next             expression.EvaluatorSuite
                     |                     (evaluate columns)
                     +---- input Chunks ---------+

The diagram names the plan and builder as entry points, not additional worker classes to learn. A Chunk is a batch of rows; a parent repeatedly calls Next to request a batch. A Projection changes the values and shape of those rows; it does not, by itself, decide which rows survive a WHERE predicate.

The builder turns the PhysicalProjection.Exprs into an EvaluatorSuite and a ProjectionExec. It also passes along the child executor and the desired projection-worker count. This is where the plan stops being a description and becomes something the executor can run.

First try the one-lane road

ProjectionExec.Open and Next set up memory accounting and choose between sequential and parallel execution. In the sequential path, each Next asks the child for one Chunk, returns immediately if the child is exhausted, and runs the evaluator over that Chunk into the parent’s output Chunk. A request for a certain number of rows is passed down to the child.

For our example, the suite treats product_id as a direct column and price * 1.08 as a computed expression. EvaluatorSuite.Run evaluates computed expressions before moving direct input columns into output positions. That order matters: column swapping may change the input Chunk, so computing from the original input afterward would be unsafe.

Child Chunk:    product_id=[17, 23]    price=[50, 75]
                        |
               compute price * 1.08
                        |
Output Chunk:   product_id=[17, 23]    marked_up_price=[54, 81]

The suite can evaluate a supported expression one column at a time in vectorized mode, or fall back to row-by-row evaluation. Vectorized expression evaluation is not the same switch as parallel projection: the former changes how expressions run inside a worker, while the latter controls how many Chunks can be processed by workers at once. The defaultEvaluator.run branch makes this distinction visible.

Then add workers without scrambling rows

When parallel execution is selected, ProjectionExec.prepare creates reusable input and output Chunks and starts one projectionInputFetcher plus the configured workers. The fetcher is the only goroutine calling the child executor. It obtains a free input buffer and output slot, hands the output slot to ProjectionExec.Next, fills the input Chunk from the child, then sends both buffers to a worker. The worker calls EvaluatorSuite.Run, signals completion through the output slot’s done channel, and returns the input buffer for reuse.

Time          fetcher                 worker A          worker B        parent Next
  |    fetch chunk 1, queue slot 1       |                 |                |
  |    fetch chunk 2, queue slot 2       |                 |                |
  |                   |              compute 1         compute 2            |
  |                   |                 |              finish 2            |
  |                   |              finish 1             |                |
  v                   |                 |                 |          return slot 1,
                                                                        then slot 2

Worker B may finish first, but the fetcher puts output slots on the parent’s queue in fetch order. ProjectionExec.parallelExecute takes the next slot, waits for that slot’s done signal, swaps its columns into the parent’s Chunk, and recycles it. It does not return worker B’s later Chunk ahead of worker A’s earlier Chunk. When the child is exhausted, the fetcher signals the queued output and closes the output channel; Close stops workers, waits for them, and drains reusable buffers.

Why the concurrency knob may have no effect

The builder and Open have deliberate sequential fallbacks:

These rules are in newProjectionExec and ProjectionExec.open. Consult PingCAP’s system-variable reference before tuning: tidb_projection_concurrency is deprecated in favor of tidb_executor_concurrency. Its default -1 inherits the latter, while 0 selects the sequential path. tidb_max_chunk_size defaults to 1024 rows and tidb_enable_vectorized_expression defaults to ON in that reference. Higher worker counts can also mean more simultaneously live Chunks and more query memory, not a free speedup.

The useful mental model is: one reader preserves the child’s order, several workers can compute concurrently, and the parent waits for results in order. If you see Projection under cop[tikv] or an MPP task in EXPLAIN, this particular TiDB-server ProjectionExec walkthrough is not the operator you are observing.

References

Share this post on:

Previous Post
System Design - How Sagas Coordinate Distributed Transactions
Next Post
LeetCode 2140 Solving Questions With Brainpower