SQL models by industry
Four models teams actually build: an order book, a financial ledger, inventory with reservations and an event stream. Each starts with the tables, because the shape of the tables decides which questions stay cheap.
Retail: an order book
Orders, their lines, and the products they point at. The reporting questions are all aggregates over a join.
The tables
shop.products (id PK, name, brand, price)
shop.orders (id PK, customer_id, status, placed_at)
shop.lines (id PK, order_id -> orders.id, product_id -> products.id, qty)Revenue by brand, last month
One statement, joined and grouped in the engine, rather than three round trips and a loop in your service.
SELECT p.brand,
SUM(l.qty * p.price) AS revenue,
COUNT(DISTINCT o.id) AS orders
FROM shop.lines l
JOIN shop.orders o ON l.order_id = o.id
JOIN shop.products p ON l.product_id = p.id
WHERE o.status = 'paid'
GROUP BY p.brand
ORDER BY revenue DESCThe same catalog, searched
The rows you just queried are the rows full-text and vector search see. Nothing is copied, so a product added by an INSERT is searchable immediately — see the search quickstart.
SELECT id, name FROM shop.products WHERE brand = 'Aero' ORDER BY price ASCFinancial services: a ledger
Append-only entries, balances derived rather than stored, and money kept in minor units so totals never drift.
The tables
fin.accounts (id PK, holder, currency, opened_at)
fin.entries (id PK, account_id -> accounts.id, amount_minor, kind, booked_at)Balance per account
Derived from the entries, so it cannot disagree with them.
SELECT a.id, a.holder, a.currency,
SUM(e.amount_minor) AS balance_minor
FROM fin.accounts a
JOIN fin.entries e ON e.account_id = a.id
GROUP BY a.id, a.holder, a.currencyRow-level security and column masking are enforced by the engine, not by your query. A caller scoped to one desk gets that desk's rows from this exact statement, and a masked column stays masked in an aggregate. You do not add a filter for it, and you cannot forget to.
Logistics: inventory with reservations
Stock on hand, minus what is promised. The trap is reading availability and acting on it after someone else already did.
The tables
wh.stock (sku PK, on_hand)
wh.reservations (id PK, sku -> stock.sku, qty, state, made_at)What is actually available
Subtract live reservations from stock in one statement.
SELECT s.sku,
s.on_hand,
COALESCE(SUM(r.qty), 0) AS reserved,
s.on_hand - COALESCE(SUM(r.qty), 0) AS available
FROM wh.stock s
LEFT JOIN wh.reservations r
ON r.sku = s.sku AND r.state = 'held'
GROUP BY s.sku, s.on_handReading availability and then reserving is two steps, and another writer fits between them. Do the read and the write in one transaction — see transactions — so the reservation either wins or fails cleanly.
Product analytics: an event stream
High write volume, and questions that are almost always time-bucketed.
The tables
app.events (id PK, user_id, name, props_json, at)Daily active users
Grouped in the engine over the whole table, not sampled in a job.
SELECT DATE_TRUNC('day', at) AS day,
COUNT(DISTINCT user_id) AS dau
FROM app.events
WHERE at >= '2026-09-01T00:00:00Z'
GROUP BY day
ORDER BY dayA query a dashboard runs every minute is a materialized view. Declare it once and read it as a table — see materialized views.
What these have in common
- The tables are the design. Every cheap question later comes from a key or a foreign key you declared now.
- One store. These same rows answer full-text, vector, graph and natural-language queries. Nothing is exported to a warehouse to be asked a question.
- Policy is enforced below your query. Row-level security and masking apply to the statement itself, including inside aggregates.