Core CS · DBMS
Why WHERE cannot see your alias
You write a query SELECT first. The database runs it FROM first. One six-row table and one query, taken apart clause by clause in the order it really happens.
Run one query clause by clause, six rows at a time →01 The idea
Written in one order, run in another
A SELECT statement is written in an order that reads well in English: take these columns, from this table, where this is true. The database does not run it that way. It runs FROM first, because it has nothing to filter until it has rows in hand, and it runs SELECT fifth, long after WHERE has finished its work. The order you type is a reading order. The order it runs in is a fixed list of eight steps, and every database on the planet agrees on that list.
Each clause is a small machine with rows going in and rows coming out. FROM hands over every row of the table. WHERE tests one condition against each of those rows and passes on the ones that come back true. SELECT throws away the columns you did not ask for and creates any new ones you named with AS. DISTINCT folds identical rows together, ORDER BY sorts whatever is left, and LIMIT keeps the first few rows of that sorted list and throws the rest away. Nothing skips ahead and nothing reaches back.
That single-file arrangement has one consequence you will meet almost immediately. SELECT marks AS score creates the name score at step five. WHERE ran at step two, when that name did not exist, so WHERE score >= 60 is not a rule you have broken. It is a column the database has never heard of, and it will say so in exactly those words. ORDER BY runs at step seven, after score exists, so ORDER BY score is accepted without complaint. Same alias, same query, two different answers, and the whole explanation is a position on a list of eight.
02 Worked example
One table, one query, six clauses
Six students. Four columns. Divya sat the exam late, so her marks are not recorded yet and that cell holds NULL, which is not zero and not an empty string but the absence of a value. This table is the only data in the lesson: the flow below, the console, the cheat sheet and every practice problem run against these same six rows.
| sid | name | city | marks |
|---|---|---|---|
| S1 | Asha | Pune | 78 |
| S2 | Ravi | Delhi | 55 |
| S3 | Meera | Pune | 91 |
| S4 | Karan | Delhi | 78 |
| S5 | Divya | Surat | NULL |
| S6 | Arjun | Kochi | 64 |
Here is the question we are asking of it: the first two cities, in alphabetical order, that have at least one student scoring 60 or more. Written out, that is five clauses and one keyword.
SELECT DISTINCT cityFROM studentWHERE marks >= 60ORDER BY cityLIMIT 2;
Read it top to bottom and you would guess the database picks the city column first and then filters. It does the opposite. Follow the row set instead, because the row set is what actually moves.
6 rows, 4 columns
6 → 4 rows
4 cols → 1
4 → 3 rows
3 → 2 rows
The answer is Delhi, Kochi. Six rows went in and two came out, and each of the five nodes is responsible for a named part of the loss: WHERE removed two rows, SELECT removed three columns, DISTINCT removed one duplicate, LIMIT removed one more row. Nothing was removed twice and nothing was removed by accident.
Node 3 is highlighted because it is the one people place wrongly. SELECT is the first word you type and the third thing that happens. Everything before it works on all four columns whether you asked for them or not, which is why WHERE marks >= 60 is legal even though marks is nowhere in the select list. Everything after it works on what SELECT left behind.
Hold on to that asymmetry. It is the difference between the two halves of the next section: what a clause can filter on, and what a clause can name.
03 Mechanics
The eight slots, and where each name is born
This is the list. Two of the eight slots are empty in our query because it has no grouping, and they are shown anyway because you need to know where they sit before you meet GROUP BY. The last column is the one that answers interview questions: it says which names come into existence at that step, and therefore which clauses are allowed to use them.
| # | Clause | What it does | Our query, rows in and out | Names it creates |
|---|---|---|---|---|
| 1 | FROM | Names the source and produces every row in it. | — → 6 rows, 4 cols | Every column of student, plus the table alias if you wrote AS s. |
| 2 | WHERE | Tests one predicate against each row and keeps the rows that come back true. | 6 → 4 rows | Nothing. |
| 3 | GROUP BY | Folds rows into one row per group. | 4 → 4 (not used here) | Aggregates over each group. From this step on, the only things you may name are the grouping columns and aggregates; a bare column that is neither is not valid standard SQL. |
| 4 | HAVING | The same kind of test as WHERE, but on groups, and it may use aggregates. | 4 → 4 (not used here) | Nothing. |
| 5 | SELECT | Evaluates the expressions you listed and drops every column you did not. | 4 cols → 1 col | Column aliases written with AS. |
| 6 | DISTINCT | Removes rows that are identical across the whole select list. | 4 → 3 rows | Nothing. |
| 7 | ORDER BY | Sorts the rows that are left. ASC is the default. It may still name a column SELECT dropped, as long as each surviving row stands for exactly one row of the input. | 3 → 3 rows | Nothing. |
| 8 | LIMIT / OFFSET | Skips OFFSET rows from the front, then keeps LIMIT rows. | 3 → 2 rows | Nothing. |
What can go inside WHERE
Step 2 is where most of a query’s work happens, so it is worth knowing everything you are allowed to put there. Each line below is a complete WHERE clause against the six rows from section 02, with the surviving sids worked out by hand.
WHERE marks >= 60 -- comparison -> S1 S3 S4 S6WHERE marks BETWEEN 60 AND 80 -- both ends included -> S1 S4 S6WHERE city IN ('Pune', 'Kochi') -- a list of values -> S1 S3 S6WHERE name LIKE 'A%' -- % = any run -> S1 S6WHERE name LIKE '_a%' -- _ = exactly one -> S2 S4WHERE marks IS NULL -- the only test -> S5WHERE marks = NULL -- never true -> no rowsWHERE NOT marks >= 60 -- unknown stays out -> S2
Lines 6, 7 and 8 are one idea in three costumes. NULL is not a value, so no comparison involving it produces true or false; it produces unknown. Line 7 asks whether an unrecorded mark equals nothing in particular, gets unknown for all six rows, and returns none of them. Line 8 is stranger: NOT applied to unknown is still unknown, so Divya does not appear in marks >= 60 and does not appear in its negation either. A row can be missing from both halves of a yes-or-no question, and that surprises people once and then never again.
Line 5 needs one more word. % matches any run of characters including an empty one, and _ matches exactly one. So '_a%' reads “any first character, then an a, then anything”, which is why it finds Ravi and Karan and not Asha. Whether LIKE cares about upper and lower case is not part of the operator at all; it comes from the collation of the column, and it differs between products.
AND before OR, always
When a WHERE clause holds more than one condition, NOT is applied first, then AND, then OR, exactly as multiplication is applied before addition. The query below is the one that catches people, because the reading a human gives it left to right is not the reading the database gives it.
WHERE city = 'Pune' OR city = 'Kochi' AND marks >= 80-- AND binds tighter, so the database reads it as:WHERE city = 'Pune' OR (city = 'Kochi' AND marks >= 80) -- S1 S3-- if you meant the other one, the brackets are not optional:WHERE (city = 'Pune' OR city = 'Kochi') AND marks >= 80 -- S3
Line 3 returns Asha and Meera, because every Pune row satisfies the left half on its own and nothing else clears 80 in Kochi. Line 5 returns Meera alone, because now the mark test applies to the Pune rows too and Asha’s 78 fails it. One pair of brackets, two different answers, and no error either way.
The alias, and the two clauses that disagree about it
Now the payoff. The query below is legal English and illegal SQL, and the line numbers in the comments are the step numbers from the table above.
SELECT name, marks AS score -- step 5: the name "score" is created hereFROM student -- step 1: runs first, whatever you typedWHERE score >= 60 -- step 2: ERROR, no column named scoreORDER BY score DESC, name; -- step 7: fine, score exists by now -- the fix is one word: name the real column, not the aliasSELECT s.name, s.marks AS scoreFROM student AS s -- FROM names the source and creates the alias sWHERE s.marks >= 60 -- Meera 91, Asha 78, Karan 78, Arjun 64ORDER BY score DESC, name;
The second query returns Meera 91, then Asha 78, then Karan 78, then Arjun 64. The second sort key earns its place: Asha and Karan both scored 78, and a single-key sort promises nothing at all about which of two tied rows comes first. Add a tiebreaker whenever you would notice the difference.
Two aliases appear across that pair of queries, the table alias s and the column alias score, and only score is refused in WHERE. That is not a special rule about the word AS. It follows from the eight-step list and nothing else, and working out why is worth more than memorising the exception.
One qualification, because you will meet it the first time you sort by something you did not display. ORDER BY is allowed to name a column SELECT dropped: SELECT name FROM student ORDER BY marks DESC is accepted everywhere, even though marks is not in the output. That is not a contradiction of the rule above, because FROM created marks at step 1 and every row of the result still stands for exactly one row of the table, so the value to sort by is never in doubt. The moment a step folds several input rows into one output row, that one-to-one link is gone and so is any column the fold discarded, which is a good thing to have thought about before you reach the last practice problem.
05 Cheat sheet
Every clause on one card, checked against the same six rows
The right-hand column is what makes this worth glancing at before an interview: every claim here was worked out by hand against Asha, Ravi, Meera, Karan, Divya and Arjun, so you can check any row of it in thirty seconds without a database.
| Feature | Written as | On our six rows | The trap |
|---|---|---|---|
| Comparison | marks >= 60 | S1 S3 S4 S6 | a NULL satisfies no comparison, so Divya is in neither >= 60 nor < 60 |
| BETWEEN | marks BETWEEN 60 AND 80 | S1 S4 S6 | inclusive at both ends; BETWEEN 80 AND 60 returns nothing |
| IN | city IN ('Pune','Kochi') | S1 S3 S6 | a list of exact values, not a pattern |
| NOT IN with a NULL | city NOT IN ('Pune', NULL) | no rows at all | it expands to NOT (city='Pune' OR city=NULL), and that is never true |
| LIKE, percent sign | name LIKE 'A%' | S1 S6 | % matches a run of any length, including zero characters |
| LIKE, underscore | name LIKE '_a%' | S2 S4 | _ matches exactly one character, never zero |
| IS NULL | marks IS NULL | S5 | marks = NULL returns nothing; IS NULL is the only test that works |
| AND, OR, NOT | city='Pune' OR city='Kochi' AND marks>=80 | S1 S3 | NOT, then AND, then OR; this reads as Pune OR (Kochi AND 80) |
| DISTINCT | SELECT DISTINCT city | Pune, Delhi, Surat, Kochi | compares the whole select list, and folds every NULL into one row |
| ORDER BY | ORDER BY city | Delhi, Kochi, Pune, Surat (on the distinct list above) | ASC is the default; where the NULLs land is vendor-specific |
| LIMIT and OFFSET | ORDER BY city LIMIT 2 OFFSET 1 | Kochi, Pune (same distinct list, skip one) | OFFSET skips first, then LIMIT counts; meaningless without ORDER BY |
| Column alias | marks AS score | created at step 5 | invisible to WHERE, visible to ORDER BY |
| Table alias | FROM student AS s | created at step 1 | the alias becomes the name of the source for the rest of the query |
06 Where & why
The same query, five products, three things that move
The eight-step order is the part that does not change between databases. Three things around it do, and each of them has caught out somebody porting a query on their first job: how you spell LIMIT, whether LIKE cares about case, and where the NULLs go in a sort.
SELECT marks AS score FROM student WHERE score >= 60 comes back with ERROR: column "score" does not exist, which names the problem precisely: not a bad rule, a missing name. ORDER BY score on the same query is accepted. LIKE here is case-sensitive, so name LIKE 'a%' matches none of our six students, and ILIKE is the case-insensitive form. In a sort, PostgreSQL puts NULLs last ascending and first descending unless you write NULLS FIRST or NULLS LAST yourself.
MySQL allows a column alias in GROUP BY, HAVING and ORDER BY, and follows the standard in refusing it in WHERE, which tells you the restriction is about the running order and not about strictness. Its default collation is case-insensitive, so name LIKE 'a%' does match Asha and Arjun here, the opposite of PostgreSQL. It sorts NULLs first when ascending, and it accepts both LIMIT 2 OFFSET 1 and the older LIMIT 1, 2, where the offset is written first.
Oracle has never had LIMIT. From 12c you write OFFSET 1 ROWS FETCH FIRST 2 ROWS ONLY after the ORDER BY. Older code uses ROWNUM, and that carries a trap this lesson explains for you: ROWNUM is assigned as rows are produced, before ORDER BY runs, so WHERE ROWNUM <= 2 ORDER BY city takes two arbitrary rows and then sorts those two. Oracle also stores an empty string as NULL, so WHERE city = '' matches nothing, ever.
SQL Server has no LIMIT either: use SELECT TOP (2), or OFFSET 0 ROWS FETCH NEXT 2 ROWS ONLY, which is only legal when an ORDER BY is present. Its default collation is case-insensitive like MySQL’s. SQLite does have LIMIT 2 OFFSET 1, and its LIKE ignores case for ASCII letters. So the eight steps hold on all five, and that is as far as the agreement goes: the last line of our lesson query needs three different spellings across them, and name LIKE 'a%' finds Asha and Arjun on MySQL, SQL Server and SQLite while finding nobody at all on PostgreSQL or Oracle, with nothing changed but the server.
07 Interview questions
What they actually ask
SQL is the part of a DBMS interview where the interviewer can check you instead of believing you, because both of you can run the query. Answers that name the step number beat answers that name the rule.
In what order does a SELECT statement actually run?
Why can WHERE not use a column alias when ORDER BY can?
What is the difference between WHERE and HAVING?
Somebody writes WHERE marks = NULL and gets nothing back. What is wrong?
COUNT(*) returns 6 and COUNT(marks) returns 5 on the same table. Why?
What exactly does DISTINCT compare?
What do % and _ match in LIKE, and is LIKE case-sensitive?
Is BETWEEN inclusive?
Does AND or OR bind tighter?
Is LIMIT 10 without an ORDER BY safe?
Is SELECT * acceptable?
Is the logical order the same as how the database really executes the query?
08 Practice problems
Six to work through
All six use the same six students from section 02. Work them with a pen: write the row set after each clause before you move to the next one, and write the truth value for every row, including Divya, before you decide anything.