skip to content

SELECT and Filtering

The core of every query: the SELECT list, WHERE predicates, NULL and its three-valued logic, DISTINCT, CASE, ORDER BY, LIMIT and OFFSET, and the set operations. Interviewers start here because most wrong answers come from NULL handling or from assuming clauses evaluate in the order you typed them.

part ofSQLoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

Why does SELECT id FROM orders JOIN customers ON customers.id = orders.customer_id fail?

level: juniorimportance: must knowfreq 65%

answer

  1. two tables, one column name
  2. the parser refuses to choose for you
  3. it is decided from the schema, not the data
  4. prefix the column with its source
  5. aliasing every table makes it impossible

basics

~20 s

Both tables in scope expose a column named id, so the unqualified reference is ambiguous and the statement is rejected before it runs. Qualify it as orders.id or customers.id, or give each table an alias and select o.id.

solid answer

~40 s

A bare column name is resolved against every table the `FROM` clause brings into scope. `orders` and `customers` both have an `id`, so `id` matches two candidates, and SQL treats an ambiguous reference as an error rather than picking one for you. The fix is to qualify the reference — `orders.id`, or `o.id` after `FROM orders AS o` — which is why experienced authors alias every table in a multi-table query and qualify every column. Note that the error is purely about **names**: it does not matter that the join condition forces the two columns to hold the same value, because resolution happens from the schema, before any row is read. The same rule applies to references in `ON`, `WHERE`, `GROUP BY` and `ORDER BY`, not only the select list.

code

sql · 9 lines
sql
-- Ambiguous: both tables expose a column named id
SELECT id, name
FROM orders
JOIN customers ON customers.id = orders.customer_id;

-- Unambiguous: alias each table, qualify each column
SELECT o.id AS order_id, c.name AS customer_name
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id;

go deeper

for a junior

Recall the rule and the fix: when two tables in scope share a column name, an unqualified reference is rejected, and you qualify it with the table name or a table alias.

for a middle

Explain resolution from the FROM clause outward — each table introduces a range variable, and a bare name must match exactly one column across all of them — and note the same rule governs ON, WHERE, GROUP BY and ORDER BY.

for a senior

Make the maintenance argument: qualify every column in multi-table queries so that adding a same-named column to another table cannot turn working queries into errors, and avoid SELECT * where duplicate output names would confuse client code.

for a principal

Turn it into a standard. Decide whether qualified references and explicit column lists are enforced by review or by linting, since the failure mode is a schema change in one team breaking queries owned by another.

