Basic SELECT Queries and Projection

Every question anyone ever asks a database eventually compiles down to one shape: pick some rows, keep some columns. A payroll clerk asking "who earns more than $80,000 in the Sales department?" is issuing a fundamental operation. So is a data scientist asking "give me the timestamps and error codes from the last hour." That operation is the SQL `SELECT` statement. Its two core moves — selection (choosing rows) and projection (choosing columns) — are the atoms from which every more elaborate query is built. This matters because relational databases are deliberately restrictive. They expose a small, closed set of operations that a query optimizer can reason about. This lets them execute efficiently over data sets far larger than memory. Understanding `SELECT-FROM-WHERE` precisely matters — not just "it filters stuff." You need its exact evaluation semantics, its treatment of duplicates, and how predicates are constructed. This is the foundation for everything that follows, from joins to aggregation to subqueries. Get this base layer wrong, and every later concept becomes a guess instead of a derivation. That includes a `JOIN` condition, a `GROUP BY` filter, or a correlated subquery. The big picture is that SQL is a declarative language: you state what result you want, not how to compute it. The `SELECT` statement is the syntactic surface of the relational algebra operations of projection (Π) and selection (σ). The database's query optimizer decides the actual execution plan — index scan, table scan, sort — invisibly to the user. Because the language is declarative, correctness hinges on knowing the precise, official evaluation order of clauses. That order is not the order they are written in. This article gives you the exact structural grammar of `SELECT-FROM-WHERE`, the logical order of evaluation, and the semantics of `DISTINCT`. It also covers multi-valued (three-valued) predicate logic with `NULL`, a full worked trace over a concrete table, and the classic traps. These traps — especially around `NULL` and duplicate handling — catch nearly every SQL beginner at least once.

Key takeaways
  • `SELECT-FROM-WHERE` maps directly onto the relational algebra: projection (Π), selection (σ), and Cartesian product/join (×).
  • Clauses are written as SELECT-FROM-WHERE but logically evaluated as FROM → WHERE → SELECT — this order explains many "surprising" behaviors.
  • Projection can produce duplicate rows unless you add `DISTINCT`, which requires an extra duplicate-elimination step (typically a sort or hash).
  • Predicates use three-valued logic (`TRUE`/`FALSE`/`UNKNOWN`) because of `NULL`, and only rows evaluating to `TRUE` survive `WHERE`.
  • Column expressions (arithmetic, string concatenation, `CASE`) can appear in the `SELECT` list and are computed per row after filtering.
  • This is the seed operation for Joins in SQL, Aggregation and the GROUP BY Clause, and Set Operations in SQL — all of them extend this same skeleton.
The core insight

`SELECT` is projection over a filtered, possibly duplicate-laden result — and its meaning is fixed by a strict logical evaluation order that differs from its written order.

The idea

A query in the relational model asks for a subset of the rows (selection) and a subset of the columns (projection) of one or more tables, optionally transformed by expressions. In relational algebra these are two independent operators:

σpredicate(R)— selection: keep tuples satisfying a condition\sigma_{\text{predicate}}(R) \quad \text{— selection: keep tuples satisfying a condition}
Πattributes(R)— projection: keep only the listed attributes (and remove duplicate tuples)\Pi_{\text{attributes}}(R) \quad \text{— projection: keep only the listed attributes (and remove duplicate tuples)}

