SQL • SQL Foundations

SQL Logical Order — Describe the logical order of SQL operations (FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT) (conceptual)

Understanding how the database engine processes your query, regardless of how you write it.

SQL illustration with a database, structured table, and query code symbols
Inside this lesson 10 sections 5 questions 5 figures

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

Edgar F. Codd publishes "A Relational Model of Data for Large Shared Data Banks," establishing the theoretical basis for relational databases and declarative query languages.

1974 • SEQUEL at IBM

Donald Chamberlin and Raymond Boyce develop SEQUEL (later renamed SQL) at IBM's San Jose Research Laboratory, introducing the familiar clause-based syntax: SELECT, FROM, WHERE.

1986 • ANSI SQL Standard

SQL becomes an ANSI standard (SQL-86), formalizing the language's syntax and, implicitly, the logical processing order that all conforming engines must respect.

1992 • SQL-92 & GROUP BY / HAVING

The SQL-92 standard refines aggregation semantics, codifying the relationship between GROUP BY, HAVING, and SELECT and making the logical order explicitly relevant for query correctness.

2003–Present • Window Functions & Modern SQL

SQL:2003 introduces window functions, adding new phases to logical processing. Modern optimizers reorder physical execution while preserving logical semantics, reinforcing the importance of understanding the logical order.

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.

1

Declarative vs. Procedural

SQL tells the engine what to retrieve. The logical order is the engine's conceptual recipe for transforming that declaration into a result set. You never write loops or index lookups—the engine handles that.
2

Logical vs. Physical Order

The logical order is an abstract contract: given these clauses, the result must be as if they were executed in this sequence. The physical execution plan—decided by the query optimizer—may reorder, parallelize, or skip steps entirely.
3

Scope & Visibility

Each phase can only reference information available from earlier phases. A column alias defined in SELECT is invisible to WHERE because WHERE executes first. This scoping rule is the most common source of SQL errors.
4

Set-Based Thinking

Each logical phase takes a set of rows as input and produces a new set of rows as output. FROM builds the initial set, WHERE filters it, GROUP BY partitions it, and so on. SQL operates on entire sets, not individual rows.
5

The Seven-Phase Pipeline

The canonical logical order is: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. Each phase narrows, reshapes, or reorders the intermediate result before passing it to the next phase.
✦

KEY TAKEAWAY

Think of the logical order like a manufacturing assembly line. The raw materials arrive at the dock (FROM), are inspected for defects (WHERE), sorted into bins (GROUP BY), bins are inspected (HAVING), labels are printed (SELECT), boxes are arranged on the shelf (ORDER BY), and finally you load only what fits in the truck (LIMIT). You don't print labels before you know which bins passed inspection—just as SQL doesn't evaluate SELECT before HAVING.

SECTION 3

Visual Explanation — The Logical Pipeline

SQL Logical Processing Order(Syntactic order shown on right for comparison)1FROM / JOINAssemble source tables & apply joins2WHEREFilter individual rows (no aggregates)3GROUP BYPartition rows into groups4HAVINGFilter groups using aggregate conditions5SELECTEvaluate expressions, aliases, DISTINCT6ORDER BY7LIMITSYNTACTIC ORDERSELECT ← 5thFROM ← 1stWHERE ← 2ndGROUP BY ← 3rdHAVING ← 4thORDER BY ← 6thLIMIT ← 7thNotice: SELECT appears 1stin syntax but is evaluated5th in logical order.
The left column shows the logical processing order as a pipeline: each phase receives the output of its predecessor. The right column maps each clause to its position in the familiar syntactic order. The key mismatch is SELECT, which we write first but which is logically evaluated fifth.

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.

Column Alias Visibility by Logical PhaseFROMWHEREGROUP BYHAVINGSELECTORDER BYLIMITTable AliasesColumn AliasesAggregates✓✓✓✓✓✓✓✗✗✗✗✓Defined here✓✓✗✗No groups yet✓Groups formed✓✓✓✓
This matrix shows which types of references are available at each logical phase. Column aliases become available only at SELECT (phase 5) and are therefore visible to ORDER BY and LIMIT but not to WHERE, GROUP BY, or HAVING. Aggregate functions become meaningful only after GROUP BY (phase 3), which is why they cannot appear in WHERE.
ClauseCan Use Column Aliases?Can Use Aggregates?Reason
FROMNoNoOnly raw table/column names are resolved.
WHERENoNoSELECT hasn't run; groups don't exist yet.
GROUP BYNoImplicitlyGroups are being formed; aggregates computed here.
HAVINGNoYesGroups exist; SELECT aliases still not defined.
SELECTDefined hereYesAliases are created; aggregates evaluated.
ORDER BYYesYesRuns after SELECT; all aliases visible.
LIMITYesYesRuns last; operates on final sorted result.

Alias and aggregate visibility by clause

⚠

Dialect Exception: MySQL

