Query Processing Overview

Consider a five-line SQL query joining three tables with a filter and an ordering clause. A database engine must transform that declarative request into a sequence of low-level operations. These operations — scans, joins, sorts — actually touch disk pages and CPU registers. The gap between "what the user asked for" and "how the machine computes it" is enormous. Bridging that gap correctly and efficiently is the job of query processing. This matters because the same SQL statement can be executed in wildly different ways. Some take milliseconds and others take hours over the same data. A system that ignores this gap either produces wrong answers. Or it produces right answers too slowly to be useful. Every production database runs some version of the pipeline described here. This includes an embedded SQLite instance in a mobile app. It also includes a distributed warehouse serving petabyte-scale analytics. The pipeline steps are: parse the query, translate it into an algebraic form, generate candidate execution plans, pick one, and execute it. The big picture is a pipeline with four conceptual stages. These are parsing and validation, translation to relational algebra, plan generation and selection, and plan execution. Each stage has a precise job. Parsing checks that the query is syntactically legal. It also checks the query is semantically meaningful against the schema. Translation converts the query into an internal, formally defined representation. This is usually an extended relational algebra expression. It has no ambiguity about what result is required. Plan generation enumerates different physical strategies for computing that algebra expression. This includes which join algorithm, which access path, and which order. It is guided by Indexing Overview for Query Access and statistics about the data. Execution walks the chosen plan, typically as a tree of iterators. It streams tuples from leaves to root. This article gives you the full skeleton of that pipeline. It shows how a query string becomes a parse tree. It shows how the parse tree becomes a relational algebra expression. It shows how algebra expressions become annotated evaluation plans. It also shows how the execution engine runs those plans. This uses the classic iterator (open/next/close) model. It sets up, but deliberately does not fully solve, harder questions. These are Cost Estimation for Query Plans and Query Optimization Fundamentals. These build directly on the representations introduced here.

Key takeaways
  • Query processing turns declarative SQL into an executable sequence of physical operations.
  • The pipeline has four stages: parse → translate to relational algebra → generate/choose a plan → execute.
  • Relational algebra is the pivot representation: one query can map to many equivalent algebra expressions.
  • Evaluation plans annotate algebra operators with concrete algorithms (e.g., hash join vs. nested-loop join).
  • The iterator model (`open`, `next`, `close`) lets operators be composed into pipelines that stream tuples.
  • Choosing among equivalent plans is a search problem solved later by cost-based Query Optimization Fundamentals.
The core insight

A SQL query has many equivalent relational-algebra forms and many possible execution plans; query processing is the disciplined pipeline that picks and runs one of them correctly and efficiently.

The idea

Query processing is the sequence of activities involved in extracting data from a database: parsing and translating a query into an internal form, choosing an efficient execution strategy, and evaluating that strategy against the stored data. It is distinct from query optimization, which is specifically the sub-problem of choosing which plan to execute among the many algebraically equivalent options; query processing is the umbrella term for the whole pipeline that optimization lives inside. The central design idea is separation of concerns via an intermediate representation. SQL text is convenient for humans but ambiguous and verbose as a computational object — the same result can be phrased in countless syntactic ways. The engine instead translates SQL into relational algebra, a small, closed set of operators (selection σ, projection π, join ⋈, union ∪, set difference −, rename ρ, and grouping/aggregation extensions) with precise, composable semantics. Because relational algebra is a formal language with algebraic identities (e.g., selections commute, joins are associative and commutative up to renaming), the engine can rewrite one algebra expression into an equivalent but cheaper one — this rewriting is exactly what Query Optimization Fundamentals studies. Query processing supplies the representation that makes such rewriting possible in the first place. A second key idea is the distinction between a logical query plan (a relational algebra expression — what to compute) and a physical/evaluation query plan (an algebra tree annotated with specific algorithms and access paths — how to compute it). The same logical join can be realized physically as a nested-loop join, a sort-merge join, or a hash join; the same logical selection can be realized as a full table scan or an index lookup via Indexing Overview for Query Access. Query processing is precisely the machinery that produces, compares, and executes these physical plans.

How it works

The pipeline below is the standard architecture used by essentially every relational engine, from teaching systems to production servers.

