P1 · Chapter 12 / 15

SQL / PostgreSQL cheat sheet

The 60-second cheat sheet#

SQL validates stored facts and helps trace integration defects. Use joins and aggregates carefully: one-to-many fanout can duplicate totals. NULL means unknown, not empty string or zero. Prefer authorized read-only investigation, parameterized queries and consistent time windows.

All examples assume hypothetical PostgreSQL tables users(id,email,status), transactions(id,user_id,amount,currency,status,created_at), events(id,user_id,correlation_id,payload,created_at), projections(event_id,user_id,status), staged_transactions(id,amount,currency). Adapt identifiers/schema and use a sandbox.

Filtering, NULL and pagination#

sql
-- 1. Parenthesize AND/OR precedence; half-open timestamp window.
SELECT id, amount, currency FROM transactions
WHERE status IN ('FAILED', 'REJECTED') AND amount >= 100
  AND created_at >= TIMESTAMPTZ '2026-10-01 00:00:00+00'
  AND created_at < TIMESTAMPTZ '2026-10-02 00:00:00+00'
ORDER BY created_at, id LIMIT 50;
sql
-- 2. NULL comparisons require IS NULL; COALESCE sets a reporting default.
SELECT id, COALESCE(email, '(missing)') AS contact,
  CASE WHEN email IS NULL THEN 'MISSING' ELSE 'PRESENT' END AS quality
FROM users WHERE email IS NULL OR email ILIKE '%@example.test';
sql
-- 3. BETWEEN is inclusive on both ends; use intentionally for numeric bounds.
SELECT id FROM transactions WHERE amount BETWEEN 10 AND 20
  AND NOT (status = 'FAILED' OR status = 'REJECTED');
sql
-- 4. Stable keyset pagination; next cursor is last (created_at,id).
SELECT id, created_at FROM transactions
WHERE (created_at, id) > (TIMESTAMPTZ '2026-10-01 12:00:00+00', 'tx-100')
ORDER BY created_at, id LIMIT 100;

OFFSET is simple but large offsets cost more and changing datasets shift page membership. LIKE is case-sensitive in usual PostgreSQL usage; ILIKE is case-insensitive. % matches many characters, _ one; escape when searching literal patterns. NULL = NULL is unknown; WHERE retains only true. NOT IN with a NULL in its set can surprise: prefer NOT EXISTS for nullable anti-joins.

Aggregation and reconciliation#

sql
-- 5. Aggregate per currency: COUNT(*) counts rows, COUNT(amount) excludes nulls.
SELECT currency, COUNT(*) AS rows, COUNT(amount) AS amounts,
       SUM(amount) AS total, AVG(amount) AS mean, MIN(amount), MAX(amount)
FROM transactions GROUP BY currency HAVING COUNT(*) > 1;
sql
-- 6. Duplicate business value; decide normalization rules before grouping.
SELECT email, COUNT(*) FROM users
WHERE email IS NOT NULL GROUP BY email HAVING COUNT(*) > 1;
sql
-- 7. DISTINCT removes repeated result tuples, not an explanation for duplication.
SELECT DISTINCT user_id, status FROM transactions ORDER BY user_id, status;
sql
-- 8. Missing source IDs in target: count equality alone cannot prove completeness.
SELECT s.id FROM staged_transactions s
WHERE NOT EXISTS (SELECT 1 FROM transactions t WHERE t.id = s.id);
sql
-- 9. Mismatched totals with pre-aggregation avoids join fanout.
WITH source AS (
  SELECT currency, SUM(amount) total FROM staged_transactions GROUP BY currency
), target AS (
  SELECT currency, SUM(amount) total FROM transactions GROUP BY currency
)
SELECT COALESCE(s.currency, t.currency) AS currency, s.total AS expected, t.total AS actual
FROM source s FULL JOIN target t USING (currency)
WHERE s.total IS DISTINCT FROM t.total;

JOIN, EXISTS and projections#

Join Retained rows Typical trap
INNER Matching pairs Missing entities disappear
LEFT All left + matched right Right predicate in WHERE can remove unmatched rows
RIGHT All right + matched left Usually clearer rewritten as LEFT
FULL Both sides including unmatched COALESCE keys carefully; NULL comparisons
EXISTS Whether matching row exists Safer than join when only existence needed
sql
-- 10. Orphan transaction (if integrity constraints permit historical bad rows).
SELECT t.id FROM transactions t LEFT JOIN users u ON u.id = t.user_id
WHERE u.id IS NULL;
sql
-- 11. Users with successful transactions, no duplicate user rows.
SELECT u.id FROM users u WHERE EXISTS (
  SELECT 1 FROM transactions t WHERE t.user_id = u.id AND t.status = 'COMPLETED'
);
sql
-- 12. Missing event projection after a controlled watermark/lag window.
SELECT e.id, e.correlation_id FROM events e
LEFT JOIN projections p ON p.event_id = e.id
WHERE p.event_id IS NULL AND e.created_at < CURRENT_TIMESTAMP - INTERVAL '2 minutes';
sql
-- 13. Explicit INNER/RIGHT examples; each event may multiply user rows.
SELECT u.id, e.id AS event_id FROM users u INNER JOIN events e ON e.user_id = u.id;
SELECT u.id, e.id AS event_id FROM users u RIGHT JOIN events e ON e.user_id = u.id;