SQL's `SELECT-FROM-WHERE` block realizes both at once: ```sql SELECT [DISTINCT] column_expr_1 [AS alias_1], column_expr_2, ... FROM table_or_tables WHERE predicate; ``` The `FROM` clause names the source relation(s) — for now, think of a single table; multi-table `FROM` clauses correspond to a Cartesian product and are properly explored under Joins in SQL. The `WHERE` clause is a predicate: a boolean-valued expression evaluated independently for each row of the source, built from comparison operators (`=`, `<>`, `<`, `>`, `<=`, `>=`), boolean connectives (`AND`, `OR`, `NOT`), pattern matching (`LIKE`), range and membership tests (`BETWEEN`, `IN`), and null tests (`IS NULL`). The `SELECT` clause is the projection list: a sequence of column references or arbitrary scalar expressions (arithmetic, string functions, `CASE` expressions) that determines the shape of the output, evaluated only for rows that survived the `WHERE` filter. Crucially, relational algebra projection is set-based — Π always removes duplicate tuples. SQL, however, models tables as multisets (bags) by default, so plain `SELECT` preserves duplicates; you must explicitly write `SELECT DISTINCT` to get true set semantics. This divergence between the algebra's mathematical purity and SQL's pragmatic bag semantics is one of the first places students' mental models need correcting, and it has real performance consequences: `DISTINCT` is not free — it forces the engine to detect and remove duplicate rows, typically via a sort or hash pass, adding a nontrivial cost that plain projection does not incur.

How it works

The written order vs. the logical evaluation order

SQL queries are written `SELECT ... FROM ... WHERE ...`, but the standard defines their meaning via a logical processing order that starts with `FROM`: 1. FROM — construct the source relation (for a single table, just that table; for multiple tables, the Cartesian product, refined later by `JOIN ... ON`). 2. WHERE — evaluate the predicate against each row of the FROM-result; keep only rows where it evaluates to `TRUE`. 3. SELECT — evaluate the projection list's expressions against each surviving row, producing the output columns. 4. DISTINCT (if present) — eliminate duplicate output rows from the projected result. This ordering is why you cannot reference a `SELECT`-list alias inside the same query's `WHERE` clause in standard SQL: at the point `WHERE` is logically evaluated, the `SELECT` list has not been computed yet. It also explains why aggregate functions and computed aliases behave the way they do once `GROUP BY` and `HAVING` enter the picture (foreshadowed, not covered, here).

Predicate evaluation: three-valued logic

A `WHERE` predicate does not simply return `TRUE`/`FALSE` — SQL uses three-valued logic (3VL): `TRUE`, `FALSE`, `UNKNOWN`. Any comparison involving a `NULL` operand (e.g. `salary > 50000` when `salary IS NULL`) evaluates to `UNKNOWN`, not `FALSE`. The `WHERE` clause keeps a row only if the predicate evaluates to `TRUE` — rows yielding `UNKNOWN` are discarded, exactly like `FALSE` rows, but they are not logically equivalent, which matters once you combine conditions with `NOT` or `OR`. Truth tables for 3VL (T = true, F = false, U = unknown): ``` AND | T F U OR | T F U NOT ----+--------- ----+--------- ---- T | T F U T | T T T T -> F F | F F F F | T F U F -> T U | U F U U | T U U U -> U ```

Step-by-step execution model

```pseudocode function EXECUTE_SELECT(table, predicate, projectionList, distinct): sourceRows = READ_ALL_ROWS(table) # FROM clause filteredRows = [] for row in sourceRows: # WHERE clause result = EVALUATE_PREDICATE(predicate, row) # returns TRUE, FALSE, or UNKNOWN if result == TRUE: filteredRows.append(row)

# FALSE or UNKNOWN rows are dropped projectedRows = [] for row in filteredRows: # SELECT clause outRow = [] for expr in projectionList: outRow.append(EVALUATE_EXPRESSION(expr, row)) projectedRows.append(outRow) if not distinct: return projectedRows # bag semantics, duplicates kept

# DISTINCT: duplicate elimination (conceptually a sort or hash pass) seen = HASH_SET() result = [] for row in projectedRows: key = HASH(row) if key not in seen: seen.add(key) result.append(row) return result ``` ```pseudocode function EVALUATE_PREDICATE(predicate, row):

# handles comparisons, AND/OR/NOT, IS NULL, LIKE, BETWEEN, IN

# returns one of {TRUE, FALSE, UNKNOWN} per three-valued logic match predicate: case Comparison(left, op, right): lval = EVALUATE_EXPRESSION(left, row) rval = EVALUATE_EXPRESSION(right, row) if lval is NULL or rval is NULL: return UNKNOWN return APPLY_OP(op, lval, rval) # TRUE or FALSE case IsNull(expr): return TRUE if EVALUATE_EXPRESSION(expr, row) is NULL else FALSE case And(p1, p2): return AND_3VL(EVALUATE_PREDICATE(p1, row), EVALUATE_PREDICATE(p2, row)) case Or(p1, p2): return OR_3VL(EVALUATE_PREDICATE(p1, row), EVALUATE_PREDICATE(p2, row)) case Not(p1): return NOT_3VL(EVALUATE_PREDICATE(p1, row))

# ... Between, In, Like handled as sugar over Comparison/Or ``` A production query engine never literally does a full table scan followed by a full-row filter loop — the query optimizer rewrites this logical plan into a physical plan that may use an index to jump directly to qualifying rows, push the projection earlier to shrink intermediate row width, or evaluate `DISTINCT` via sorting so duplicates become adjacent. The pseudocode above describes the logical semantics that any physical plan must reproduce exactly, regardless of the shortcuts it takes.