Stage 1 — Lexing and parsing. The SQL text is tokenized (keywords, identifiers, literals, operators) and parsed against the SQL grammar into a parse tree (or abstract syntax tree). A recursive-descent or table-driven parser checks syntax: are the keywords in the right places, are parentheses balanced, is the `FROM` clause well-formed. Purely syntactic errors (a missing comma, an unmatched `SELECT`) are caught here.

Stage 2 — Semantic validation and translation to relational algebra. The parse tree is walked against the catalog (schema metadata): do the referenced relations and attributes exist, are the types compatible, is a `JOIN ... ON` predicate well-typed, is a view expandable in place. Once validated, the tree is translated into an initial relational algebra expression — this translation is mechanical and follows the syntactic structure of the query almost directly (a `WHERE` clause becomes a σ, a `SELECT` list becomes a π, a `JOIN` becomes a ⋈, `GROUP BY`/aggregate functions become a grouping operator, `ORDER BY` becomes a sort operator, and a nested subquery may become a further algebra sub-tree or be rewritten away entirely). Views referenced in the `FROM` clause are expanded into their defining queries at this point (view expansion), so downstream stages never need to know a view was involved — this is the connective tissue back to Views in SQL.

Stage 3 — Logical and physical plan generation. The initial algebra expression, usually built in a "canonical" but naive shape (e.g., selections and projections deferred to the very end, joins in `FROM`-clause order), is transformed using algebraic equivalence rules into alternative logical expressions. Each logical expression is then annotated with concrete algorithms and access paths to produce candidate physical plans. For example, a logical σ over relation R can become "scan R sequentially, filter in memory" or "use a B⁺-tree index on the filtered attribute." A logical join can become nested-loop, block nested-loop, sort-merge, or hash join. This enumeration-and-annotation process is where Query Optimization Fundamentals and Cost Estimation for Query Plans do their work — this article only describes that plans are enumerated, annotated, and one is chosen; the search strategy and cost model are separate topics.

Stage 4 — Plan selection. Among the candidate physical plans, the optimizer picks the one with the lowest estimated cost (I/O, CPU, sometimes network in distributed settings), using statistics maintained about relation sizes, value distributions, and index selectivity.

Stage 5 — Execution. The chosen physical plan is a tree of physical operators. The iterator model (also called the Volcano or pull model) executes it: every operator implements three methods — ``` open() — initialize operator state, recursively open() children next() — produce (and return) the next output tuple, pulling from children's next() as needed; return end-of-stream when exhausted close() — release resources, recursively close() children ``` The root operator's `next()` is called repeatedly by the client (or by the code producing the final result set) until end-of-stream, and each call may cascade into calls on children, forming a demand-driven pipeline. This lets operators be composed uniformly regardless of their internal algorithm, and lets tuples flow through the whole tree without materializing every intermediate result on disk (pipelining), except where an operator is blocking (like sort or hash-build) and must consume all input before producing any output. ``` // Generic evaluation-plan execution driver function EXECUTE_PLAN(rootOperator): rootOperator.open() results = [] tuple = rootOperator.next() while tuple != END_OF_STREAM: results.append(tuple) tuple = rootOperator.next() rootOperator.close() return results // Example physical operator: file scan class FileScan implements Operator: function open(): this.cursor = storage.openScan(this.relationName) function next(): record = this.cursor.getNextRecord() if record == null: return END_OF_STREAM return record function close(): storage.closeScan(this.cursor) // Example physical operator: selection (filter), pipelined class Select implements Operator: // predicate: a function tuple -> boolean function open(): this.child.open() function next(): loop: t = this.child.next() if t == END_OF_STREAM: return END_OF_STREAM if predicate(t): return t // else loop and pull the next child tuple function close(): this.child.close() // Example physical operator: block nested-loop join (blocking on the outer's current block) class BlockNestedLoopJoin implements Operator: function open(): this.outer.open() this.inner.open() this.outerBlock = [] this.innerBuffered = [] // reloaded from scratch per outer block this.fillOuterBlock() function fillOuterBlock(): this.outerBlock = readUpTo(this.outer, BLOCK_SIZE_TUPLES) this.inner.close(); this.inner.open() // rescan inner this.matchQueue = [] function next(): loop: while this.matchQueue is empty: if this.outerBlock is empty: return END_OF_STREAM innerTuple = this.inner.next() if innerTuple == END_OF_STREAM: this.fillOuterBlock() if this.outerBlock is empty: return END_OF_STREAM continue for outerTuple in this.outerBlock: if joinCondition(outerTuple, innerTuple): this.matchQueue.push(combine(outerTuple, innerTuple)) return this.matchQueue.pop() function close(): this.outer.close() this.inner.close() ``` The `Select` operator above is a pipelined (non-blocking) operator: it produces output as soon as it sees a matching tuple. The join operator is partially blocking: it must buffer an entire block of the outer relation before it can start matching, but it does not need the whole relation. A full hash join build phase or an external merge sort is fully blocking: no output tuple can be produced until all input has been consumed. Recognizing which operators block matters directly for pipelining and for Cost Estimation for Query Plans, since blocking operators force materialization and add distinct I/O cost terms.

