Skip to main content
You write SELECT first. PostgreSQL runs it fifth. That one fact explains half of the confusing errors in SQL: why WHERE can’t see a column name you just made up, why HAVING exists at all, and why LIMIT without ORDER BY gives you random rows. This page is a cheatsheet with a twist. Each part has the SQL you can copy, and the tricky parts have a figure you can play with. Everything uses the same tiny tables, so you can check every answer by hand. It follows the order of the Neon PostgreSQL Tutorial, squeezed onto one page.
In short
  • A query runs in this order: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. A clause can only use what earlier steps made.
  • WHERE filters rows. HAVING filters groups. Test for missing values with IS NULL, never = NULL.
  • INNER JOIN keeps matches only. LEFT JOIN keeps every row on the left and fills the gaps with NULL.
  • ON CONFLICT … DO UPDATE is an upsert: insert, or update the row that is already there.
  • An index lets PostgreSQL skip most of a table. EXPLAIN shows whether it did.

The sample data

Every example uses this film table. Hollow Pine has no price yet, so its price is NULL.

The order a query runs

Take the query in the figure at the top: “which rating has the most films longer than an hour?”
PostgreSQL does not read it top to bottom. It starts with the table, cuts rows, makes groups, cuts groups, and only then builds the columns you asked for. So the name films exists from step 5 on. ORDER BY films works. WHERE films > 1 fails with column "films" does not exist. The full logical order, including the parts not in the example, is FROM and JOIN → WHERE → GROUP BY → HAVING → SELECT (with window functions) → DISTINCT → ORDER BY → LIMIT / OFFSET. This is the order of meaning. The planner may do the work in a different order if the answer is the same.

Filter rows: WHERE

WHERE tests each row and keeps the ones where the test is true. Try each kind of test on the eight films. More ways to shape the rows you get back:
Without ORDER BY, PostgreSQL may return rows in any order, and the order can change between runs. LIMIT without ORDER BY gives you “some” rows, not “the first” rows.

Join tables

A join puts rows from two tables side by side. Here are a customer table and a rental table. Ben has never rented anything. Rental 104 points at customer 4, who doesn’t exist. (A foreign key would stop that; this table has none, to show what each join does with an orphan.) Other join forms:
JOIN on its own means INNER JOIN, and LEFT JOIN means LEFT OUTER JOIN. Avoid NATURAL JOIN: it joins on every column with the same name, so adding a column like updated_at to both tables quietly changes the result.

Group and summarize

GROUP BY folds many rows into one row per group. Aggregate functions (COUNT, SUM, AVG, MIN, MAX) then work inside each group. The rules worth remembering:
  • Every column in SELECT must be grouped or aggregated. SELECT rating, title … GROUP BY rating fails: which title would it show for PG?
  • WHERE before, HAVING after. WHERE length > 60 drops rows before grouping. HAVING COUNT(*) > 1 drops groups after.
  • COUNT(*) counts rows; COUNT(price) counts non-NULL values. On our table they give 8 and 7.

Combine results: UNION, INTERSECT, EXCEPT

A join puts columns side by side. A set operation stacks rows from two queries on top of each other. Both queries must return the same number of columns, with matching types. Use UNION ALL unless you need duplicates removed. Removing them means sorting or hashing every row, which costs time. INTERSECT ALL and EXCEPT ALL also exist and keep duplicates.

Subqueries and CTEs

A subquery is a query inside another query.
One trap: NOT IN (subquery) returns no rows at all if the subquery returns even one NULL. Use NOT EXISTS instead. A CTE (common table expression) names a subquery with WITH, so you can read the query top to bottom:
WITH RECURSIVE lets a CTE refer to itself. It is how SQL walks a tree, such as an org chart. Here staff holds Mia (the boss), Raj and Sol (report to Mia), Tom and Uma (report to Raj) and Val (reports to Tom). The query finds everyone under Raj. Each pass only sees the rows found in the pass before. When a pass finds nothing new, the query stops. If your data has a loop (A manages B, B manages A), it never stops: add a depth limit such as WHERE t.depth < 10.

Change data

An UPDATE or DELETE without WHERE changes every row. Run the same WHERE as a SELECT first.

Insert or update: upsert

What should happen when you insert a row whose key already exists? Plain INSERT fails. ON CONFLICT lets you choose. EXCLUDED is the row you tried to insert. ON CONFLICT needs a unique constraint or primary key on the columns you name. PostgreSQL 15 and later also has MERGE, the SQL-standard way to insert, update or delete in one statement.

Transactions

Group statements so they all happen or none do:
Why this matters is the whole subject of ACID Transactions.

Import and export CSV

COPY reads files on the server and needs special rights. On a hosted database you almost always want psql’s \copy, which reads the file on your machine.

Make and change tables

Constraints at a glance: PRIMARY KEY (unique and not NULL), REFERENCES (a foreign key), UNIQUE, NOT NULL, CHECK (…) and DEFAULT …. Keys and Relationships explains them with examples, including ON DELETE CASCADE. For auto-numbered ids, use GENERATED ALWAYS AS IDENTITY (PostgreSQL 10 and later). The older SERIAL still works and you will see it in tutorials, but it is a shortcut for a separate sequence, which is clumsier to manage. Sequences on their own: CREATE SEQUENCE s;, nextval('s'), currval('s'), setval('s', 100).

Common data types

You can also make your own: CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy'); for a fixed list, or CREATE DOMAIN email AS TEXT CHECK (VALUE LIKE '%@%'); for a type with a rule. Avoid CHAR(n): it pads with spaces and has no speed benefit.

Conditional expressions

Make it fast: indexes and EXPLAIN

Without an index, PostgreSQL finds rows by reading the whole table. An index is a sorted copy of one or more columns with pointers back to the rows. It is usually a B-tree, a few levels deep. EXPLAIN shows the plan PostgreSQL picked.
EXPLAIN ANALYZE really runs the statement. On an UPDATE or DELETE, wrap it in BEGIN; … ROLLBACK;. You will also see Bitmap Index Scan in plans: a middle path that reads the index first, then fetches the table pages in order. Every index speeds up some reads and slows down every write to that table. Add them for the queries you actually run.

Views, functions and triggers

psql commands

psql is PostgreSQL’s command-line client. Commands that start with a backslash belong to psql, not SQL, and need no semicolon.

Recipes

Check yourself

Glossary

For AI agents


Adapted from the Neon PostgreSQL Tutorial. Added beyond it: logical query order, the NOT IN + NULL trap, RIGHT JOIN, FILTER, MERGE (15+), random(min, max) (17+), and modern defaults (GENERATED … AS IDENTITY over SERIAL, TIMESTAMPTZ, JSONB, \copy for hosted databases). The film, customer, staff and stock data are illustrative; figures are simplified teaching models.