Figure
Logical evaluation pipeline for SELECT-FROM-WHERE
1. FROM Employee id | name | dept | salary 6 rows (raw table) 2. WHERE dept='Sales' AND salary>60000 3-valued predicate drops F and UNKNOWN 3. SELECT name, salary projection list (bag semantics) 4. DISTINCT (opt.) hash/sort pass removes duplicate output rows (only if DISTINCT written) Written order: SELECT → FROM → WHERE Logical/evaluation order: FROM → WHERE → SELECT → DISTINCT

Complexity & correctness

Time complexity. Let nn be the number of rows in the source table and kk the number of columns projected.

  • A full table scan to evaluate `WHERE` against every row costs O(n)O(n) predicate evaluations, each O(1)O(1) or O(p)O(p) where pp is the number of atomic conditions in the predicate — so scan-based filtering is O(n⋅p)O(n \cdot p).
  • If an index exists on a column used in an equality or range predicate, evaluation can be sped up to O(log⁡n+m)O(\log n + m) where mm is the number of matching rows (a B-tree index lookup plus retrieval), which is why indexing is central to query performance — a topic developed alongside physical query processing.
  • Projection itself, absent `DISTINCT`, is O(n⋅k)O(n \cdot k): for every surviving row, compute kk output expressions. It is a linear pass, no comparison between rows required.
  • `SELECT DISTINCT` requires detecting duplicate output rows. The two standard strategies are:
  • Sort-based: sort the mm projected rows by all output columns, O(mlog⁡m)O(m \log m), then scan once to drop adjacent duplicates, O(m)O(m). Total O(mlog⁡m)O(m \log m).
  • Hash-based: insert each row into a hash set keyed by its full column tuple; average O(m)O(m) expected time (assuming a well-behaved hash function and no adversarial collisions), O(m)O(m) space for the set.

So plain projection is linear, but `DISTINCT` adds an O(mlog⁡m)O(m \log m) or expected-O(m)O(m) overhead and O(m)O(m) auxiliary space — never assume `DISTINCT` is "free," especially over large intermediate results.

Space complexity. A streaming execution of `WHERE` + `SELECT` needs only O(1)O(1) extra memory per row (or O(k)O(k) for the current output row buffer) if the engine can emit rows as they are produced — this is the basis of pipelined query execution. `DISTINCT`, by contrast, needs O(m)O(m) space to hold the hash set or to materialize the sort, because duplicate detection is inherently non-local: you cannot know a row is a duplicate without remembering rows you've already emitted.