Figure
From SQL text to a running iterator tree
SQL query text SELECT … FROM … WHERE … Parse tree syntax + schema check (catalog lookup) Relational algebra π,σ,⋈,ρ,∪,−,γ tree the logical plan Candidate physical plans algorithm + access-path choices (cost-estimated, one selected) → Query Optimization Fundamentals Execution engine: iterator tree π (project) σ (select) ⋈ hash join Index scan Orders(customer_id) File scan Customers open() → next() → next() … → close() tuples pulled top-down, pushed bottom-up

Complexity & correctness

Parsing. Standard SQL grammars are context-free (with some context-sensitive checks like type resolution deferred to a separate semantic pass). A well-designed grammar can be parsed in O(n) time and space for a query of length n using a deterministic (LL/LALR) parser, since SQL's grammar is engineered to avoid the super-linear behavior of ambiguous grammars. Semantic validation against the catalog (resolving each identifier to a relation/attribute) costs O(n · log m) or O(n) amortized, where m is the number of catalog objects, assuming a hashed or tree-indexed catalog lookup — this is negligible next to actual data processing cost.

Translation to algebra. This is a single structural recursion over the parse tree, so it is O(n) in the size of the parse tree. Correctness here means semantic equivalence: the resulting algebra expression must denote exactly the same set (or bag, for SQL's multiset semantics) of result tuples as the original SQL query for every database instance. This is established by defining the translation compositionally — each SQL clause maps to an algebra operator whose formal semantics (given in terms of relations as sets/bags of tuples) is proven to match the SQL standard's clause semantics. For example, `SELECT DISTINCT A FROM R WHERE P` translates to π_A(σ_P(R)) with duplicate elimination, and one checks that both sides produce, for every instance of R, the identical distinct-tuple set.

Plan generation and rewriting. The number of syntactically distinct join orders for k relations is (2k−2)!/(k−1)! for left-deep trees alone (accounting for choice of join method separately), and grows even faster if bushy trees are allowed — this combinatorial explosion is precisely why Query Optimization Fundamentals needs dynamic programming or heuristics rather than brute force, and why this stage is described only structurally here. Correctness of plan generation means every candidate plan is algebraically equivalent to the original expression: rewriting rules (selection pushdown, join commutativity/associativity, projection pushdown) are each proven correct as algebra identities independent of the actual data, so any composition of valid rules preserves the final result.

Execution (iterator model). For an operator tree with k operators where operator i does O(f_i) work per `next()` call in the worst case, and the tree produces r output tuples total, the total work is bounded by the sum, over all operators, of (number of times `next()` is called on that operator) × (cost per call). For purely pipelined trees (scans, selections, projections, pipelined nested-loop-style joins) this yields time linear in the total number of tuples read across all base relations, i.e. O(N) where N is total input size, plus O(r) for output — but for blocking operators such as sort (O(N log N) time, O(N) extra I/O in the external case) or hash-join build phases (O(N) average time, quadratic worst case under adversarial hash collisions), the tree's overall complexity is the sum of each operator's own algorithmic cost, not simply linear in N. This is exactly why algorithm choice at the physical-plan stage matters so much, and why Cost Estimation for Query Plans must model I/O (page reads/writes) separately from CPU tuple-processing cost — disk I/O typically dominates for data sets larger than main memory.

Correctness invariant for the iterator protocol. Every operator must satisfy: (1) `open()` must be called exactly once before any `next()` call and establishes the operator's internal state as "ready, zero tuples emitted yet"; (2) each `next()` call either returns exactly one new output tuple consistent with the operator's semantics given its children's outputs so far, or returns end-of-stream, and once end-of-stream is returned the operator must continue to return end-of-stream on subsequent calls (no false restarts); (3) `close()` releases all resources and may not be followed by further `next()` calls. If every operator in a tree satisfies this contract, a simple induction over tree height shows the whole tree satisfies it too: the base case is a leaf scan operator (trivially satisfies the contract by construction over the stored file), and the inductive step holds because each interior operator's `next()` is defined purely in terms of calling its children's `next()` according to its own algebra semantics (e.g., `Select.next()` returns the first child tuple satisfying the predicate, which is exactly the set semantics of σ). This is why the iterator model composes safely: any tree built from contract-respecting operators is itself contract-respecting and computes the algebra expression it represents.

Tradeoffs. Pipelining minimizes memory and lets results start streaming to the client immediately, but it forces algorithm choices (e.g., you cannot use a plain in-memory hash join if the build side does not fit in memory) and complicates operators like sort that inherently need to see all their input. Materializing intermediate results (writing them to temporary storage) simplifies operator implementation and permits re-scans, but costs extra I/O. Real engines mix both: blocking operators materialize just enough (a hash table, a run of sorted tuples) while everything else stays pipelined.

Worked example

Consider the query, over a schema `Orders(order_id, customer_id, amount, order_date)` and `Customers(customer_id, name, city)`: ```sql SELECT c.name, o.amount FROM Orders o JOIN Customers c ON o.customer_id = c.customer_id WHERE o.amount > 500 ORDER BY o.amount DESC; ```

Step 1 — Parse. The parser confirms the grammar (`SELECT-FROM-WHERE-ORDER BY` shape) is well-formed, that `o` and `c` are valid table aliases, and that `o.customer_id`, `c.customer_id`, `o.amount`, `c.name` all resolve to real columns in the catalog with compatible types for `=` and `>`.

Step 2 — Translate to relational algebra. The canonical (unoptimized) translation nests operators exactly following clause order: ``` τ_{amount DESC} ( π_{name, amount} ( σ_{amount > 500} ( Orders ⋈_{customer_id = customer_id} Customers ) ) ) ``` where τ denotes sort. Note this "obvious" translation joins the full Orders and Customers relations before filtering — correct, but potentially wasteful if Orders is large and the filter is selective.

Step 3 — Generate candidate plans. Using the selection-pushdown identity σ_P(R ⋈ S) ≡ σ_P(R) ⋈ S (valid because P only references attributes of R), the optimizer can produce an equivalent, cheaper logical plan: ``` τ_{amount DESC} ( π_{name, amount} ( σ_{amount > 500}(Orders) ⋈_{customer_id = customer_id} Customers ) ) ``` Two physical plans might then be considered for the σ over Orders — a full scan with an in-memory filter, or an index scan using a B⁺-tree on `amount` (see Indexing Overview for Query Access) — and two physical algorithms for the join, say hash join versus sort-merge join. Suppose the catalog statistics say Orders has 1,000,000 rows with roughly 8% satisfying `amount > 500`, and Customers has 10,000 rows (small enough to build an in-memory hash table).

CandidateAccess path on OrdersJoin algorithmRough cost driver
Plan AFull scan + filterHash join (build on Customers)Reads all 1,000,000 Orders pages once; builds hash table over 10,000 Customers
Plan BIndex scan on `amount`Hash join (build on Customers)Reads only ~80,000 matching Orders rows via index, avoiding a full scan
Plan CFull scan + filterSort-merge joinRequires sorting both filtered Orders and Customers on `customer_id`

A cost-based optimizer (mechanics covered in Cost Estimation for Query Plans) estimates I/O and CPU cost for each and picks the cheapest — here likely Plan B, since the selective filter combined with an index avoids touching 92% of Orders pages.

Step 4 — Execute. Suppose Plan B is chosen. The operator tree is: ``` Sort(amount DESC) └─ Project(name, amount) └─ HashJoin(Orders.customer_id = Customers.customer_id) ├─ IndexScan(Orders, amount > 500) └─ FileScan(Customers) ``` Execution trace (abridged, showing the hash join's two phases): 1. `HashJoin.open()` calls `FileScan(Customers).open()` and builds an in-memory hash table keyed on `customer_id` over all 10,000 Customers rows — this is the build phase, fully blocking. 2. `HashJoin.open()` calls `IndexScan(Orders, amount > 500).open()`, positioning a cursor at the first index entry satisfying the predicate. 3. The `Sort` operator, being blocking, must pull every tuple from `Project` before it can emit anything: it repeatedly calls `Project.next()`, which calls `HashJoin.next()`, which pulls one Orders tuple via `IndexScan.next()` and probes the Customers hash table for a match — the probe phase. 4. Each matched (Orders, Customers) pair flows up through `Project` (dropping unneeded columns down to `name, amount`) and is buffered by `Sort`. 5. Once `IndexScan.next()` signals end-of-stream (no more index entries satisfy the range predicate), `Sort` has all ~80,000 filtered/joined tuples buffered, sorts them by `amount` descending (external sort if they do not fit in memory — see the sort operator's own O(N log N) analysis above), and only then starts returning tuples one at a time via its own `next()`. The final answer streams out of the root `Sort.next()` calls in descending `amount` order, and by the compositional correctness argument above, this output is guaranteed to equal exactly π_{name,amount} applied to the σ-filtered ⋈ of Orders and Customers, sorted — i.e., exactly the semantics of the original SQL query.

Common pitfalls & traps

PitfallWhy it happensConsequence
Assuming SQL execution order matches clause order`SELECT`/`FROM`/`WHERE` textual order is not evaluation orderMisjudging where filters actually get applied, misestimating cost
Forgetting NULLs and three-valued logic in translationσ predicates in SQL use `UNKNOWN`, not just true/falseTranslated algebra silently drops or keeps wrong rows
Confusing logical and physical plansBoth are "trees," easy to conflateBelieving a chosen join order is fixed when only the algorithm differs, or vice versa
Treating every operator as pipelinedOnly some operators are non-blockingUnderestimating memory needs, assuming results stream immediately when `Sort`/hash-build blocks
Ignoring view/subquery expansion effectsViews can hide expensive joins or duplicate predicatesAn innocuous-looking query over a view silently re-joins large tables every execution
Forgetting duplicate (bag) semanticsRelational algebra is often taught with set semantics`SELECT` without `DISTINCT` must preserve duplicates; naive set-based translation gives wrong counts

Pioneers & history

Origins · 1 minEdgar F. Codd and how this was discovered
EF
Edgar F. Codd
1923–2003 · IBM San Jose Research Laboratory, birth of the relational model

Codd's 1970 paper proposing the relational model gave databases their algebraic foundation: relations as sets of tuples, and a small closed algebra (selection, projection, join, union, etc.) sufficient to express any query expressible in relational calculus. Query processing as a discipline exists because Codd first insisted that what a query means (its algebra) could be separated cleanly from how it is computed — the foundational idea this article builds every stage around.

History
1970
— Codd publishes the relational model and relational algebra.
1974
— IBM's System R project begins, building the first cost-based query optimizer and the SEQUEL/SQL language.
1979
— Selinger et al. (System R) publish dynamic-programming join-order optimization, the ancestor of nearly all modern optimizers.
1986
— SQL becomes an ANSI standard, cementing the parse-translate-optimize-execute pipeline as an industry norm.
1993
— Graefe's Volcano/iterator model paper formalizes the open/next/close execution protocol used across engines today.

Where this matters

Every relational database — PostgreSQL, MySQL, Oracle, SQL Server, SQLite — implements exactly this pipeline internally: a parser/validator, a translator to an internal algebra or plan-tree IR, a planner that enumerates and costs alternatives, and an executor built on iterator-style physical operators (sometimes replaced or augmented by vectorized/columnar "batch" execution for analytics, but the logical stages remain the same). Distributed SQL engines (Spark SQL, Presto/Trino, Google BigQuery, Snowflake) extend this same pipeline with an extra stage that also decides data placement and shuffling across machines, but still bottom out in the same parse → algebra → physical-plan → execute skeleton. Application-facing tools such as ORMs and query builders generate SQL that ultimately funnels through this exact pipeline, and understanding it is the prerequisite for reading `EXPLAIN`/`EXPLAIN ANALYZE` output, tuning slow queries, designing indexes intelligently using Indexing Overview for Query Access, and reasoning about when a view from Views in SQL will be expanded versus materialized. The topics that follow — Selection and Projection Evaluation, Cost Estimation for Query Plans, and Query Optimization Fundamentals — each drill into one stage of exactly this pipeline in depth.

Ask the tutor: How does the query optimizer decide which of several equivalent physical plans has the lowest cost?

What is the difference between query processing and query optimization?

Query processing is the entire pipeline: parsing, semantic validation, translation to relational algebra, plan generation, plan selection, and execution. Query optimization is specifically the sub-stage that searches among algebraically equivalent plans and estimates their costs to pick the cheapest one. Every optimizer is part of a query processor, but a query processor also includes parsing and the execution engine, which optimization does not cover. Thinking of optimization as one stage inside the larger pipeline avoids conflating "the query didn't run" (a processing/parsing failure) with "the query ran slowly" (an optimization failure).

Why do systems bother with an intermediate relational algebra representation instead of executing SQL directly?

SQL syntax is flexible and redundant: many different-looking queries denote the same result. Relational algebra strips away syntactic variation and expresses the query as a small tree of well-defined operators with known algebraic identities. This lets the optimizer apply proven equivalence-preserving rewrites (like pushing selections below joins) mechanically and safely, and lets the execution engine be built against a small, stable set of operator types rather than the sprawling SQL grammar. It is the same reason compilers use an intermediate representation between source code and machine code.

What exactly does the iterator model buy us over evaluating one operator fully before starting the next?

The iterator model allows pipelining: output can start flowing before all input has been consumed, which saves memory (no need to materialize every intermediate table) and can return the first rows to a client quickly. It also gives a uniform interface (open/next/close) so any operator can be a child of any other, letting the optimizer freely compose different algorithms into a tree without special-casing their interaction. The tradeoff is that some operators (sort, hash-build) cannot be pipelined at all and must block, which the model accommodates but does not eliminate.

Where does cost estimation fit into this pipeline, and why isn't it covered here in full?

Cost estimation happens during plan generation and selection: for each candidate physical plan, the optimizer estimates I/O, CPU, and sometimes memory cost using statistics about relation sizes and value distributions. This overview describes that a choice is made among candidate plans, but the statistical models, selectivity formulas, and histogram techniques used to actually compute those cost numbers are substantial enough to be their own topic, covered separately as Cost Estimation for Query Plans. Understanding the pipeline first makes that later material easier to place correctly.

How do indexes change which physical plans are even possible?

An index changes the available access paths for a relation: instead of only a full sequential scan, an indexed attribute allows direct lookup or range scan without touching every page. This shows up in plan generation as an extra candidate physical operator (an index scan) alongside the file scan, and it can also enable index-based join algorithms (like index nested-loop join) that would otherwise be far too slow. See Indexing Overview for Query Access for how these structures are built; here, the important point is only that their presence enlarges the candidate-plan search space that optimization later chooses from.

Do views get executed as a separate step, or folded into the main plan?

In most systems, a view referenced in a query is expanded in place during translation: the view's defining query is substituted as a sub-tree of relational algebra wherever the view name appears, so the optimizer sees one combined expression and can push filters or projections through the view boundary just as it would for a base table. Some systems instead materialize a view's results ahead of time (materialized views) and treat it like a stored table with its own statistics and indexes. Both approaches route through the same pipeline described here; only the moment of expansion versus scanning differs, as detailed further in Views in SQL.

Why can the same SQL query produce very different execution times on different databases, or even on the same database at different times?

Because the SQL text only fixes the query's meaning, not its execution strategy. Different systems (or the same system with different statistics, indexes, or configuration) can choose different physical plans — a different join order, a different join algorithm, a different access path — for the identical logical query, and these choices can differ in cost by orders of magnitude. Statistics going stale, indexes being added or dropped, and data growing over time all shift which plan the optimizer judges cheapest, which is why the same query can regress or improve in performance without any change to the SQL itself.

Before this

Where this leads