## What the FROM clause puts in scope Every table (or derived table) in the `FROM` clause introduces a **range variable** — a name under which its columns become referable for the rest of the query. After ```sql FROM orders JOIN customers ON customers.id = orders.customer_id ``` two range variables are in scope: `orders` and `customers`. Every column of both is now a candidate for any unqualified reference in the statement. ## How a bare column reference is resolved When the parser meets a name such as `id`, it looks for columns with that name across all range variables currently in scope: - exactly one match — the reference resolves to that column; - no match — "column does not exist"; - two or more matches — **ambiguous column reference**, and the statement is rejected. SQL deliberately refuses to guess. Silently choosing the leftmost table would make the meaning of a query depend on the order of tables in `FROM`, and adding a column to an unrelated table could quietly change results instead of failing loudly. ## It is a name problem, not a value problem Candidates often argue that `customers.id = orders.customer_id` makes the values unambiguous. It does not matter. Name resolution happens while the statement is being analysed, from schema metadata alone — no rows have been read, and the engine has no notion that the values would agree. Ambiguity is decided purely from which tables expose which column names. ## Fixing it ```sql -- explicit qualification SELECT orders.id, customers.name FROM orders JOIN customers ON customers.id = orders.customer_id; -- the idiomatic form: alias every table, qualify every column SELECT o.id AS order_id, c.name AS customer_name FROM orders AS o JOIN customers AS c ON c.id = o.customer_id; ``` Renaming the *output* does not help the input: `SELECT id AS order_id` still fails, because the ambiguity is in the reference `id`, not in the result column's name. ## Where else it bites The same resolution rule governs `ON`, `WHERE`, `GROUP BY`, `HAVING` and `ORDER BY`. A query can select fine and then fail on `ORDER BY id`, or on `WHERE id > 100`, for exactly this reason. ## The maintenance hazard The most unpleasant version of this error appears without anyone touching the query. A multi-table query that used bare `status` worked because only one of its tables had a `status` column; someone later adds `status` to the other table, and every query with that bare reference starts failing. That is the strongest argument for a house rule: **in any query with more than one table in scope, qualify every column reference.** It costs two characters per reference and makes queries immune to that class of breakage, as well as far easier to read, because the reader no longer has to remember which table owns which column. ## SELECT * in a join ```sql SELECT * FROM orders AS o JOIN customers AS c ON c.id = o.customer_id; ``` This is legal — `*` expands to all columns of both tables — but the result carries two columns named `id`. Client libraries that index result columns by name then behave unpredictably, since only one of them can win a name lookup. In application code, list the columns explicitly and alias the collisions (`o.id AS order_id`, `c.id AS customer_id`). ## What a good answer sounds like "Both tables expose `id`, so the unqualified reference matches two columns and the parser rejects it instead of guessing. I would alias both tables and qualify every column — that also protects the query from breaking later when a column with the same name is added to the other table." That answer shows the rule, the fix, and the reason the fix is a habit rather than a patch.

  • The join condition forces both id columns to hold the same value — why is it still an error?
    Because name resolution happens during statement analysis, from schema metadata, before a single row is read. The engine has no way to know the values would agree, and even if it did, resolving names by value would make a query's meaning depend on its data. Ambiguity is decided by which tables expose the name.
  • Does SELECT * in a join hit the same problem?
    It is legal — `*` expands to all columns of both tables — but the result then contains two columns named `id`. Client code that looks columns up by name cannot address them reliably, so in application queries list columns explicitly and alias the collisions, for example `o.id AS order_id` and `c.id AS customer_id`.
  • Can a query that worked yesterday start failing with an ambiguous-column error?
    Yes. If a bare reference such as `status` resolved to one table only because the other table lacked that column, adding a `status` column to the other table makes the reference ambiguous and the query fails. Qualifying every column in multi-table queries immunises them against that.

saying these in an interview costs you the question

  • Thinks the engine silently picks the first table's column
  • Claims qualification is only a readability preference
  • Believes it is a runtime error that returns wrong rows
  • Says aliasing the output with AS resolves the input ambiguity
  • Argues the join condition makes the reference unambiguous

context

open as a page

How does SQL's simple CASE form differ from the searched CASE form?

level: juniorimportance: must knowfreq 75%

basics

~20 s

Simple CASE compares one expression for equality against a list of values (CASE status WHEN 'N' THEN ...). Searched CASE takes an independent boolean predicate per WHEN, so it can test ranges, several columns, or IS NULL.

open as a page

Does AND bind tighter than OR in a WHERE clause, and what bug does that cause?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Yes. Standard SQL precedence is NOT, then AND, then OR, so a OR b AND c means a OR (b AND c). The classic bug is an OR list of values silently escaping a filter that was meant to apply to all of them.

open as a page

In SELECT DISTINCT city, state FROM addresses, what exactly does DISTINCT deduplicate?

level: juniorimportance: must knowfreq 78%

basics

~10 s

DISTINCT applies to the whole select list, not to one column. SELECT DISTINCT city, state returns every unique city-and-state combination, so one city appears repeatedly if it occurs with several states.

open as a page

What does the predicate x BETWEEN 10 AND 20 mean, and are both endpoints included?

level: juniorimportance: must knowfreq 80%

basics

~20 s

BETWEEN is inclusive at both ends: x BETWEEN 10 AND 20 means x >= 10 AND x <= 20, so 10 and 20 both match. The lower bound must be written first, or the predicate matches nothing.

open as a page

In a SQL LIKE pattern, what do the % and _ wildcards each match?

level: juniorimportance: must knowfreq 85%

basics

~20 s

In a LIKE pattern, % matches any sequence of zero or more characters and _ matches exactly one character. Every other character is a literal, and the pattern must match the whole value, not just part of it.

open as a page

In SELECT ... ORDER BY id LIMIT 10 OFFSET 20, what do LIMIT and OFFSET each control?

level: juniorimportance: must knowfreq 82%