Correctness argument. The semantics of `SELECT-FROM-WHERE` is correct by definition once the logical evaluation order is fixed, so "correctness" here means proving that any physical execution strategy produces the same bag of output rows as the canonical logical order. The key invariant a query optimizer must preserve is: > Predicate soundness invariant: a row rr appears in the WHERE-filtered set if and only if `EVALUATE_PREDICATE(predicate, r) = TRUE` under three-valued logic, evaluated using the original row's column values (not partially transformed ones). Any rewrite — pushing a selection below a projection, using an index instead of a scan, reordering conjuncts in an `AND` — is valid only if it preserves this invariant for every row. This is why, for example, a query optimizer may freely reorder the conjuncts of `WHERE a AND b` (evaluating `b` first if it's cheaper) as long as it correctly implements 3VL `AND`, but it may not silently treat `x > 5` as `FALSE` when `x` is `NULL` — that would violate the invariant and change which rows are kept versus discarded in queries involving `NOT`.

Tradeoffs. Bag semantics (plain `SELECT`) is faster and cheaper because no duplicate-detection pass is needed, but it can return misleading results when the caller actually wants distinct answers (e.g., "which departments have at least one high earner?" run without `DISTINCT` may repeat a department name once per qualifying employee). `DISTINCT`, conversely, gives set semantics but costs an extra sort/hash pass and forces the engine to fully materialize the projected result before it can guarantee no duplicate will appear later — this can break pipelining and increase memory pressure on large result sets.

Worked example

Consider a table created (per SQL Data Definition and Schema Creation) as: ```sql CREATE TABLE Employee ( id INT PRIMARY KEY, name VARCHAR(30), dept VARCHAR(20), salary INT ); ``` with contents:

idnamedeptsalary
1AliciaSales72000
2BenSales55000
3CarlaSales91000
4DeepakEngineering91000
5ElenaMarketingNULL
6FaridSales91000

Run: ```sql SELECT dept, salary FROM Employee WHERE dept = 'Sales' AND salary > 60000; ```

Step 1 — FROM: the source relation is all 6 rows of `Employee`.

Step 2 — WHERE: evaluate `dept = 'Sales' AND salary > 60000` per row using 3VL.

iddept='Sales'?salary>60000?AND resultkept?
1TRUETRUETRUE✅ yes
2TRUEFALSE (55000)FALSE❌ no
3TRUETRUETRUE✅ yes
4FALSE (Eng.)TRUEFALSE❌ no
5FALSE (Mktg.)UNKNOWN (NULL)FALSE❌ no
6TRUETRUETRUE✅ yes

Row 5 is instructive: even though its `dept` comparison alone is `FALSE`, note `AND` with `UNKNOWN` still yields `FALSE` here (from the 3VL `AND` table: `FALSE AND anything = FALSE`), so it is correctly excluded regardless of the salary being unknown.

Step 3 — SELECT (projection onto `dept, salary` for the 3 surviving rows):

deptsalary
Sales72000
Sales91000
Sales91000

Notice `91000` appears twice — rows 3 and 6 are different employees (Carla, Farid) but project to an identical `(dept, salary)` pair. Plain `SELECT` (bag semantics) returns 3 rows, with a duplicate.

Step 4 — add DISTINCT: ```sql SELECT DISTINCT dept, salary FROM Employee WHERE dept = 'Sales' AND salary > 60000; ``` Now a duplicate-elimination pass runs over the 3 projected rows (sort or hash on `(dept, salary)`), collapsing the two `('Sales', 91000)` rows into one:

deptsalary
Sales72000
Sales91000

Result: 2 rows. This single example demonstrates the full pipeline — FROM (6 rows) → WHERE (3 survive, with a correctly-handled `NULL` case) → SELECT (3 projected rows, one duplicate pair) → DISTINCT (2 final rows) — and shows concretely why `DISTINCT` is a separate, costed operation rather than an automatic property of projection.

Common pitfalls & traps

