Appendix C — SQL Quick Reference

A keep-it-next-to-your-keyboard reference for PostgreSQL SQL. Organized by task. (Examples use Mercado.)

SELECT — the anatomy & logical order

SELECT   columns / expressions [AS alias]      -- π  (projection)        -- 5th
FROM     table [alias] [JOIN ...]              --     source             -- 1st
WHERE    row_condition                          -- σ  (selection)         -- 2nd
GROUP BY columns                                --     grouping           -- 3rd
HAVING   group_condition                        --     filter groups      -- 4th
ORDER BY columns [ASC|DESC] [NULLS FIRST|LAST]  --     sort               -- 6th
LIMIT n [OFFSET m];                             --     slice              -- 7th

Logical evaluation: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. (So WHERE can't see SELECT aliases; ORDER BY can.)

WHERE operators

Comparison = <> < > <= >=
Logical AND OR NOT (parenthesize when mixing!)
Range x BETWEEN a AND b (inclusive)
Set x IN (...), x NOT IN (...) (beware NULL)
Pattern LIKE 'a%' (%=any, _=one); ILIKE (case-insensitive)
Null x IS NULL, x IS NOT NULL (never = NULL)

Joins

FROM a JOIN b  ON b.a_id = a.id          -- INNER: matches in both
FROM a LEFT JOIN b ON ...                 -- all of a, NULLs where no b
FROM a LEFT JOIN b ON ... WHERE b.id IS NULL   -- ANTI-join ("a with no b")
FROM a RIGHT JOIN b / FULL OUTER JOIN b   -- mirror / both sides
FROM a CROSS JOIN b                        -- every pairing (Cartesian)
FROM emp e JOIN emp m ON m.id = e.mgr_id   -- self-join (two aliases)

Aggregation

COUNT(*) | COUNT(col) | COUNT(DISTINCT col) | SUM | AVG | MIN | MAX
SELECT g, COUNT(*) FROM t GROUP BY g HAVING COUNT(*) > 1;
COUNT(*) FILTER (WHERE cond)              -- conditional aggregate
GROUP BY ROLLUP (a) | CUBE (a,b) | GROUPING SETS (...)   -- subtotals

Aggregates skip NULLs; COUNT(*) counts rows. Across a one-to-many join use COUNT(DISTINCT key).

Subqueries

WHERE x > (SELECT AVG(x) FROM t)          -- scalar
WHERE id IN (SELECT id FROM ...)          -- list
FROM (SELECT ... ) sub                     -- derived table (alias required)
WHERE EXISTS (SELECT 1 FROM b WHERE b.a_id = a.id)        -- existence
WHERE NOT EXISTS (...)                      -- NULL-safe anti-join (prefer over NOT IN)

Set operations

q1 UNION q2        -- in either (dedup)        q1 UNION ALL q2   -- keep dups (faster)
q1 INTERSECT q2    -- in both                   q1 EXCEPT q2      -- in q1 not q2 (difference)

Union-compatible (same column count/types); one ORDER BY at the end.

CTEs & recursion

WITH step1 AS (...), step2 AS (... step1 ...) SELECT ... FROM step2;
WITH RECURSIVE t AS (
    <anchor>            UNION ALL    <recursive term referencing t>
) SELECT ... FROM t;     -- guard cycles: CYCLE col SET is_cycle USING path

Window functions

fn() OVER (PARTITION BY g ORDER BY o [frame])
ROW_NUMBER() | RANK() | DENSE_RANK() | NTILE(n)          -- ranking
SUM(x) OVER (ORDER BY t)                                  -- running total
LAG(x) / LEAD(x) OVER (ORDER BY t)                        -- adjacent rows
AVG(x) OVER (ORDER BY t ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)   -- moving avg

Top-N-per-group: ROW_NUMBER() in a subquery, then WHERE rn <= N (can't filter windows in WHERE).

Data modification (DML)

INSERT INTO t (a,b) VALUES (1,2),(3,4) RETURNING id;     -- name columns!
INSERT INTO t (...) SELECT ...;                            -- from a query
UPDATE t SET a = a+1 WHERE ...;                            -- never without WHERE
DELETE FROM t WHERE ...;          TRUNCATE t [RESTART IDENTITY];
INSERT INTO t (...) VALUES (...) ON CONFLICT (key) DO UPDATE SET ...;   -- upsert
-- safe habit: SELECT the WHERE first → BEGIN → change → verify → COMMIT/ROLLBACK

Data definition (DDL)

CREATE TABLE t (
  id    integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  email text NOT NULL UNIQUE,
  qty   integer NOT NULL CHECK (qty > 0) DEFAULT 0,
  fk_id integer REFERENCES other(id) ON DELETE CASCADE,
  PRIMARY KEY (a, b)             -- composite (table constraint)
);
ALTER TABLE t ADD COLUMN c text;  ALTER TABLE t ALTER COLUMN c SET NOT NULL;
DROP TABLE [IF EXISTS] t [CASCADE];
CREATE [UNIQUE] INDEX [CONCURRENTLY] idx ON t (col);
CREATE VIEW v AS SELECT ...;   CREATE MATERIALIZED VIEW mv AS ...;  REFRESH MATERIALIZED VIEW [CONCURRENTLY] mv;

Common functions (Appendix-level; see Ch. 8)

-- strings: || concat (NULL-propagating), concat(), length, upper/lower/initcap, trim,
--          substring(s from a for b), position, replace, split_part, lpad/rpad
-- numbers: round, ceil, floor, trunc, abs, mod (mind integer division: 5/2=2, use 5.0/2)
-- dates:   now(), current_date, extract(field FROM ts), date_trunc('month', ts),
--          age(a,b), ts + interval '1 day', to_char(ts,'YYYY-MM-DD')
--          (use half-open ranges: >= start AND < next_start)
-- cast:    CAST(x AS type) or x::type
-- cond:    CASE WHEN ... THEN ... ELSE ... END, COALESCE(a,b), NULLIF(x,0), GREATEST/LEAST

JSONB (Ch. 16)

data -> 'k'    -- as jsonb      data ->> 'k'   -- as text
data @> '{"k":1}'   -- contains (GIN-indexable)    data ? 'k'   -- key exists
CREATE INDEX ON t USING GIN (data);

Transactions & locking

BEGIN [ISOLATION LEVEL READ COMMITTED | REPEATABLE READ | SERIALIZABLE];
... ; COMMIT | ROLLBACK;   SAVEPOINT s; ROLLBACK TO SAVEPOINT s;
SELECT ... FOR UPDATE;     -- pessimistic row lock

Diagnostics

EXPLAIN [ANALYZE] [BUFFERS] <query>;   ANALYZE [table];   VACUUM [table];
\d t   \di   \timing   \conninfo
SELECT * FROM pg_stat_activity;        -- running queries

See also: Appendix D (data types), Appendix E (relational algebra), Appendix G (EXPLAIN/tuning), Appendix I (cookbook), Appendix J (dialect differences).