basics

~20 s

OFFSET 20 discards the first 20 rows of the ordered result; LIMIT 10 then returns at most the next 10 — rows 21 through 30. OFFSET applies first, and LIMIT counts only rows that survive the skip.

open as a page

In what order does SQL logically evaluate the clauses of a SELECT statement?

level: juniorimportance: must knowfreq 78%

basics

~10 s

SQL is written SELECT-first but evaluated FROM-first: FROM and its joins, then WHERE, GROUP BY, HAVING, the SELECT list, DISTINCT, ORDER BY, and finally the row limit. Each stage consumes the previous stage's output.

open as a page

Why does WHERE email = NULL return no rows, and what do you write instead?

level: juniorimportance: must knowfreq 88%

basics

~20 s

Comparing anything with NULL yields UNKNOWN rather than TRUE, and WHERE keeps only rows whose predicate is TRUE. So email = NULL matches nothing, not even rows whose email is NULL. Write email IS NULL.

open as a page

In ORDER BY last_name, first_name DESC, which columns sort descending, and how do you reverse both?

level: juniorimportance: must knowfreq 82%

basics

~10 s

ASC and DESC bind to one sort key each, so only first_name is descending; last_name still uses the default ASC. To reverse both, spell it out: ORDER BY last_name DESC, first_name DESC.

open as a page

What can you put in a SELECT list besides plain column names?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Any value expression: literals, arithmetic and string operators, scalar function calls such as UPPER, SUBSTRING or CAST, and CASE expressions. Each one is evaluated per row and produces a derived column, normally named with AS.

open as a page

What is the difference between UNION and UNION ALL in SQL?

level: juniorimportance: must knowfreq 88%

basics

~20 s

UNION combines the rows of two queries and removes duplicate rows from the combined result. UNION ALL concatenates them and keeps every row, duplicates included. UNION ALL is the cheaper choice whenever duplicates are impossible or wanted.

open as a page

How does SQL evaluate CASE WHEN branches when two WHEN conditions both match a row?

level: middleimportance: must knowfreq 62%

basics

~20 s

Branches are considered in written order and the first WHEN whose condition is TRUE supplies the result; later matching branches are never reached. Overlapping conditions are legal, so ordering the branches wrongly makes a branch unreachable.

open as a page

What happens when a WHERE predicate compares a VARCHAR column to a numeric literal?

level: middleimportance: must knowfreq 58%

basics

~20 s

The operands have different types, so something must convert. Standard SQL treats character and numeric values as not comparable; engines that allow the comparison apply their own implicit conversion, so results and errors vary. Compare like with like instead.

open as a page

Why does WHERE created_at BETWEEN '2024-01-01' AND '2024-01-31' miss orders placed on 31 January?

level: middleimportance: must knowfreq 72%

basics

~20 s

The end literal is a date, so it compares as 2024-01-31 00:00:00. A timestamp column keeps only rows at exactly midnight that day; everything later on the 31st fails the <= test. Use a half-open range instead: >= '2024-01-01' AND < '2024-02-01'.

open as a page

Why can SELECT * FROM orders LIMIT 10 return different rows each time it runs?

level: middleimportance: must knowfreq 70%

basics

~20 s

Without ORDER BY, a SQL result has no defined order, so LIMIT 10 returns whichever ten rows the engine produced first. A different plan, different data or different timing can produce a different ten. Only a total ORDER BY makes the slice deterministic.

open as a page

Why can't a WHERE clause reference a column alias defined in the SELECT list?

level: middleimportance: must knowfreq 72%

basics

~20 s

WHERE is evaluated before the SELECT list, so the alias does not exist yet. Repeat the expression in WHERE, or compute it in a derived table or WITH clause and filter in the outer query.

open as a page

How do AND, OR and NOT behave when an operand is UNKNOWN in SQL's three-valued logic?

level: middleimportance: must knowfreq 60%

basics

~10 s

UNKNOWN propagates unless the other operand already decides the result: FALSE AND UNKNOWN is FALSE, TRUE OR UNKNOWN is TRUE, and every other combination with UNKNOWN — including NOT UNKNOWN — stays UNKNOWN.

open as a page

Why can SELECT wins / games return 0 when both columns are integers, and how do you fix it?