PitfallWhy it happensFix
Assuming `WHERE x <> 5` returns rows where `x IS NULL``NULL <> 5` is `UNKNOWN`, not `TRUE`; `UNKNOWN` rows are dropped by `WHERE`Add explicit `OR x IS NULL` if nulls should be included
Forgetting `DISTINCT` and being surprised by repeated rowsPlain `SELECT` uses bag semantics; duplicates in the projected columns are never auto-removedAdd `DISTINCT` when you want set semantics, and expect a cost
Referencing a `SELECT`-list alias inside the same query's `WHERE``WHERE` is evaluated logically before `SELECT`, so the alias doesn't exist yet at that pointRepeat the full expression in `WHERE`, or use a subquery/CTE
Using `NOT (x > 5)` expecting it to behave like `x <= 5` when `x` can be `NULL``NOT UNKNOWN = UNKNOWN`, not `TRUE`; the row is still dropped either wayUse `x <= 5 OR x IS NULL` if nulls must be included
Comparing strings with `=` expecting case-insensitivitySQL string comparison is typically case-sensitive (collation-dependent)Use `UPPER()`/`LOWER()` or a case-insensitive collation explicitly
Believing `DISTINCT` applies per-column when multiple columns are listed`DISTINCT` deduplicates the whole output row (the full tuple), not each column independentlyUnderstand `SELECT DISTINCT a, b` removes rows identical in both `a` and `b` together

Pioneers & history

Origins · 1 minEdgar F. Codd and how this was discovered
EF
Edgar F. Codd
1923–2003 · IBM Research, relational database theory

Edgar Codd, a mathematician working at IBM's San Jose research laboratory, published the paper that made `SELECT-FROM-WHERE` possible in principle: a 1970 proposal to represent data as relations — mathematical sets of tuples — manipulated by a small closed algebra of operators including selection and projection. Codd's insight was that separating the logical structure of data from its physical storage would let programmers state what they wanted without specifying how to retrieve it, a radical departure from the navigational, pointer-chasing database systems of the 1960s. His relational model directly motivated the design of SQL (initially SEQUEL) by Donald Chamberlin and Raymond Boyce at IBM, and it won Codd the 1981 Turing Award. Every `WHERE` clause a student writes today is, formally, an instance of Codd's selection operator σ.

History
1970
— Codd publishes "A Relational Model of Data for Large Shared Data Banks," defining relational algebra including selection and projection.
1974
— Chamberlin and Boyce introduce SEQUEL, IBM's prototype query language directly implementing SELECT-FROM-WHERE syntax.
1979
— Oracle (then Relational Software Inc.) ships the first commercially available SQL-based database system.
1986
— ANSI publishes SQL-86, the first standardized version of SQL, formalizing clause syntax and evaluation semantics.
1992
— SQL-92 formalizes three-valued logic and NULL handling rules that remain the semantic backbone of WHERE evaluation today.

Where this matters

`SELECT-FROM-WHERE` with projection is the operation executed, in some form, by essentially every application that touches structured data: a web server rendering a user's order history runs a filtered, projected `SELECT` against an orders table; a business intelligence dashboard issues thousands of such queries per hour against a data warehouse; an object-relational mapper (ORM) like Hibernate or Django's ORM translates method calls into exactly this SQL skeleton behind the scenes. Every relational database system — PostgreSQL, MySQL, Oracle, SQL Server, SQLite — implements this logical evaluation order as a hard constraint on its query optimizer, and every query plan visualizer (`EXPLAIN`) shows, in some form, the scan-filter-project-dedupe pipeline traced above. Understanding this skeleton is also the prerequisite for reading and writing more advanced constructs: a `JOIN` (see Joins in SQL) is really just an extended `FROM` clause feeding the same `WHERE`/`SELECT` pipeline; `GROUP BY` (see Aggregation and the GROUP BY Clause) inserts a grouping step between `WHERE` and `SELECT`; and `UNION`/`INTERSECT`/`EXCEPT` (see Set Operations in SQL) combine the outputs of multiple such pipelines using true set semantics, echoing the `DISTINCT` mechanics introduced here.

Ask the tutor: How does adding a `JOIN` change the FROM-WHERE-SELECT evaluation order and the complexity analysis you just learned?

Why does SQL use bag semantics instead of pure set semantics like relational algebra?

