The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Join a correlated subquery | SELECT c.id, x.id
FROM app.customers c
CROSS JOIN LATERAL (
SELECT id
FROM app.orders
WHERE customer_id = c.id
) x; | View examples |
| Return top one per row | SELECT c.id, x.id
FROM app.customers c
CROSS JOIN LATERAL (
SELECT id
FROM app.orders
WHERE customer_id = c.id
ORDER BY created_at DESC
LIMIT 1
) x; | View examples |
| Make lateral explicit | SELECT t.id, f.value
FROM app.things t
CROSS JOIN LATERAL app.expand(t.payload) AS f(value); | View examples |
| Preserve unmatched outer rows | SELECT c.id, x.id
FROM app.customers c
LEFT JOIN LATERAL (
SELECT id
FROM app.orders
WHERE customer_id = c.id
LIMIT 1
) x
ON true; | View examples |
| Filter after optional expansion | SELECT c.id, x.id
FROM app.customers c
LEFT JOIN LATERAL app.lookup(c.id) x
ON true
WHERE x.id IS NULL; | View examples |
| Generate integers | SELECT n FROM generate_series(1, 10, 1) AS g(n); | View examples |
| Generate daily timestamps | SELECT ts
FROM generate_series(timestamp '2026-08-01', timestamp '2026-08-07', interval '1 day') AS g(ts); | View examples |
| Expand an array | SELECT value FROM unnest(ARRAY[10,20,30]) AS u(value); | View examples |
| Add ordinality | SELECT value, position
FROM unnest(ARRAY[10,20])
WITH ORDINALITY AS u(value, position); | View examples |
| Zip arrays in FROM | SELECT *
FROM unnest(ARRAY[1,2], ARRAY['a','b']) AS u(id, label); | View examples |
| Expand a JSON array | SELECT value
FROM jsonb_array_elements('[1,2,3]'::jsonb) AS j(value); | View examples |
| Project JSON objects | SELECT *
FROM jsonb_to_recordset('[{"id":1}]'::jsonb) AS x(id integer); | View examples |
| Declare estimated SRF rows | ALTER FUNCTION app.recent_orders(bigint, integer) ROWS 10; | View examples |
| Call a table function | SELECT * FROM app.recent_orders(42, 5); | View examples |
| Inspect without execution | EXPLAIN
SELECT *
FROM app.customers c
CROSS JOIN LATERAL app.recent_orders(c.customer_id, 3) r; | View examples |
| Measure lateral loops | EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM app.customers c
CROSS JOIN LATERAL app.recent_orders(c.customer_id, 3) r; | View examples |
LATERAL lets a FROM item refer to columns from items on its left, enabling parameterized subqueries evaluated per input row. Set-returning functions expand one value into rows; used deliberately in FROM with explicit aliases, they make cardinality and outer-row preservation much clearer than target-list expansion.
Step by step
Detailed examples
Use LEFT JOIN LATERAL to retain unmatched rows
CROSS JOIN LATERAL removes an outer row when the right side returns no rows. LEFT JOIN LATERAL with ON true preserves it and null-extends right-side columns, which is ideal for optional latest-child queries. Put correlation filters inside the lateral subquery so LIMIT applies to the intended set.
SELECT c.customer_id, latest.order_id, latest.created_at
FROM app.customers c
LEFT JOIN LATERAL (
SELECT o.order_id, o.created_at
FROM app.orders o
WHERE o.customer_id = c.customer_id
ORDER BY o.created_at DESC, o.order_id DESC
LIMIT 1
) latest ON true; Generate bounded rows with explicit types
generate_series supports integer, numeric, timestamp, and timezone-aware timestamptz forms. Inclusive endpoints and step sign affect results; zero step is an error and NULL input returns no rows. Bound series generation before joining because row counts multiply quickly.
SELECT d.day, coalesce(sum(o.total), 0) AS revenue
FROM generate_series(date '2026-08-01', date '2026-08-07', interval '1 day') AS d(day)
LEFT JOIN app.orders o ON o.created_at >= d.day AND o.created_at < d.day + interval '1 day'
GROUP BY d.day ORDER BY d.day; Expand arrays with position and aligned semantics
unnest expands array elements in storage order. WITH ORDINALITY adds a one-based row number independent of the array lower bound, useful for reconstructing presentation order. Multiple arrays in FROM-function syntax are padded with NULL to the longest input, so validate lengths when pairwise alignment is required.
SELECT p.product_id, tag.value, tag.position
FROM app.products p
CROSS JOIN LATERAL unnest(p.tags) WITH ORDINALITY AS tag(value, position)
ORDER BY p.product_id, tag.position; Expand JSON with declared relational types
jsonb_array_elements returns raw JSON values, while jsonb_to_recordset projects object fields into explicitly declared SQL types. Missing fields become NULL and incompatible values raise errors. Treat untrusted payloads as validation input, cap document size, and use LEFT JOIN LATERAL when empty arrays should not discard the parent.
SELECT o.order_id, item.sku, item.qty
FROM app.orders o
CROSS JOIN LATERAL jsonb_to_recordset(o.items) AS item(sku text, qty integer)
WHERE item.qty > 0; Note: Validate object shape at ingestion if conversion errors must not abort reporting queries.
Declare custom SRF cardinality and volatility honestly
Functions returning SETOF or TABLE are usable as FROM items. ROWS supplies the planner an expected row count and COST estimates per returned row for set-returning functions; inaccurate values distort join choices. Volatility, leakproofness, security-definer search paths, and parallel safety must reflect actual behavior.
CREATE FUNCTION app.recent_orders(p_customer bigint, p_limit integer DEFAULT 10)
RETURNS TABLE(order_id bigint, created_at timestamptz)
LANGUAGE sql STABLE PARALLEL SAFE ROWS 10
AS $$
SELECT o.order_id, o.created_at
FROM app.orders o
WHERE o.customer_id = p_customer
ORDER BY o.created_at DESC
LIMIT p_limit
$$; Note: Clamp p_limit in trusted code if callers can request unbounded results; audit PARALLEL SAFE before using it.
Control cardinality before per-row work becomes expensive
A lateral plan can execute its inner side once per outer row, although the planner may memoize or transform safe cases. Use EXPLAIN ANALYZE carefully to check loops and actual rows, and filter the outer relation early. Read queries take ordinary ACCESS SHARE locks; functions that write, lock rows, or call volatile code add their own transaction and replication consequences.
EXPLAIN (ANALYZE, BUFFERS)
SELECT c.customer_id, x.order_id
FROM app.customers c
CROSS JOIN LATERAL (
SELECT order_id FROM app.orders o
WHERE o.customer_id = c.customer_id
ORDER BY created_at DESC LIMIT 3
) x
WHERE c.active; Note: The inner node loops often match qualifying customers; test against production-scale cardinalities.
Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
- PostgreSQL Global Development GroupTable Expressionspostgresql.org
- PostgreSQL Global Development GroupSELECTpostgresql.org
- PostgreSQL Global Development GroupSet Returning Functionspostgresql.org
- PostgreSQL Global Development GroupArray Functions and Operatorspostgresql.org
- PostgreSQL Global Development GroupJSON Functions and Operatorspostgresql.org
- PostgreSQL Global Development GroupCREATE FUNCTIONpostgresql.org
- PostgreSQL Global Development GroupEXPLAINpostgresql.org
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