level: middleimportance: must knowfreq 62%

basics

~20 s

Several engines keep integer arithmetic in the integer domain, so dividing two integers truncates the fraction: 3 / 4 becomes 0. Cast one operand to an exact numeric type first, for example CAST(wins AS DECIMAL(10,4)) / games.

open as a page

What does EXCEPT return, and how does it treat duplicate rows?

level: middleimportance: must knowfreq 55%

basics

~20 s

A EXCEPT B returns the distinct rows produced by A that do not appear anywhere in B. Duplicates are removed by default, and the operator is not commutative — swapping the two queries changes the result.

open as a page

Why can ORDER BY status alone return tied rows in a different order on each run?

level: seniorimportance: must knowfreq 56%

basics

~20 s

ORDER BY constrains only the relative order of rows that differ on a sort key. Rows equal on every key you listed may be returned in any order, and SQL promises no stability across runs. Add a unique final key to make the ordering total.

open as a page

What does a CASE expression return when no WHEN matches and there is no ELSE?

level: juniorimportance: should knowfreq 68%

basics

~20 s

It returns NULL. An omitted ELSE is defined as ELSE NULL, so any row matching no WHEN branch gets NULL rather than an empty string, a zero, or an error — and the row itself is still returned.

open as a page

What does WHERE status IN ('new','open') mean, and does repeating a value in the list change the result?

level: juniorimportance: should knowfreq 68%

basics

~20 s

IN against a value list is shorthand for a chain of equality tests joined by OR: status = 'new' OR status = 'open'. It is a per-row test, so duplicates and the order of values in the list change nothing about which rows or how many rows come back.

open as a page

Why does base_salary + bonus come back NULL for some rows, and how do you fix it?

level: juniorimportance: should knowfreq 68%

basics

~10 s

Arithmetic on an absent value is undefined, so any expression with a NULL operand evaluates to NULL — the whole sum vanishes when bonus is missing. Substitute a default first: base_salary + COALESCE(bonus, 0).

open as a page

What does UNION require of the two SELECT lists it combines?

level: juniorimportance: should knowfreq 60%

basics

~20 s

UNION matches the two SELECT lists by position, not by name. Both must produce the same number of columns, and each positional pair must have types the engine can resolve to one common type. Result column names come from the first branch.

open as a page

What changes when a SQL identifier is written in double quotes instead of bare?

level: middleimportance: should knowfreq 42%

basics

~20 s

A double-quoted (delimited) identifier is taken literally: case-sensitive, and free to contain spaces or reserved words. An unquoted identifier is case-insensitive because the engine folds its case, so quoting can make Orders and orders different objects.

open as a page

After writing FROM orders AS o, why does referring to orders.total in the same query fail?

level: middleimportance: should knowfreq 38%

basics

~20 s

A table alias replaces the table name for that query: once orders is given the correlation name o, only o.total is a valid qualified reference. Every occurrence of a table needs its own alias to be addressable separately.

open as a page

Why does CASE status WHEN NULL THEN 'unknown' ELSE status END never return 'unknown'?

level: middleimportance: should knowfreq 45%

basics

~20 s

Simple CASE matches with equality, and status = NULL evaluates to UNKNOWN, never TRUE, so the branch cannot fire; a NULL status falls through to ELSE. Use a searched CASE with WHEN status IS NULL instead.

open as a page

How do you use a CASE expression in ORDER BY to impose a custom priority order?

level: middleimportance: should knowfreq 52%

basics

~20 s

Make CASE map each value to a sort rank and use that expression as a sort key: ORDER BY CASE status WHEN 'urgent' THEN 1 WHEN 'high' THEN 2 ELSE 3 END, created_at. The ranks are sorted, not displayed.

open as a page

How do you correctly negate a compound predicate such as NOT (a = 1 AND b = 2)?

level: middleimportance: should knowfreq 52%

basics

~20 s

NOT binds tighter than AND and OR, so parenthesize whatever you negate. Negating a compound predicate flips the connective by De Morgan: NOT (a = 1 AND b = 2) equals a <> 1 OR b <> 2, and NOT (a = 1 OR b = 2) equals a <> 1 AND b <> 2.

open as a page

showing 1–30 of 60