Pure set semantics would require the database engine to check every new row against every previously produced row before emitting it, which is expensive and breaks pipelined, streaming execution. Real workloads also frequently want duplicates preserved — for instance, listing every order a customer placed, even if two orders happen to share the same product and price. SQL's designers chose bags as the default so that projection is a cheap, linear, streaming operation, and made `DISTINCT` an explicit, separately-costed choice for when true set semantics are actually needed. This mirrors a general database design principle: make the cheap, common case the default, and require an explicit keyword for the more expensive guarantee.

Why is the logical evaluation order FROM-WHERE-SELECT instead of the written order SELECT-FROM-WHERE?

SQL's designers separated the textual order, chosen for English-like readability ("select what, from where, given what condition"), from the logical order needed for the semantics to make sense. You cannot filter rows (`WHERE`) using a projection (`SELECT`) that hasn't been computed yet, so `FROM` must supply raw rows first, `WHERE` must filter them next, and only then can `SELECT` compute output expressions on the surviving rows. This is why an alias defined in the `SELECT` list generally cannot be reused in that same query's `WHERE` clause — logically, `WHERE` runs before the alias exists.

Does adding DISTINCT ever change which rows are read from the base table?

No. `DISTINCT` operates strictly after `FROM` and `WHERE` have determined which rows qualify and after `SELECT` has computed the projected columns for those rows. It only removes duplicate output rows from the already-projected result; it cannot cause additional base-table rows to be included or excluded. This is an important distinction from a `WHERE` predicate, which does control which base rows are considered at all — confusing the two responsibilities is a common source of subtle query bugs.

Can the SELECT list contain expressions, not just column names?

Yes. The projection list may contain arbitrary scalar expressions evaluated per surviving row: arithmetic (`salary * 1.1`), string operations (`first_name || ' ' || last_name`), conditional logic (`CASE WHEN salary > 100000 THEN 'high' ELSE 'normal' END`), and function calls. Each expression is computed independently for every row that passed the `WHERE` filter, and the result becomes one output column, optionally renamed with `AS alias`. This is still "projection" in the generalized sense used by real SQL engines — the algebraic Π operator is extended beyond simple attribute selection to arbitrary computed columns.

How does an index change the complexity of a SELECT with a WHERE clause?

Without an index, the engine must scan all nn rows to evaluate the predicate, costing O(n)O(n). With a B-tree index on a column used in an equality or range condition, the engine can navigate directly to qualifying rows in O(log⁡n)O(\log n) time and then retrieve the mm matches in O(m)O(m) time, for a total of O(log⁡n+m)O(\log n + m) — dramatically faster when m≪nm \ll n. The logical semantics of the query are completely unaffected; only the physical execution strategy changes, which is exactly the separation between declarative meaning and physical plan that makes SQL's optimizer possible.

What is the difference between selection and projection, precisely?

Selection (σ) chooses a subset of rows from a relation based on a predicate, keeping all columns unchanged; it corresponds to the `WHERE` clause. Projection (Π) chooses a subset of columns (or computed expressions) from a relation, potentially discarding some columns entirely, and in the pure algebra also removes duplicate resulting tuples; it corresponds to the `SELECT` clause (with `DISTINCT` needed in SQL to fully match algebraic projection). The two are orthogonal and composable — you always apply selection to determine which rows, then projection to determine which columns/expressions, matching the FROM-WHERE-SELECT logical order.

Why does WHERE dept='Sales' AND salary>60000 exclude a row whose salary is NULL, even if dept really is 'Sales'?

Because the second conjunct, `salary > 60000`, evaluates to `UNKNOWN` when `salary` is `NULL`, and by the three-valued `AND` truth table, `TRUE AND UNKNOWN = UNKNOWN`. Since `WHERE` keeps only rows where the predicate is exactly `TRUE`, an `UNKNOWN` result is discarded just like a `FALSE` one. This is a frequent source of confusion: the row is not excluded because `dept` failed to match, but because the overall conjunction could not be proven true given missing salary data — the database refuses to guess.

Before this

Where this leads