The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Keep matching rows | SELECT o.id, c.name
FROM orders AS o
INNER JOIN customers AS c
ON c.id = o.customer_id; | View examples |
| Alias joined tables | SELECT o.id, c.name
FROM orders AS o
JOIN customers AS c
ON c.id = o.customer_id; | View examples |
| Join shared column names | SELECT order_id, status
FROM orders
JOIN shipments
USING (order_id); | View examples |
| Keep every left row | SELECT c.name, o.id
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.id; | View examples |
| Find unmatched left rows | SELECT 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 matches | SELECT 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 row | SELECT c.name, o.id
FROM customers AS c
RIGHT JOIN orders AS o
ON o.customer_id = c.id; | View examples |
| Keep both sides | SELECT 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 key | SELECT 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 combination | SELECT c.color, s.size
FROM colors AS c
CROSS JOIN sizes AS s; | View examples |
| Join a table to itself | SELECT 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 joins | SELECT 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 columns | SELECT 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 rows | SELECT 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 values | SELECT 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 subquery | SELECT 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
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.
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; id | name
----+-----
101 | Ada
102 | AdaWITH 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); order_id | status
---------+-------
101 | sentPreserve 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.
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; name | id
------+----
Ada | 101
Grace | 103
Linus | NULLWITH 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; id
--
3Preserve 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.
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; name | id
-----+----
Ada | 101
NULL | 102WITH 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; email | has_account | subscribes
------------------+-------------+-----------
ada@example.com | true | true
grace@example.com | false | true
linus@example.com | true | falseNote: RIGHT JOIN and FULL OUTER JOIN are available in PostgreSQL but are not implemented by every SQL database.
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.
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; color | size
------+-----
blue | M
blue | S
green | M
green | SRelate 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.
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; name | manager
------+--------
Ada | NULL
Grace | Ada
Linus | AdaChain 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.
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; id | customer | product
----+----------+---------
101 | Ada | KeyboardWITH 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; 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.
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; id | order_count | revenue
---+-------------+--------
1 | 2 | 40.00
2 | 0 | 0Evaluate 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.
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; customer_id | latest_order_id
------------+----------------
1 | 102
2 | NULLNote: LATERAL is a PostgreSQL-supported SQL feature; check the target database before relying on the exact syntax.
Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