MySQL allows column aliases in GROUP BY and HAVING as a non-standard extension. While convenient, relying on this behavior produces queries that are not portable to PostgreSQL, SQL Server, or Oracle. For academic and production work, follow the ANSI standard and assume aliases are invisible before ORDER BY.

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. 1

    Step 1 — FROM

    The engine identifies the source table: orders. All rows and all columns of orders form the initial virtual table. If there were JOINs, they would be resolved here.
    Virtual table VT₁ = all rows from orders.
  2. 2

    Step 2 — WHERE

    The predicate order_date >= '2024-01-01' is evaluated against each row in VT₁. Every row with an order_date before January 1, 2024 is discarded. Note that we cannot reference total_sales here because SELECT hasn't executed.
    Virtual table VT₂ = rows from 2024 onward.
  3. 3

    Step 3 — GROUP BY

    The surviving rows in VT₂ are partitioned by product_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 distinct product_category.
  4. 4

    Step 4 — HAVING

    The aggregate predicate SUM(amount) > 10000 is 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. 5

    Step 5 — SELECT

    The engine now evaluates the SELECT expressions: product_category (a grouping column, so one value per group) and SUM(amount) AS total_sales (the aggregate, now given the alias total_sales). The result set is projected down to these two columns.
    Virtual table VT₅ = two-column result with alias total_sales now defined.
  6. 6

    Step 6 — ORDER BY

    The result is sorted by total_sales DESC. Because ORDER BY runs after SELECT, the alias total_sales is now visible and can be used. The highest-revenue categories appear first.
    Virtual table VT₆ = sorted by total_sales descending.
  7. 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.

PitfallIncorrect UsageCorrect Approach
Alias in WHEREWHERE total_cost > 100WHERE price * qty > 100
Aggregate in WHEREWHERE COUNT(*) > 5HAVING COUNT(*) > 5
Row filter in HAVINGHAVING status = 'active'WHERE status = 'active'
LIMIT without ORDER BYSELECT * FROM t LIMIT 10Add ORDER BY for deterministic results
Non-grouped column in SELECTSELECT name, SUM(x) ... GROUP BY deptAdd name to GROUP BY or use an aggregate

Common SQL mistakes caused by misunderstanding logical order

⚡

WHERE vs. HAVING — THE DEFINITIVE RULE

Think of WHERE as a bouncer at the door of a nightclub—it decides who gets in before anyone sits down at tables (groups). HAVING is the manager who walks through the club after groups have formed and asks certain tables to leave. A filter on a raw column (e.g., 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.

ConceptLogical Order PerspectiveAdvanced / Physical Perspective
Subqueries & CTEsEach subquery follows the same 7-phase pipeline internally.Optimizer may inline CTEs or materialize them; logical order is per query block.
Window FunctionsEvaluated after SELECT, before ORDER BY (phase 5.5 conceptually).Can share sort operations with ORDER BY for efficiency.
DISTINCTLogically applied during SELECT (phase 5), removes duplicate rows.May be implemented via hash or sort-based deduplication.
Predicate PushdownWHERE logically runs in phase 2, after FROM.Optimizer pushes predicates into index scans or JOIN conditions for early elimination.
UNION / INTERSECT / EXCEPTEach 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

A student writes 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
The query fails for two reasons, both rooted in the logical order. First, 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

Given the query 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
Phase 1 — FROM: The engine reads the 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

Explain why the following query is valid in standard SQL: 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
ORDER BY (phase 6) has access to the virtual table produced by HAVING/SELECT, but in most SQL dialects, it can also reference expressions computable from the grouped data, including aggregate functions over group members. Since GROUP BY has already been executed (phase 3), the engine can compute 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

You are building a dashboard query for an e-commerce platform. You need the top 10 product categories by total revenue for 2024, but only categories with more than 100 orders and revenue exceeding $50,000. A teammate writes: 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
Error 1: 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

Some SQL dialects (notably MySQL and SQLite) allow column aliases in GROUP BY and HAVING. Analyze the trade-offs of this design decision. Consider portability, potential for ambiguity (what if an alias collides with a column name?), and how this relates to the theoretical logical processing order. Would you recommend relying on this extension in production code?
Show answer
Allowing aliases in GROUP BY/HAVING is a convenience that violates the strict logical processing order, where SELECT (which defines aliases) runs after these clauses. The trade-off involves several dimensions. Portability: code relying on this extension will fail on PostgreSQL, Oracle, and SQL Server, making database migration costly. Ambiguity: if a SELECT alias matches an existing column name, the engine must decide which takes precedence; MySQL resolves this by preferring column names, which can produce silent logic errors if a table schema changes. Theoretical consistency: the extension means MySQL's parser effectively performs an extra pass to resolve aliases before GROUP BY, which deviates from the standard's single-pass logical model. Performance-wise, the extension has no impact because the optimizer can still process phases in any physical order. For production code, the recommendation is to avoid this extension: repeat the expression in GROUP BY/HAVING rather than using the alias. The minor syntactic convenience is not worth the risks of non-portability, ambiguity, and the conceptual confusion it creates for developers who need to reason about query correctness across platforms.

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.

Varsity Tutors • SQL • SQL Logical Order of Operations