# SQL Logical Query Order: Why WHERE Can't See Your Alias

> SQL is written SELECT-first but evaluated FROM-first. Knowing the logical order explains alias errors, HAVING vs WHERE, and where window functions run.

- Source: https://akshaythakare.com/notes/sql/sql-logical-query-order
- Topic: SQL
- Level: beginner
- Reading time: 4 min
- Tags: sql, fundamentals, interview
- Updated: 2026-10-11

## Written order vs evaluated order

You write a query starting with `SELECT`, but the database evaluates it in a different logical order:

```mermaid
flowchart TB
  accTitle: SQL logical evaluation order
  accDescr: FROM and JOIN run first, then WHERE, GROUP BY, HAVING, window functions, SELECT, DISTINCT, ORDER BY and finally LIMIT.
  A[FROM / JOIN] --> B[WHERE] --> C[GROUP BY] --> D[HAVING] --> E[Window functions] --> F[SELECT] --> G[DISTINCT] --> H[ORDER BY] --> I[LIMIT]
```

This is the *logical* order — the optimizer may physically reorder work (for example pushing filters into a scan), but the result must be as if this order were followed.

## Consequence 1 — WHERE cannot use a SELECT alias

```sql
-- fails in most engines: net_amount does not exist yet when WHERE runs
SELECT amount - discount AS net_amount
FROM orders
WHERE net_amount > 100;

-- repeat the expression, or wrap it
SELECT * FROM (
  SELECT amount - discount AS net_amount FROM orders
) t
WHERE net_amount > 100;
```

Some warehouses (Snowflake, Databricks, BigQuery in some cases) allow aliases in `WHERE` as an extension. Don't rely on it in portable SQL.

## Consequence 2 — WHERE filters rows, HAVING filters groups

```sql
SELECT customer_id, SUM(amount) AS total
FROM orders
WHERE status = 'PAID'          -- row filter: runs before grouping
GROUP BY customer_id
HAVING SUM(amount) > 1000;     -- group filter: runs after aggregation
```

> **Tip: Filter early**
>
> Anything that can go in `WHERE` should. Filtering rows before `GROUP BY` means fewer rows to aggregate, which is cheaper on every engine.

## Consequence 3 — window functions run after GROUP BY

Window functions are computed after grouping and `HAVING`, just before `SELECT` finishes. So you can rank aggregated values in one query:

```sql
SELECT customer_id,
       SUM(amount) AS total,
       RANK() OVER (ORDER BY SUM(amount) DESC) AS spend_rank
FROM orders
GROUP BY customer_id;
```

But you cannot filter on a window function in `WHERE` — use a subquery or `QUALIFY` where supported.

## Consequence 4 — ORDER BY can use aliases

`ORDER BY` runs after `SELECT`, so aliases are visible there:

```sql
SELECT customer_id, SUM(amount) AS total
FROM orders
GROUP BY customer_id
ORDER BY total DESC;
```

## Interview answer in one breath

SQL is evaluated FROM, WHERE, GROUP BY, HAVING, window functions, SELECT, DISTINCT, ORDER BY, LIMIT. That is why `WHERE` cannot see a `SELECT` alias, why aggregates belong in `HAVING`, and why you filter window results in an outer query or with `QUALIFY`.
