🗄️

A language tour

The Declarative Power of SQL

Say what you want, not how to find it. A fifty-year-old language that still runs the world's data.

scroll

01 — Philosophy

Describe the goal, not the algorithm

Most languages tell a computer what to do, step by step. SQL tells it what you want, and lets the query optimizer decide how to get there. You describe the shape of the result; the database figures out the joins, the indexes, the execution plan. This is the declarative model — and it's remarkably powerful.

"SQL is the closest thing we have to a universal language for asking questions about structured data. It has survived every wave of the database wars because the idea behind it is simply correct."

— Edgar F. Codd's relational model, first described in 1970
declarative.sql
-- You describe the *shape* of what you want.
-- The database decides how to retrieve it.

SELECT
    c.name,
    o.order_date,
    o.total
FROM   customers c
JOIN   orders o  ON o.customer_id = c.id
WHERE  o.total > 1000
  AND  o.order_date >= DATE_TRUNC('month', CURRENT_DATE - INTERVAL '1 month')
  AND  o.order_date <  DATE_TRUNC('month', CURRENT_DATE)
ORDER BY o.total DESC;

-- No loop. No index management. No iteration logic.
-- The planner chooses the index, the join strategy, the sort method.
-- You only said: "give me this month's big orders, newest first."

The query optimizer may use a B-tree index, a hash join, or a sequential scan — you wrote none of that. You wrote a question, and the database answered it.


02 — The Relational Model

Data spread across tables, reunited

Relational databases store each fact once. JOINs are the mechanism for bringing related facts together at query time. Rather than duplicating data — or nesting it in documents — the relational model normalises it, then composes it on demand. A three-way join that would take pages of code in another language is a single, readable query in SQL.

joins.sql
-- Three tables. One question:
-- "Which UK authors wrote which books in which genres?"

SELECT
    a.name           AS author,
    b.title          AS book,
    g.name           AS genre,
    b.published_year
FROM       authors a
INNER JOIN books   b  ON b.author_id = a.id
INNER JOIN genres  g  ON g.id        = b.genre_id
WHERE a.country = 'UK'
ORDER BY a.name, b.published_year;

-- LEFT JOIN: include authors with no books yet
SELECT
    a.name,
    COUNT(b.id) AS book_count
FROM      authors a
LEFT JOIN books   b ON b.author_id = a.id
GROUP BY a.name
ORDER BY book_count DESC;

LEFT JOIN includes rows from the left table even when no match exists on the right — authors with zero books appear with a count of 0, not silently dropped.


03 — Window Functions

Aggregation without collapsing rows

A regular GROUP BY collapses many rows into one. Window functions are different — they compute across a set of rows but keep every row intact. Running totals, rankings, moving averages: all expressed through the elegant OVER() clause, which defines the window of rows each calculation sees.

window_functions.sql
-- Running total and rank — per category, not globally
SELECT
    product_name,
    category,
    revenue,

    -- OVER() defines the window: "within each category, ordered by revenue"
    SUM(revenue) OVER (
        PARTITION BY category
        ORDER BY     revenue DESC
    ) AS running_total,

    RANK() OVER (
        PARTITION BY category
        ORDER BY     revenue DESC
    ) AS rank_in_category,

    -- Look one row back — no self-join needed
    LAG(revenue) OVER (
        PARTITION BY category
        ORDER BY     revenue DESC
    ) AS prev_revenue

FROM  product_sales
ORDER BY category, rank_in_category;

PARTITION BY resets the window per category — so RANK() = 1 is the top product in each category, not globally. Every source row is preserved.


04 — Common Table Expressions

Complex queries written in steps

A Common Table Expression — the WITH clause — lets you name an intermediate result and reference it like a table. Large queries stop being single monolithic expressions and become a sequence of named, readable steps. Chain them together, and even recursive problems like tree traversal become approachable.

cte_growth.sql
-- Month-over-month revenue growth — built in readable steps

WITH monthly AS (
    -- Step 1: aggregate orders by month
    SELECT
        DATE_TRUNC('month', order_date) AS month,
        SUM(total)                      AS revenue
    FROM  orders
    GROUP BY 1
),

with_prev AS (
    -- Step 2: pull in the previous month's revenue
    SELECT
        month,
        revenue,
        LAG(revenue) OVER (ORDER BY month) AS prev_revenue
    FROM monthly
)

-- Step 3: compute the percentage change
SELECT
    TO_CHAR(month, 'YYYY-MM')              AS month,
    revenue,
    ROUND(
        100.0 * (revenue - prev_revenue)
               / NULLIF(prev_revenue, 0),
    1)                                     AS growth_pct
FROM  with_prev
WHERE prev_revenue IS NOT NULL
ORDER BY month;

NULLIF(prev_revenue, 0) prevents a division-by-zero without an explicit CASE expression — a small example of SQL rewarding careful thought about edge cases.


05 — Transactions & ACID

Correctness as a first-class guarantee

SQL databases don't just store data — they make promises about it. ACID transactions guarantee that a group of operations either all succeed or none of them do. There is no state in which the money has left Alice's account but not arrived in Bob's. This guarantee, taken for granted by most applications, is one of the most important properties in all of computing.

"Atomicity, Consistency, Isolation, Durability. Four words that mean: your data will not lie to you."

— the ACID guarantee, formalised by Jim Gray and Andreas Reuter
transaction.sql
-- A bank transfer: both updates happen, or neither does.

BEGIN;

UPDATE accounts
SET    balance = balance - 500
WHERE  id = 'alice';

-- If this raises an error, the whole transaction is rolled back.
-- Alice's balance stays unchanged. No money disappears.

UPDATE accounts
SET    balance = balance + 500
WHERE  id = 'bob';

COMMIT;

-- Durability: once COMMIT returns, the data is on disk.
-- A power failure after this point loses nothing.

-- SAVEPOINT lets you nest rollback points within a transaction
BEGIN;
    INSERT INTO audit_log (event) VALUES ('transfer_started');
    SAVEPOINT before_debit;
    UPDATE accounts SET balance = balance - 500 WHERE id = 'alice';
    -- can ROLLBACK TO before_debit without losing the audit log entry
COMMIT;

The COMMIT on the last line is a hard promise — the database has flushed to durable storage. Without it, a crash mid-transaction leaves both accounts untouched.


06 — The Whole Picture

More reasons SQL endures

📐

Constraints

NOT NULL, UNIQUE, CHECK, FOREIGN KEY — the database enforces your data rules even when your application code forgets to. The schema is the contract.

🌳

Recursive CTEs

Hierarchies — org charts, bill of materials, file systems — are naturally expressed with WITH RECURSIVE. Tree traversal without writing a tree traversal algorithm.

Indexes

B-trees, hash indexes, partial indexes, expression indexes. The planner chooses automatically — but understanding them turns a 10-second query into a 10-millisecond one.

NULL and Three-Valued Logic

SQL uses three-valued logic: true, false, and unknown. NULL means "no value" — it propagates through expressions and requires explicit IS NULL checks. Misunderstood but precise.

🔍

Subqueries & EXISTS

EXISTS, IN, scalar subqueries, lateral joins — SQL composes queries within queries, giving you the full power of relational algebra in a single statement.

📅

Fifty Years Old, Still Relevant

SQL was formalised in 1974. It has outlived every "SQL killer" that came for it. NewSQL, NoSQL, graph databases — most of them eventually added a SQL interface.