In a vectorized engine, what is a selection vector and why keep one instead of compacting the batch after each filter?
answer
- the values never move, only the list of positions
- copying fifteen columns to filter one is the waste
- chained predicates narrow the same index array
- sparse access wastes whole cache lines
- past some threshold, densify again
basics
~20 sA selection vector is a small list of the positions in a batch that survived a filter. Carrying it lets later operators skip non-matching rows without physically copying every projected column, which would cost more than the filter itself.
solid answer
~50 sAfter a filter runs over a batch, the engine has a result bitmap of qualifying positions. It can either **compact** — copy the surviving values of every projected column into fresh, dense buffers — or keep a **selection vector**, a short array of surviving indices (or the bitmap itself) alongside the untouched column buffers. Compaction costs a copy of every column, including ones the next filter may throw away anyway; the selection vector costs nothing but an extra indirection when downstream operators iterate. So engines chain several filters against the same buffers, narrowing the selection each time, and materialize only for the columns that actually reach the output. The trade-off is that a very sparse selection wastes cache lines and SIMD lanes — you touch a whole line to read one value — so engines compact when selectivity drops below a threshold, and always before an operator that needs dense input.
code
text · 8 linesbatch (2048 positions), predicate: amount > 100
amount[] : 40 250 90 310 ...
mask[] : 0 1 0 1 ...
sel[] : [1, 3, ...] <- 312 of 2048 survive
next filter reads amount/status ONLY at positions in sel[]
payload columns are never copied until outputgo deeper
You are unlikely to be asked this. Knowing that a filter marks which rows in a batch survived, rather than deleting them, is already enough at this stage.
Be able to state the two options after a filter — copy the survivors into dense buffers, or pass a list of surviving positions — and give the memory-traffic reason the second is usually cheaper.
Show the trade-off in both directions: name why a very sparse selection hurts cache lines and SIMD lanes, when an engine densifies anyway, and how predicate ordering compounds with it.
Treat it as a design constraint on the operator API. Every kernel needing flat and selected paths is real engineering cost, and that tension is what shapes an engine's extension surface and its maintenance burden.
## The problem the selection vector solves A vectorized engine works on batches: a few thousand values per column, held in flat buffers. A filter kernel over one of those buffers produces a mask — one bit or byte per position saying whether that row qualified. The question is what to do next, because the *values themselves have not moved*, and other columns of the same batch have not even been looked at. There are two answers. **Compact (also called flatten or coalesce):** allocate fresh buffers and copy, for every column the query still needs, only the surviving values into dense positions 0..k-1. Downstream operators then see an ordinary dense batch of k rows and need no special handling. **Select:** leave every buffer exactly where it is and pass along a *selection vector* — typically an array of the surviving positions, e.g. `[1, 4, 6, 9, ...]`, or equivalently a bitmap. Every downstream operator learns to iterate `for (j = 0; j < sel_count; j++) { i = sel[j]; ... }` instead of `for (i = 0; i < batch_size; i++)`. ## Why selection usually wins The cost of compaction is proportional to *the number of columns carried*, not to the number of columns filtered. A query with a fifteen-column projection and three filters would, under eager compaction, copy fifteen columns three times — and the values copied in step one are largely thrown away in step two. That copying is pure memory traffic, exactly the resource a scan is already stressing. A selection vector turns a chain of predicates into successive narrowings of a tiny index array: 1. `country = 'DE'` scans the country vector, produces `sel` with 300 of 2048 positions. 2. `amount > 100` runs **only over those 300 positions** of the amount vector, producing a shorter `sel`. 3. `status = 'PAID'` narrows further. Only the survivors of the full chain are ever gathered out of the payload columns. This is the execution-layer sibling of the storage idea that you should touch the fewest bytes possible, and it interacts with predicate ordering: cheap and selective predicates first, expensive ones (string matching, arithmetic, function calls) last, evaluated over an already-narrow selection. ## Why selection sometimes loses The indirection is not free, and it has three specific costs: - **Wasted lanes.** A SIMD kernel loads sixteen adjacent values whether or not you want them all. Over a dense batch, all sixteen count. Over a selection of every ninth row, you pay the same load and use one lane. Below some selectivity the packed kernel effectively degrades to scalar speed. - **Wasted cache lines.** Reading position 9, then 400, then 1500 of a column touches three separate cache lines to retrieve three values. A dense batch of the same three values touches one. - **Operator complexity.** Every kernel needs two code paths — flat and selected — or must always pay the indirection. Real engines maintain both, which doubles the kernel surface. Because of this, engines apply a heuristic: keep the selection vector while the batch is still reasonably dense, and compact once the surviving fraction falls below a threshold, so that downstream work runs on dense data again. Certain operators force compaction regardless — anything that builds or probes a hash table, spills, writes out, or crosses a pipeline boundary generally wants dense input. ## Relationship to branch-free filtering The reason a mask exists at all is that vectorized filters are written *without branches*. Instead of `if (amount[i] > 100) emit(i);` — a data-dependent branch the CPU must predict, mispredicting roughly half the time on random data and flushing a deep pipeline each time — the kernel unconditionally computes `mask[i] = amount[i] > 100` for every position, then derives the selection from the mask. Computing results for rows that will be discarded is deliberately cheaper than mispredicting. This is the same reason both arms of a `CASE` may be evaluated for the whole batch and then blended by mask: predictable work beats unpredictable branching. A related detail: converting a mask to a compact index list is itself a vectorized operation on modern CPUs, which is what makes the whole scheme cheap. ## What an interviewer is checking They want to see that you understand execution as a memory-traffic problem, not just an instruction-count problem: the point of the selection vector is *not copying columns you may not need*, and the reason it is not universal is that sparse access wastes the very cache lines and lanes that vectorization exists to exploit. Naming the compaction threshold as a heuristic rather than a fixed rule, and connecting predicate ordering to it, is the senior-level version of the answer.
- When should an engine stop carrying a selection vector and physically compact the batch?When the surviving fraction gets low enough that indirection wastes more than copying saves — sparse reads touch a cache line per value and leave SIMD lanes idle — or when the next operator needs dense input, such as a hash-table build, a spill to disk, or the write-out at the end of a pipeline. Engines use a selectivity threshold rather than a fixed rule.
- How does predicate ordering interact with selection vectors?Strongly. Because each predicate runs only over the current selection, putting cheap, highly selective predicates first means expensive ones — string matching, arithmetic, function calls — evaluate over far fewer positions. Cost-based optimizers order predicates by estimated cost divided by selectivity for exactly this reason, and a bad estimate here shows up as CPU time in the filter, not as extra I/O.
saying these in an interview costs you the question
- Thinks the engine physically deletes filtered rows from the batch
- Assumes carrying a selection vector is always faster than compacting
- Confuses a selection vector with a storage-level index
- Says filters must branch per row to skip non-matching values
- Ignores that sparse access wastes cache lines and SIMD lanes