Window functions and JSONB#

sql
-- 14. Latest event per user; deterministic tie-breaker.
WITH ranked AS (
  SELECT e.*, ROW_NUMBER() OVER (
    PARTITION BY user_id ORDER BY created_at DESC, id DESC
  ) AS rn FROM events e
)
SELECT user_id, id, payload FROM ranked WHERE rn = 1;
sql
-- 15. RANK ties share rank and leave gaps; LAG compares preceding row.
SELECT id, user_id, amount,
  RANK() OVER (PARTITION BY currency ORDER BY amount DESC) AS amount_rank,
  LAG(amount) OVER (PARTITION BY user_id ORDER BY created_at, id) AS previous_amount
FROM transactions;
sql
-- 16. JSONB: -> returns JSON, ->> returns text, @> tests containment.
SELECT id, payload->'identity' AS identity,
       payload->>'status' AS status
FROM events WHERE payload @> '{"status":"VERIFIED"}'::jsonb;
sql
-- 17. UTC representation: timestamptz stores an instant, display depends on session TZ.
SELECT id, created_at AT TIME ZONE 'UTC' AS created_utc
FROM events ORDER BY created_at DESC LIMIT 20;

Transactions, indexes and safe reads#

ACID: atomicity, consistency of declared invariants, isolation and durability. PostgreSQL read committed normally takes a snapshot per statement; repeatable read provides a stable transaction snapshot but serialization anomalies still need the stronger serializable level where required. Serializable transactions can require retry on serialization failure. Do not confuse that with blind replay of external side effects. Read uncommitted behaves as read committed in PostgreSQL. Dirty/nonrepeatable/phantom reads depend on isolation and database implementation.

sql
-- 18. Safe multi-query reconciliation snapshot; rollback ends read-only investigation.
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY;
SELECT COUNT(*) FROM staged_transactions;
SELECT COUNT(*) FROM transactions;
ROLLBACK;
sql
-- 19. EXPLAIN estimates; EXPLAIN ANALYZE actually executes the statement.
EXPLAIN SELECT id FROM transactions WHERE user_id = 'qa-user' ORDER BY created_at DESC;

Indexes accelerate selective predicates/joins/order at storage and write cost; inspect plans and cardinality before proposing indexes. EXPLAIN ANALYZE is risky on writes and expensive queries. Even read-only production queries can load services or expose sensitive data; use authorized replicas, bounded windows and approved access.

typescript
// 20. Scoped node-postgres example; optional `pg` dependency, injected client.
// client.query supports positional parameters; never concatenate untrusted IDs.
const result = await client.query('SELECT id, status FROM users WHERE id = $1', [userId]);

Interview response#

30 seconds: “I validate key completeness, duplicates, relationships and values, not just row counts. I watch join fanout and NULL semantics, use stable UTC windows and a consistent snapshot when needed, and parameterize inputs. For async projections I distinguish allowed lag from permanent missing data.”

90-second senior answer: reconcile two systems

I agree the source of truth, business key, transformation rules, currency precision and time watermark first. I compare missing and unexpected keys in both directions, detect duplicates, then compare attributes and totals within compatible groups. I pre-aggregate before joins so event fanout doesn't inflate financial sums. For a live event-driven system I allow only documented lag and late arrival rules; where appropriate I use a read-only consistent snapshot. Correlation IDs help trace mismatches to ingestion or transformation. I use parameterized queries and bounded authorized reads, inspect execution plans before expensive operations, and report exactly what the snapshot proves.

Memory hook: keys before counts; grain before joins; time before totals.

Quick practice · reveal answers#

Why can LEFT JOIN lose unmatched rows?
  • A right-side condition in WHERE can reject NULL extended rows.
  • Put intended match restrictions in ON or handle NULL explicitly.

Pitfall: Accidentally turning the outer join into inner behavior.

Full explanation →

How do you find the latest row per user?
  • ROW_NUMBER partitioned by user ordered by timestamp DESC plus deterministic ID tie-breaker
  • select rn=1.

Pitfall: MAX(timestamp) alone returns no associated coherent row.

Full explanation →

How do you prevent SQL injection?
  • Parameterized driver query with bound values, approved read-only access and bounded queries.
  • Identifiers need allowlisting rather than value placeholders.

Pitfall: Concatenating an untrusted user ID into SQL.

Full explanation →

SQL totals double after joining events: diagnose.
  • One-to-many fanout changes grain
  • aggregate at intended grain before joining or use EXISTS.

Pitfall: DISTINCT blindly hiding a broken financial query.

Full explanation →

Official references and related topics#

Optional online further reading; all core study material is included above.

Back to library index

Prepared for both Senior automation roles. Hypothetical sandbox contracts; no employer systems or credentials. Content: 16 October 2026.