The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Keep matching rowsSELECT o.id, c.name FROM orders AS o INNER JOIN customers AS c ON c.id = o.customer_id;View examples
Alias joined tablesSELECT o.id, c.name FROM orders AS o JOIN customers AS c ON c.id = o.customer_id;View examples
Join shared column namesSELECT order_id, status FROM orders JOIN shipments USING (order_id);View examples
Keep every left rowSELECT c.name, o.id FROM customers AS c LEFT JOIN orders AS o ON o.customer_id = c.id;View examples
Find unmatched left rowsSELECT c.id FROM customers AS c LEFT JOIN orders AS o ON o.customer_id = c.id WHERE o.id IS NULL;View examples
Filter joined matchesSELECT c.name, o.id FROM customers AS c LEFT JOIN orders AS o ON o.customer_id = c.id AND o.status = 'paid';View examples
Keep every right rowSELECT c.name, o.id FROM customers AS c RIGHT JOIN orders AS o ON o.customer_id = c.id;View examples
Keep both sidesSELECT a.email, s.email FROM app_users AS a FULL OUTER JOIN subscribers AS s ON s.email = a.email;View examples
Merge an outer-join keySELECT COALESCE(a.email, s.email) AS email FROM app_users AS a FULL JOIN subscribers AS s ON s.email = a.email;View examples
Create every combinationSELECT c.color, s.size FROM colors AS c CROSS JOIN sizes AS s;View examples
Join a table to itselfSELECT e.name, m.name AS manager FROM employees AS e LEFT JOIN employees AS m ON m.id = e.manager_id;View examples
Chain several joinsSELECT o.id, c.name, p.name FROM orders AS o JOIN customers AS c ON c.id = o.customer_id JOIN products AS p ON p.id = o.product_id;View examples
Match multiple columnsSELECT i.sku, p.price FROM inventory AS i JOIN prices AS p ON p.sku = i.sku AND p.region = i.region;View examples
Count related rowsSELECT c.id, COUNT(o.id) AS order_count FROM customers AS c LEFT JOIN orders AS o ON o.customer_id = c.id GROUP BY c.id;View examples
Sum related valuesSELECT c.id, COALESCE(SUM(o.total), 0) AS revenue FROM customers AS c LEFT JOIN orders AS o ON o.customer_id = c.id GROUP BY c.id;View examples
Join a correlated subquerySELECT c.id, latest.id FROM customers AS c LEFT JOIN LATERAL ( SELECT o.id FROM orders AS o WHERE o.customer_id = c.id ORDER BY o.created_at DESC LIMIT 1 ) AS latest ON true;View examples

A join combines rows according to an explicit relationship. Start from the rows the result must preserve, qualify shared column names, place relationship predicates in ON or USING, and remember that filters applied after an outer join can remove its null-extended rows.

Step by step

Detailed examples

01

Join matching rows with ON or USING

An inner join returns one output row for every pair that satisfies its condition. ON supports any Boolean relationship and keeps both join columns, while USING is concise for identically named columns and emits that column once. Qualify columns through aliases whenever more than one input could supply the same name.

Orders with known customers
WITH customers (id, name) AS (
  VALUES (1, 'Ada'), (2, 'Grace')
), orders (id, customer_id) AS (
  VALUES (101, 1), (102, 1), (103, 3)
)
SELECT o.id, c.name
FROM orders AS o
INNER JOIN customers AS c ON c.id = o.customer_id
ORDER BY o.id;
Output
id  | name
----+-----
101 | Ada
102 | Ada
Shared order identifier with USING
WITH orders (order_id) AS (VALUES (101), (102)),
shipments (order_id, status) AS (VALUES (101, 'sent'), (103, 'pending'))
SELECT order_id, status
FROM orders
JOIN shipments USING (order_id);
Output
order_id | status
---------+-------
101      | sent
Back to quick reference ↑
02

Preserve left rows and place filters correctly

A left join first matches rows and then adds a null-extended result for each unmatched left row. A right-side restriction in ON changes which rows match while preserving the left side; the same restriction in WHERE runs afterward and can remove the null-extended rows, effectively changing the result toward an inner join.

All customers and only paid orders
WITH customers (id, name) AS (
  VALUES (1, 'Ada'), (2, 'Grace'), (3, 'Linus')
), orders (id, customer_id, status) AS (
  VALUES (101, 1, 'paid'), (102, 1, 'pending'), (103, 2, 'paid')
)
SELECT c.name, o.id
FROM customers AS c
LEFT JOIN orders AS o
  ON o.customer_id = c.id AND o.status = 'paid'
ORDER BY c.id;
Output
name  | id
------+----
Ada   | 101
Grace | 103
Linus | NULL
Customers without orders
WITH customers (id) AS (VALUES (1), (2), (3)),
orders (id, customer_id) AS (VALUES (101, 1), (102, 2))
SELECT c.id
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
WHERE o.id IS NULL;
Output
id
--
3
Back to quick reference ↑
03

Preserve the right side or both sides

A right join is the mirror of a left join, though rewriting it as a left join often makes the preserved input easier to see. A full outer join preserves unmatched rows from both sides. When a full join uses differently sourced copies of the same logical key, COALESCE can project the non-null value into one result column.

