fivenines
4/25

Theory Tutorial

SQL as a Language of Relations

Time
6m
Level
not specified
Artifacts
theory + practice
Progress0%

Build Your Own PostgreSQL

SQL as a Language of Relations

Time
6 min
Prerequisites
The Wire Protocol And Session Lifecycle

What You Will Learn

  • Describe a SQL query as a transformation from input relations to an output relation
  • Separate declarative meaning from the physical order used to produce rows
  • Trace selection, joining, grouping, and projection through one query
relationtupleselectionprojectionjoinaggregationnulldeclarative query

The table that does not exist yet

A bookstore stores customers and invoices in separate tables, but a manager asks for customer names and unpaid balances. No table contains that answer. Application code could fetch both tables and assemble the report with loops, yet that would commit the caller to one algorithm and move unnecessary data across the network. SQL starts from a different contract: describe the relation wanted, even when that relation is not stored anywhere.

A relation is not a file, a screen grid, or a list of objects. It is a set-like collection of tuples with named attributes. Real SQL adds duplicates, nulls, ordering at the edges, and many pragmatic details, but the central idea holds: a query transforms table-shaped inputs into a table-shaped output. The database engine can rearrange many of those transformations because the user describes meaning rather than a step-by-step loop.

From loops to relations

Consider a query that asks for customers with unpaid invoices. One mental model says, "For each customer, look through invoices, keep the ones that match, then print a few fields." That is an algorithm. A relational model says, "Combine customers and invoices where the customer identifiers match, keep rows whose payment status is unpaid, and project the columns needed by the result." The second phrasing gives the optimizer room to choose an index, change join order, or push a predicate earlier.

Projection chooses attributes. Selection filters tuples. A join combines relations by a condition. Aggregation collapses many tuples into grouped summaries. Ordering arranges the final result for presentation. These operations are small enough to understand alone but expressive enough to compose. SQL's surface syntax can be irregular, yet beneath it is a compact language of relational transformations.

Relational shape

Rendering diagram…

Where the abstraction helps

It helps to contrast row-at-a-time thinking with relation-at-a-time thinking. Row-at-a-time thinking mirrors application code. It feels direct because you can imagine loops and conditionals. It also hides opportunities. If the application asks the database for all customers and then loops through invoices itself, it has forced data movement across the network and taken planning choices away from the engine. Relation-at-a-time thinking gives the database the whole shape of the question.

The opposite mistake is to imagine relations as purely abstract and cost-free. A join is a clean logical operation, but physically it can mean nested loops, hashing, sorting, index probes, memory pressure, temporary files, and many pages read from disk. SQL works because the language is separated from execution, not because execution disappears. The system must preserve relational meaning while eventually choosing a concrete physical strategy.

Nulls complicate the clean picture. In SQL, a predicate can be true, false, or unknown. This affects filters, joins, constraints, and indexes. Backend engineers often expect boolean logic to be binary, but SQL's treatment of missing or inapplicable values is part of its contract. PostgreSQL must carry types and nullability through analysis and planning because a transformation that is safe for ordinary values can be wrong when unknown enters the expression.

Names also matter. A column reference such as `id` is not meaningful by itself when several relations have an `id`. SQL allows aliases, schemas, implicit search paths, function calls, operators, casts, and star expansion. Before planning can begin, the database must know what every name refers to and what type every expression has. The surface language points toward relations, but it does not yet contain enough information to execute safely.

Trace the unpaid-balance relation

Start with every customer tuple and every invoice tuple. The join condition matches tuples with the same customer identifier; it does not prescribe whether PostgreSQL scans customers first, builds a hash table, or probes an index. Selection then keeps matches whose status is unpaid. Grouping collects those tuples by customer, aggregation sums each group's amount, and projection exposes only the name and balance requested by the report.

The invariant across all valid implementations is the resulting relation. PostgreSQL may push the unpaid predicate into the invoice scan, choose a different join order, or aggregate before a later join when equivalence rules allow it. Those changes are valuable only if they preserve SQL's treatment of duplicates, nulls, types, and visible ordering. Relational freedom is therefore constrained freedom: execution may change; meaning may not.

The written order is a decoy

The order of SQL clauses can also mislead. A select list appears near the front, but conceptually it is often shaped after from, join, where, grouping, and aggregate computation. This mismatch between written order and logical order is one reason SQL parsing alone is insufficient. The database must transform the text into a representation where meaning is explicit, then bind that representation to catalog objects and types.

SQL creates a contract between user and engine. The user describes the desired relation with names, predicates, and expressions. The engine finds a valid way to produce it under the current transaction, permissions, and data state. This is the source of optimization: if the user supplied imperative loops, changing the plan would change the program; because the user supplies relational meaning, the engine can search for better routes.

From here the database faces a language problem. SQL text arrives as characters, and characters do not yet know about clauses, operators, precedence, or nested subqueries. The next layer must turn a stream of tokens into a tree that preserves the structure of the relational statement.

Three relational checks

For any clause, ask which input relation it consumes, which output relation it produces, and which observations—duplicates, nulls, types, or order—it may change. This discipline turns a long SQL statement into composed transformations without pretending they are the executor's steps. It also reveals why an apparently harmless rewrite can be wrong when an observation the user requested becomes visible.

Into parsing

That tree is the first place where the written question becomes an object the rest of the system can safely transform.

Next step

See what actually stuck.

Take the practice scenarios now.