SECTION 1
Historical Context & Motivation
When Edgar F. Codd published his seminal 1970 paper on the relational model of data, he envisioned a world where users could describe what data they wanted rather than how to retrieve it. This declarative philosophy became the foundation of SQL (Structured Query Language), which was first implemented at IBM as SEQUEL in the mid-1970s. A critical implication of this declarative design is that the order in which a programmer writes SQL clauses is not the order in which the database engine evaluates them. The engine follows a well-defined logical processing order that determines which rows are considered, how they are grouped, and what ultimately appears in the result set.
1970 • Codd's Relational Model
1974 • SEQUEL at IBM
1986 • ANSI SQL Standard
1992 • SQL-92 & GROUP BY / HAVING
2003–Present • Window Functions & Modern SQL
The fundamental question this lesson addresses is deceptively simple: in what order does the database engine logically process the clauses of a SQL query? The syntactic order—SELECT first, then FROM, then WHERE—is a notational convention for human readability. The logical order starts with FROM, then WHERE, and evaluates SELECT near the end. Failing to understand this distinction leads to common errors: referencing column aliases in the WHERE clause, misusing aggregate functions, or being confused by the behavior of HAVING versus WHERE. Mastering the logical order transforms SQL from a trial-and-error exercise into a principled, predictable tool.
SECTION 2
Core Principles & Definitions
Before dissecting each phase, it is essential to establish the conceptual framework that governs SQL query evaluation. SQL is a declarative language, meaning you specify the desired result rather than the procedural steps to compute it. The database management system (DBMS) translates your declaration into a physical execution plan, but the logical processing order defines the conceptual sequence of operations that guarantees the result's correctness. Every SQL engine—PostgreSQL, MySQL, SQL Server, Oracle—must produce results consistent with this logical order, even though its optimizer may physically execute steps in a completely different sequence for performance.
Declarative vs. Procedural
Logical vs. Physical Order
Scope & Visibility
Set-Based Thinking
The Seven-Phase Pipeline
KEY TAKEAWAY
SECTION 3
Visual Explanation — The Logical Pipeline
The diagram above illustrates the central insight of this lesson. When you write a SQL query, you begin with the SELECT keyword, but the engine begins its logical evaluation with FROM. Each phase transforms an intermediate virtual table into a new virtual table, passing it down the pipeline. The FROM clause constructs the broadest possible set of candidate rows (the Cartesian product of all joined tables, refined by join conditions). The WHERE clause then eliminates rows that fail the filter predicate. GROUP BY collapses the surviving rows into groups, after which HAVING filters entire groups. Only then does SELECT evaluate expressions and assign aliases. Finally, ORDER BY sorts the result and LIMIT truncates it.
SECTION 4
Deep Dive — Each Phase Explained
Phase 1: FROM / JOIN
The FROM clause is the logical starting point of every query. When multiple tables are specified, the engine conceptually forms their Cartesian product—every combination of rows from each table. If table A has m rows and table B has n rows, the Cartesian product contains m × n rows. JOIN conditions (ON clauses) then filter this product to retain only meaningful row pairings. OUTER JOINs additionally preserve unmatched rows from one or both sides, padding missing columns with NULLs. The output of this phase is a single virtual table that contains every column from every source table.
Phase 2: WHERE
The WHERE clause applies a Boolean predicate to each individual row in the virtual table produced by FROM. Rows for which the predicate evaluates to TRUE survive; rows evaluating to FALSE or UNKNOWN (due to NULLs) are discarded. A critical rule is that aggregate functions cannot appear in WHERE because groups have not yet been formed. You cannot write WHERE COUNT(*) > 5 — the engine has no concept of groups at this stage. This restriction is a direct consequence of the logical order.
Phase 3: GROUP BY
The GROUP BY clause partitions the filtered rows into groups based on one or more grouping columns. Within each group, all rows share identical values for the grouping columns. Once grouping occurs, the virtual table transitions from a table of individual rows to a table of groups. From this point forward, any column referenced in SELECT or HAVING must either appear in the GROUP BY list or be wrapped in an aggregate function (SUM, COUNT, AVG, MAX, MIN, etc.). This constraint ensures that each expression produces exactly one value per group.
Phase 4: HAVING
The HAVING clause is the group-level counterpart of WHERE. It applies a Boolean predicate to each group, and groups that fail are eliminated. Unlike WHERE, HAVING can reference aggregate functions because groups already exist at this stage. For example, HAVING COUNT(*) > 5 retains only groups with more than five members. A common beginner mistake is using HAVING for row-level filters — this is semantically incorrect and often less efficient than using WHERE, because HAVING runs after grouping has already been performed.
Phase 5: SELECT
The SELECT clause is evaluated fifth, despite appearing first in the syntax. It determines which columns or expressions appear in the output. Column aliases are defined here via AS, which is precisely why those aliases are invisible to WHERE, GROUP BY, and HAVING—those clauses have already been evaluated. However, aliases defined in SELECT are visible to ORDER BY and LIMIT, which run later. If DISTINCT is specified, duplicate rows are eliminated at this stage.
Phase 6: ORDER BY
The ORDER BY clause sorts the result set. Because it executes after SELECT, it can reference column aliases and ordinal positions (e.g., ORDER BY 2 refers to the second column in the SELECT list). ORDER BY is the only clause that can reference columns not in the SELECT list (in most SQL dialects), because the full virtual table is still available from earlier phases. Without ORDER BY, SQL makes no guarantees about row order; the result is technically an unordered set.
Phase 7: LIMIT / OFFSET
The LIMIT clause (called TOP in SQL Server or FETCH FIRST in ANSI SQL:2008) truncates the sorted result set to a specified number of rows. OFFSET, when used alongside LIMIT, skips a given number of rows before returning results, enabling pagination. Because LIMIT is the final logical phase, it operates on the fully sorted, fully projected result. Using LIMIT without ORDER BY is generally meaningless, since the set of rows retained is nondeterministic.
SECTION 5
Alias Visibility & Scope Rules
One of the most practical consequences of the logical order is the visibility of column aliases. Since SELECT is evaluated in phase 5, any alias you create there (for example, SELECT price * quantity AS total_cost) does not exist during earlier phases. This explains why the following query is illegal in standard SQL: SELECT price * quantity AS total_cost FROM orders WHERE total_cost > 100. The WHERE clause (phase 2) cannot see total_cost because it hasn't been defined yet. You must instead repeat the expression: WHERE price * quantity > 100.
| Clause | Can Use Column Aliases? | Can Use Aggregates? | Reason |
|---|---|---|---|
FROM | No | No | Only raw table/column names are resolved. |
WHERE | No | No | SELECT hasn't run; groups don't exist yet. |
GROUP BY | No | Implicitly | Groups are being formed; aggregates computed here. |
HAVING | No | Yes | Groups exist; SELECT aliases still not defined. |
SELECT | Defined here | Yes | Aliases are created; aggregates evaluated. |
ORDER BY | Yes | Yes | Runs after SELECT; all aliases visible. |
LIMIT | Yes | Yes | Runs last; operates on final sorted result. |
Alias and aggregate visibility by clause
Dialect Exception: MySQL
SECTION 6
Worked Example — Tracing the Logical Order
Consider the following query against a hypothetical orders table with columns customer_id, product_category, amount, and order_date:
Query
SELECT product_category, SUM(amount) AS total_sales FROM orders WHERE order_date >= '2024-01-01' GROUP BY product_category HAVING SUM(amount) > 10000 ORDER BY total_sales DESC LIMIT 5;Tracing the Logical Processing Order
- 1
Step 1 — FROM
The engine identifies the source table:orders. All rows and all columns ofordersform the initial virtual table. If there were JOINs, they would be resolved here.Virtual table VT₁ = all rows fromorders. - 2
Step 2 — WHERE
The predicateorder_date >= '2024-01-01'is evaluated against each row in VT₁. Every row with anorder_datebefore January 1, 2024 is discarded. Note that we cannot referencetotal_saleshere because SELECT hasn't executed.Virtual table VT₂ = rows from 2024 onward. - 3
Step 3 — GROUP BY
The surviving rows in VT₂ are partitioned byproduct_category. If categories include 'Electronics', 'Clothing', 'Books', etc., each becomes a separate group. The virtual table is now a table of groups, not individual rows.Virtual table VT₃ = one group per distinctproduct_category. - 4
Step 4 — HAVING
The aggregate predicateSUM(amount) > 10000is evaluated for each group. Groups whose total sales are $10,000 or less are eliminated. This is the group-level filter—analogous to WHERE for individual rows.Virtual table VT₄ = only high-revenue categories. - 5
Step 5 — SELECT
The engine now evaluates the SELECT expressions:product_category(a grouping column, so one value per group) andSUM(amount) AS total_sales(the aggregate, now given the aliastotal_sales). The result set is projected down to these two columns.Virtual table VT₅ = two-column result with aliastotal_salesnow defined. - 6
Step 6 — ORDER BY
The result is sorted bytotal_sales DESC. Because ORDER BY runs after SELECT, the aliastotal_salesis now visible and can be used. The highest-revenue categories appear first.Virtual table VT₆ = sorted by total_sales descending. - 7
Step 7 — LIMIT
Only the first 5 rows of the sorted result are retained. This gives us the top 5 product categories by revenue since January 2024.Final result: top 5 product categories by 2024 revenue exceeding $10,000.
SECTION 7
Common Pitfalls & WHERE vs. HAVING
Understanding the logical order is not merely academic; it directly prevents a class of common SQL bugs and performance anti-patterns. The table below catalogs the most frequent mistakes that arise from ignoring or misunderstanding the processing sequence.
| Pitfall | Incorrect Usage | Correct Approach |
|---|---|---|
| Alias in WHERE | WHERE total_cost > 100 | WHERE price * qty > 100 |
| Aggregate in WHERE | WHERE COUNT(*) > 5 | HAVING COUNT(*) > 5 |
| Row filter in HAVING | HAVING status = 'active' | WHERE status = 'active' |
| LIMIT without ORDER BY | SELECT * FROM t LIMIT 10 | Add ORDER BY for deterministic results |
| Non-grouped column in SELECT | SELECT name, SUM(x) ... GROUP BY dept | Add name to GROUP BY or use an aggregate |
Common SQL mistakes caused by misunderstanding logical order
WHERE vs. HAVING — THE DEFINITIVE RULE
status = 'active') always belongs in WHERE because it eliminates rows before the expensive grouping operation. A filter on an aggregate (e.g., COUNT(*) > 5) must go in HAVING because the aggregate can only be computed after groups exist. Misplacing a row-level filter in HAVING forces the engine to group unnecessary rows, degrading performance.SECTION 8
Connection to Query Optimization & Advanced SQL
The logical order establishes correctness, but real-world database engines employ query optimizers that rewrite and reorder operations for efficiency. A cost-based optimizer might push a WHERE predicate into a JOIN condition (predicate pushdown), evaluate LIMIT before fully sorting (top-N optimization), or skip GROUP BY entirely when an index already provides sorted, aggregated data. The guarantee is that the final result is indistinguishable from what the logical order would produce. Understanding the logical order thus gives you the mental model to predict what the optimizer is allowed to do and—crucially—what errors it cannot silently fix.
| Concept | Logical Order Perspective | Advanced / Physical Perspective |
|---|---|---|
| Subqueries & CTEs | Each subquery follows the same 7-phase pipeline internally. | Optimizer may inline CTEs or materialize them; logical order is per query block. |
| Window Functions | Evaluated after SELECT, before ORDER BY (phase 5.5 conceptually). | Can share sort operations with ORDER BY for efficiency. |
| DISTINCT | Logically applied during SELECT (phase 5), removes duplicate rows. | May be implemented via hash or sort-based deduplication. |
| Predicate Pushdown | WHERE logically runs in phase 2, after FROM. | Optimizer pushes predicates into index scans or JOIN conditions for early elimination. |
| UNION / INTERSECT / EXCEPT | Each operand follows the full pipeline; set operation combines results. | ORDER BY and LIMIT apply to the combined result, not individual operands. |
Logical order vs. advanced SQL concepts
As you advance into topics like window functions, recursive CTEs, and query execution plans (EXPLAIN), the logical order remains your foundational mental model. Window functions, for example, are logically evaluated after WHERE and GROUP BY but before ORDER BY—which is why they can reference columns filtered by WHERE and aggregated by GROUP BY, but not values sorted by ORDER BY. Every new SQL feature slots into this pipeline; knowing the pipeline means you can reason about any query, no matter how complex.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
SELECT department, AVG(salary) AS avg_sal FROM employees WHERE avg_sal > 50000 GROUP BY department; and receives an error. Explain, using the logical processing order, why this query fails and how to fix it.Show answer
avg_sal is a column alias defined in the SELECT clause, which is phase 5. The WHERE clause (phase 2) executes before SELECT and therefore cannot see avg_sal. Second, AVG(salary) is an aggregate function, and WHERE cannot contain aggregates because groups have not yet been formed (GROUP BY is phase 3). The fix is to move the condition to a HAVING clause: HAVING AVG(salary) > 50000. HAVING executes in phase 4, after groups exist, so aggregates are valid there.Question 2: BASIC
SELECT region, COUNT(*) AS num_orders FROM orders WHERE status = 'shipped' GROUP BY region ORDER BY num_orders DESC LIMIT 3;, list the logical processing order of the clauses as they are evaluated by the engine. For each phase, state what the virtual table contains after that phase completes.Show answer
orders table; VT₁ contains all rows. Phase 2 — WHERE: Rows where status = 'shipped' survive; VT₂ contains only shipped orders. Phase 3 — GROUP BY: Rows are partitioned by region; VT₃ is a table of groups, one per region. Phase 4 — HAVING: Not present, so VT₄ = VT₃. Phase 5 — SELECT: The engine computes region and COUNT(*) AS num_orders; VT₅ has two columns. Phase 6 — ORDER BY: Sorts descending by num_orders. Phase 7 — LIMIT: Returns only the top 3 rows.Question 3: INTERMEDIATE
SELECT customer_id, SUM(amount) FROM orders GROUP BY customer_id ORDER BY MAX(amount) DESC; even though MAX(amount) does not appear in the SELECT list. Relate your answer to the logical order of operations.Show answer
MAX(amount) for each group even though it doesn't appear in SELECT. ORDER BY is unique in this regard: it can reference expressions that aren't projected. This is valid because ORDER BY logically operates on the result of grouping, where each group's rows are still conceptually available for aggregate computation. The key insight is that ORDER BY's visibility extends to the full grouped virtual table, not just the columns that SELECT chose to project.Question 4: APPLIED
SELECT category, SUM(price) AS revenue, COUNT(*) AS order_count FROM sales WHERE YEAR(sale_date) = 2024 AND order_count > 100 GROUP BY category HAVING revenue > 50000 ORDER BY revenue DESC LIMIT 10; Identify all errors, explain each using the logical order, and provide the corrected query.Show answer
order_count > 100 is in WHERE (phase 2), but order_count is an alias defined in SELECT (phase 5). Additionally, COUNT(*) is an aggregate and cannot appear in WHERE because groups don't exist yet. This condition must move to HAVING. Error 2: HAVING revenue > 50000 references the SELECT alias revenue, which is not yet defined in HAVING (phase 4). In standard SQL, this must be rewritten as HAVING SUM(price) > 50000. Corrected query: SELECT category, SUM(price) AS revenue, COUNT(*) AS order_count FROM sales WHERE YEAR(sale_date) = 2024 GROUP BY category HAVING COUNT(*) > 100 AND SUM(price) > 50000 ORDER BY revenue DESC LIMIT 10; Note that ORDER BY can reference the alias revenue because it runs after SELECT (phase 6).Question 5: CRITICAL THINKING
Show answer
SUMMARY
Summary — SQL Logical Order of Operations
Summary
The SQL logical processing order defines a seven-phase pipeline that governs how a query is conceptually evaluated, regardless of syntactic order. Evaluation begins with FROM, which assembles and joins source tables. WHERE then filters individual rows using Boolean predicates—no aggregates allowed. GROUP BY partitions the surviving rows into groups, and HAVING filters those groups using aggregate conditions. SELECT evaluates expressions and defines column aliases. ORDER BY sorts the result, and LIMIT truncates it.
The most important practical consequence is scope visibility: column aliases defined in SELECT are invisible to WHERE, GROUP BY, and HAVING because those phases execute earlier. Aggregate functions cannot appear in WHERE because groups have not yet been formed. Placing row-level filters in HAVING instead of WHERE is both semantically incorrect and performance-degrading. The logical order is not just a theoretical curiosity—it is the single mental model that explains alias errors, aggregate placement rules, and the relationship between physical execution plans and query correctness. Master this pipeline, and every SQL query you write will be predictable and portable.