Preserve orders with missing customers
WITH customers (id, name) AS (VALUES (1, 'Ada')),
orders (id, customer_id) AS (VALUES (101, 1), (102, 9))
SELECT c.name, o.id
FROM customers AS c
RIGHT JOIN orders AS o ON o.customer_id = c.id
ORDER BY o.id;
Output
name | id
-----+----
Ada  | 101
NULL | 102
Combine application users and subscribers
WITH app_users (email) AS (VALUES ('ada@example.com'), ('linus@example.com')),
subscribers (email) AS (VALUES ('ada@example.com'), ('grace@example.com'))
SELECT COALESCE(a.email, s.email) AS email,
       a.email IS NOT NULL AS has_account,
       s.email IS NOT NULL AS subscribes
FROM app_users AS a
FULL OUTER JOIN subscribers AS s ON s.email = a.email
ORDER BY email;
Output
email             | has_account | subscribes
------------------+-------------+-----------
ada@example.com   | true        | true
grace@example.com | false       | true
linus@example.com | true        | false

Note: RIGHT JOIN and FULL OUTER JOIN are available in PostgreSQL but are not implemented by every SQL database.

Back to quick reference ↑
04

Generate deliberate combinations with CROSS JOIN

A cross join returns every possible pairing, so N rows joined with M rows produce N times M results. This is useful for small option matrices, calendars, and test data, but an accidental or unbounded cross join can grow extremely quickly.

Product color and size combinations
WITH colors (color) AS (VALUES ('blue'), ('green')),
sizes (size) AS (VALUES ('S'), ('M'))
SELECT c.color, s.size
FROM colors AS c
CROSS JOIN sizes AS s
ORDER BY c.color, s.size;
Output
color | size
------+-----
blue  | M
blue  | S
green | M
green | S
Back to quick reference ↑
05

Relate rows within one table

A self join treats the same table as two separate inputs by assigning a distinct alias to each role. Use an outer join when a top-level row such as an organization leader has no related parent and must remain visible.

Employees and their managers
WITH employees (id, name, manager_id) AS (
  VALUES (1, 'Ada', NULL), (2, 'Grace', 1), (3, 'Linus', 1)
)
SELECT e.name, m.name AS manager
FROM employees AS e
LEFT JOIN employees AS m ON m.id = e.manager_id
ORDER BY e.id;
Output
name  | manager
------+--------
Ada   | NULL
Grace | Ada
Linus | Ada
Back to quick reference ↑
06

Chain joins through explicit relationships

Each join in a chain adds another table expression and should state its own key relationship. A composite relationship requires every key component in the ON condition. Verify expected cardinality before adding more tables because multiple one-to-many joins can multiply result rows.

Orders with customer and product names
WITH customers (id, name) AS (VALUES (1, 'Ada')),
products (id, name) AS (VALUES (10, 'Keyboard')),
orders (id, customer_id, product_id) AS (VALUES (101, 1, 10))
SELECT o.id, c.name AS customer, p.name AS product
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id
JOIN products AS p ON p.id = o.product_id;
Output
id  | customer | product
----+----------+---------
101 | Ada      | Keyboard
Price by SKU and region
WITH inventory (sku, region) AS (VALUES ('A-1', 'EU'), ('A-1', 'US')),
prices (sku, region, price) AS (VALUES ('A-1', 'EU', 12.00), ('A-1', 'US', 10.00))
SELECT i.sku, i.region, p.price
FROM inventory AS i
JOIN prices AS p ON p.sku = i.sku AND p.region = i.region
ORDER BY i.region;
Back to quick reference ↑
07

Aggregate related rows without losing empty groups

Group after a left join to summarize child rows while retaining parents with no matches. COUNT of a right-side non-null key returns zero for an unmatched parent, whereas SUM returns NULL and commonly needs COALESCE. Confirm that earlier joins have not duplicated the values being aggregated.

Order counts and revenue per customer
WITH customers (id) AS (VALUES (1), (2)),
orders (id, customer_id, total) AS (VALUES (101, 1, 25.00), (102, 1, 15.00))
SELECT c.id,
       COUNT(o.id) AS order_count,
       COALESCE(SUM(o.total), 0) AS revenue
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
GROUP BY c.id
ORDER BY c.id;
Output
id | order_count | revenue
---+-------------+--------
1  | 2           | 40.00
2  | 0           | 0
Back to quick reference ↑
08

Evaluate a joined subquery for each source row

LATERAL allows a FROM subquery to reference columns from preceding inputs. A left lateral join with ON true is useful for selecting an optional top row per source row, such as the latest order. Index the correlated filtering and ordering columns for large tables because the subquery is evaluated in relation to each left row.

Latest order for every customer
WITH customers (id) AS (VALUES (1), (2)),
orders (id, customer_id, created_at) AS (
  VALUES (101, 1, DATE '2026-01-01'), (102, 1, DATE '2026-02-01')
)
SELECT c.id AS customer_id, latest.id AS latest_order_id
FROM customers AS c
LEFT JOIN LATERAL (
  SELECT o.id
  FROM orders AS o
  WHERE o.customer_id = c.id
  ORDER BY o.created_at DESC
  LIMIT 1
) AS latest ON true
ORDER BY c.id;
Output
customer_id | latest_order_id
------------+----------------
1           | 102
2           | NULL

Note: LATERAL is a PostgreSQL-supported SQL feature; check the target database before relying on the exact syntax.

Back to quick reference ↑

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. PostgreSQL Global Development GroupTable Expressionspostgresql.org
  2. PostgreSQL Global Development GroupJoins Between Tablespostgresql.org
  3. PostgreSQL Global Development GroupSELECTpostgresql.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback