zanith

LATERAL joins

A subquery inside FROM that can reference earlier tables in the same FROM list. The standard answer to top-N per group and a handful of other patterns plain LEFT JOINcan't express.

The signature

TS
lateralJoin(
joinType: 'INNER' | 'LEFT' | 'CROSS',
alias: string,
subquery: CompiledQuery,
onCondition?: string, // default 'TRUE'
dialect?: Dialect, // default postgresDialect
): LateralJoin // { sql: string; params: unknown[] }

lateralJoin returns a fragment, not a full query — splice the sql into your parent query's FROM list and merge params into the parent's parameter list. onCondition defaults to TRUE because the lateral correlation itself is usually the implicit join.

Top-3 posts per author

TS
import { lateralJoin } from 'zanith';
 
const subquery = {
sql: `SELECT id, title, created_at
FROM posts
WHERE author_id = u.id
ORDER BY created_at DESC
LIMIT 3`,
params: [],
};
 
const join = lateralJoin('LEFT', 'recent', subquery);
// join.sql = LEFT JOIN LATERAL (SELECT …) AS "recent" ON TRUE
 
const sql = `
SELECT u.id, u.email, recent.title, recent.created_at
FROM users u
${join.sql}
`;
const { rows } = await adapter.executeRaw(sql, join.params);

The lateral subquery sees u.id from the outer FROM— that's the whole reason for using LATERAL. A non-lateral LEFT JOIN can't reference u.id from inside its own subquery.

Three join types

TypeWhen to use
'LEFT'Want the outer row even when the lateral subquery returns nothing.
'INNER'Drop outer rows whose lateral subquery is empty.
'CROSS'No condition needed (the lateral correlation does the work). Equivalent to INNER with ON TRUE.

When LATERAL is the right tool

PatternWhy LATERAL
Top-N per groupWindow functions can do it but cost more on large groups; LATERAL+LIMIT lets the planner use an index.
A computed value used by both SELECT and WHERECompute once in a lateral, reference in both clauses.
Iterating a row's array column with subquery filtersunnest() inside the lateral, then filter.
Per-row aggregates that depend on the outer row's valuesThe lateral subquery can reference the outer row directly.

Parameter binding

The subquery argument is a CompiledQuery — its params become join.paramsverbatim. The subquery's placeholder numbers must match the position they will hold in the parent query's final parameter list (renumber if you're composing several sources).