Reference

SQL cheat sheet

The SQL of the ten days on one page, written against Harbour Mutual's tables. Every line runs: open it in the playground to see what it does.

Day 1 · The environment

  • SELECT * FROM products;
    Every column and row of a table.
  • SELECT COUNT(*) FROM customers;
    Count the rows.
  • CREATE TABLE notes (note_id SERIAL PRIMARY KEY, note TEXT NOT NULL);
    A table with a numbered key.
  • INSERT INTO notes (note) VALUES ('first'), ('second');
    Add rows.

Day 2 · SELECT mastery

  • SELECT policy_number, annual_premium FROM policies WHERE status = 'active' AND annual_premium > 500;
    Filter rows.
  • SELECT * FROM policies ORDER BY annual_premium DESC LIMIT 10 OFFSET 10;
    Sort, then take a page.
  • SELECT * FROM customers WHERE name ILIKE 'sofia%';
    Match text, ignoring case.
  • SELECT claim_number, reported_date - loss_date AS days_to_report FROM claims;
    Date arithmetic: days between two dates.
  • SELECT COALESCE(broker_id, 0), ROUND(annual_premium / 12, 2) FROM policies;
    Replace a null; round.

Day 3 · Joins and relationships

  • SELECT c.claim_number, p.policy_number FROM claims c JOIN policies p USING (policy_id);
    Inner join: matching rows only.
  • SELECT b.name, p.policy_id FROM brokers b LEFT JOIN policies p USING (broker_id);
    Left join: every broker, with or without policies.
  • SELECT cu.name FROM customers cu LEFT JOIN policies p USING (customer_id) WHERE p.policy_id IS NULL;
    Anti-join: customers without a policy.

Day 4 · Aggregations and reporting

  • SELECT cause, COUNT(*), SUM(reserve), AVG(reserve) FROM claims GROUP BY cause;
    Summarise per group.
  • SELECT broker_id, COUNT(*) FROM policies GROUP BY broker_id HAVING COUNT(*) > 400;
    Filter groups.
  • SELECT COUNT(*) FILTER (WHERE status = 'open') FROM claims;
    Count only some rows.
  • SELECT * FROM claims WHERE reserve > (SELECT AVG(reserve) FROM claims WHERE status = 'open');
    A subquery as a value.

Day 5 · Constraints and indexes

  • ALTER TABLE notes ADD CONSTRAINT note_not_empty CHECK (note <> '');
    A rule every row must meet.
  • ALTER TABLE notes ADD CONSTRAINT note_unique UNIQUE (note);
    No two rows with the same value.
  • CREATE INDEX IF NOT EXISTS claims_policy_id ON claims (policy_id);
    Speed up lookups and joins on a column.

Day 6 · Views, CTEs and window functions

  • WITH open_claims AS (SELECT * FROM claims WHERE status = 'open') SELECT cause, COUNT(*) FROM open_claims GROUP BY cause;
    A CTE: a named step.
  • CREATE OR REPLACE VIEW open_claims AS SELECT * FROM claims WHERE status = 'open';
    Save a query as a view.
  • SELECT broker_id, COUNT(*), RANK() OVER (ORDER BY COUNT(*) DESC) FROM policies WHERE broker_id IS NOT NULL GROUP BY broker_id;
    Rank rows without grouping them away.
  • SELECT paid_on, amount, SUM(amount) OVER (ORDER BY paid_on, payment_id) AS running_total FROM claim_payments;
    A running total.
  • SELECT claim_id, paid_on, LAG(paid_on) OVER (PARTITION BY claim_id ORDER BY paid_on) FROM claim_payments;
    The previous row's value, per claim.

Day 7 · Functions and triggers

  • CREATE OR REPLACE FUNCTION double_it(p NUMERIC) RETURNS NUMERIC LANGUAGE SQL AS $$ SELECT p * 2 $$;
    A SQL function.
  • CREATE OR REPLACE FUNCTION no_blank() RETURNS TRIGGER LANGUAGE PLPGSQL AS $$ BEGIN IF TRIM(NEW.note) = '' THEN RAISE EXCEPTION 'blank note'; END IF; RETURN NEW; END $$;
    A trigger function refusing a row.
  • CREATE OR REPLACE TRIGGER no_blank BEFORE INSERT OR UPDATE ON notes FOR EACH ROW EXECUTE FUNCTION no_blank();
    Run it on every insert and update.

Day 8 · Query optimisation

  • EXPLAIN SELECT * FROM claims WHERE policy_id = 42;
    The plan, not the result.
  • EXPLAIN ANALYZE SELECT * FROM claims WHERE loss_date >= DATE '2025-12-01';
    Run it, with real times and rows.
  • CREATE INDEX IF NOT EXISTS claims_open_by_date ON claims (loss_date) WHERE status = 'open';
    A partial index: open claims only.
  • ANALYZE claims;
    Refresh the statistics the planner uses.

Day 9 · JSONB and extensions

  • SELECT doc ->> 'adjuster', doc -> 'fraud_flags' FROM claim_documents;
    ->> gives text, -> JSON.
  • SELECT COUNT(*) FROM claim_documents WHERE doc @> '{"channel": "web"}';
    Contains this key and value.
  • SELECT i ->> 'item', (i ->> 'cost')::NUMERIC FROM claim_documents, JSONB_ARRAY_ELEMENTS(doc -> 'items') i;
    A JSON array into rows.
  • CREATE INDEX IF NOT EXISTS claim_documents_doc ON claim_documents USING GIN (doc jsonb_path_ops);
    Index JSONB for @>.
  • CREATE EXTENSION IF NOT EXISTS pg_trgm; SELECT name FROM customers WHERE name % 'Sofia Schafer';
    Similar spellings.

Day 10 · Security, roles and backup

  • CREATE ROLE analyst NOLOGIN; GRANT SELECT ON ALL TABLES IN SCHEMA public TO analyst;
    A read-only role.
  • REVOKE SELECT ON customers FROM analyst;
    Take a right away.
  • SET ROLE analyst; SELECT COUNT(*) FROM claims;
    Act as that role (the lab resets it after the run).
  • BEGIN; UPDATE claims SET reserve = reserve * 1.1 WHERE status = 'open'; ROLLBACK;
    Try a change, then undo it.
  • CREATE TABLE claims_backup AS SELECT * FROM claims;
    A snapshot before a risky change.
An unhandled error has occurred. Reload 🗙

Reconnecting…

Your connection to Data Analytics SQL Lab dropped, probably a Wi-Fi blip. Hold on while it reconnects. Your work is kept.

The connection could not be restored. The page will reload in a few seconds.