The same amount of data is scanned by two queries, yet one starts returning rows almost immediately while the other returns nothing for several seconds and then delivers everything at once. What in the execution plan explains the difference?
answer
- first row early = fully pipelined path
- one breaker gates the whole query's first row
- limit helps streaming plans, not sorts
- index-supplied order deletes the sort
- first-row cost vs total cost in the optimizer
basics
~20 sA fully pipelined plan passes each row up to the client as it is produced, so the first row arrives early. If the plan contains a blocking operator — a sort, a hash build, a grouping step — nothing can be emitted until that operator has read all its input, so results appear only at the end.
solid answer
~60 sIt is pipelining versus blocking. In a **pipelined** plan every operator can emit output after seeing one input row: scan → filter → project → client. The first result row travels the whole plan while the scan is still on the second page, so the client sees output almost immediately, and if the client stops early the engine stops working. In a plan containing a **blocking (pipeline-breaking)** operator — a sort, a hash-join build, a hash aggregation, DISTINCT — that operator must consume its entire input before it knows its first output row. Everything above it waits, so the query looks frozen and then floods. This is why engines and cost models distinguish **first-row cost** from **total cost**: an optimizer told that only a few rows are wanted may pick an index scan that streams in order rather than a cheaper-overall scan-plus-sort. When latency to the first row matters, look for breakers in the plan and try to remove them, typically by providing an index that already supplies the required ordering or grouping.
go deeper
Say clearly that a plan with no blocking operator streams rows immediately, while a sort or grouping step forces the engine to read everything first.
Add the row-limit consequence and the index-supplied-ordering fix, and name the common breakers.
Bring in first-row versus total cost, cursor and interactive-fetch behaviour, top-N sorts, and how to measure time-to-first-row in production.
Treat it as a latency-versus-throughput contract with the optimizer and design schemas, indexes and API paging so the hot interactive paths stay pipelined.
## Two ways a plan can behave in time A plan is a tree of operators. Rows flow from the scans at the leaves up to the client at the root. Whether the client sees rows early depends entirely on whether anything in that path has to wait for its whole input. **Pipelined behaviour.** Operators like filters, projections, index lookups and limits are *streaming*: give them one row and they can immediately produce zero or one output row. In a plan built only from these, row 1 is delivered to the client after roughly one row's worth of work. Total time to the last row may be large, but the *first* row is nearly free. **Blocking behaviour.** Some operators cannot produce anything correct until they have seen everything: - A **sort** cannot know which row comes first until it has seen the last one — the minimum might be the final row read. - A **hash aggregation** cannot finalize any group's total while more rows for that group might still arrive. - A **hash join's build side** must have a complete hash table before probing is correct. - **DISTINCT** implemented by hashing or sorting must see all rows to know what is unique. One of these anywhere below the root converts the whole query, from the client's point of view, from "streams" to "waits, then dumps". ## Why this shows up as a support ticket Users report it as inconsistent responsiveness: adding an ordering clause or a grouping clause to a previously snappy query makes it "hang". Nothing got slower in total; the *shape* of the time changed. Two rules of thumb follow: 1. **A row limit only helps a pipelined plan.** Limiting to ten rows above a streaming plan stops the scan after ten qualifying rows are found. Limiting to ten rows above a sort still reads and sorts everything — the limit only reduces what is emitted afterwards. (The common optimization, a top-N sort keeping a small heap, cuts memory dramatically but still consumes the whole input.) 2. **An index can delete the breaker.** If an index already returns rows in the requested order, the optimizer can drop the sort and walk the index, streaming rows and stopping after the tenth. Same for grouping when the input is already clustered on the grouping key: a streaming aggregate emits each group as soon as the key changes. ## First-row cost versus total cost Optimizers cost a plan twice: the cost to produce the first row and the cost to produce all rows. When the engine knows only a few rows are wanted — an explicit limit, a cursor that will be fetched from interactively, an optimizer hint or session setting for fast-first-row behaviour — it can prefer a plan with a worse total cost but a much better first-row cost. Typically that means an ordered index scan with a nested-loop join rather than a full scan with a hash join and a sort. If all rows will be consumed anyway (a batch export), the total-cost plan is the right one, and the blocking plan is frequently the faster of the two overall. ## What else pipelining buys - **Bounded memory.** A pipelined plan holds one row (or one batch) per operator, not the whole intermediate result, so it neither needs a large memory grant nor risks spilling to temporary files. - **Cheap abandonment.** Close the cursor after ten rows and the work below simply stops. - **Better perceived performance.** Interactive UIs can render the first page while the rest arrives. ## What pipelining does not buy It does not reduce total work, and it is not always achievable — sorting and grouping genuinely require seeing all input, and no plan shape changes that. It also does not help when the client can only use a complete result anyway (an aggregate returning one row, a report that must be sorted). ## How to investigate Read the plan from the bottom up and mark every sort, hash, aggregate and materialize node. The topmost such node is where your first row is gated. Then ask whether it can be removed (an index providing order or clustering), shrunk (top-N), or accepted (a true aggregate). Measuring only total query time hides this entirely — measure time-to-first-row separately when latency is what users feel.
- Does a row limit reduce the work a plan does?Only for the streaming part of it. Above a fully pipelined plan the limit stops execution once enough rows are produced, so scanning stops early. Above a blocking operator such as a sort or a hash aggregation, the input has already had to be fully consumed before any row could be emitted, so the limit only trims the output. Removing the breaker — usually with an index that already provides the order — is what turns the limit into real savings.
- How would you measure whether a query is pipelined?Measure time-to-first-row separately from total time, for example by timing the first fetch from a cursor rather than the full result. A pipelined plan returns row one in roughly the time to find one qualifying row; a blocked plan returns nothing until near the end. Reading the plan for sort, hash, aggregate and materialize nodes tells you the same thing statically.
Ordering at a coffee counter: a pipelined plan hands you each drink as it is made; a blocking plan makes every drink for the whole queue before serving anyone.
saying these in an interview costs you the question
- Blaming the network or client rendering when the plan clearly contains a sort or hash aggregate.
- Claiming a row limit always makes a query cheap.
- Thinking pipelining reduces total execution time — it changes when rows appear, not how much work is done.
- Believing every plan can be made pipelined; sorting and grouping fundamentally need all input.
- Judging responsiveness solely by total query duration and never measuring time-to-first-row.