Understanding PostgreSQL LATERAL Joins with Practical Examples
PostgreSQL has a powerful feature called Lateral joins, which lets you reference columns from earlier tables in the FROM clause. This becomes especially useful when working with data that changes over time, like historical addresses or versioned records.
For example, consider a domain where we keep track of customers and statements. Customers can change their addresses over time, but it’s important that a statement has the appropriate historical address for a customer.
CREATE TABLE addresses (
customer_id INTEGER NOT NULL REFERENCES customers(id),
address TEXT NOT NULL,
effective_on DATE NOT NULL,
PRIMARY KEY (customer_id, effective_on)
);
CREATE TABLE statements (
id SERIAL PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(id),
statement_date DATE NOT NULL,
balance NUMERIC(10, 2) NOT NULL
);
I have created some sample data:
SELECT
customers.id
, customers.name
, addresses.address
, addresses.effective_on
FROM
customers
INNER JOIN
addresses
ON
customers.id = addresses.customer_id
;
id | name | address | effective_on
----+-------+-------------+--------------
1 | Alice | 123 Main St | 2024-01-01
1 | Alice | 456 Oak Ave | 2024-04-01
2 | Bob | 789 Pine Rd | 2024-02-15
Think of the Lateral join as a kind of “for-each loop” inside SQL: for each row in statements, it runs a subquery on addresses using the statement_date. Without LATERAL
, this would require a more verbose join with DISTINCT ON
, a window function, or a nested subquery.
SELECT
statements.id
, statements.statement_date
, customers.name
, statement_addresses.address
FROM
statements
JOIN LATERAL (
SELECT
addresses.address
FROM
addresses
WHERE
addresses.customer_id = statements.customer_id
AND
addresses.effective_on <= statements.statement_date
ORDER BY
addresses.effective_on DESC
LIMIT 1
) statement_addresses
ON
TRUE
JOIN
customers
ON
customers.id = statements.customer_id
;
id | statement_date | name | address
----+----------------+-------+-------------
1 | 2024-02-01 | Alice | 123 Main St
2 | 2024-05-01 | Alice | 456 Oak Ave
3 | 2024-03-01 | Bob | 789 Pine Rd
Over the past few years, I’ve become more of an advocate for tracking changes over time and using SQL to reconstruct the state of data at any given point. PostgreSQL’s LATERAL
joins make this approach even more powerful, enabling elegant, performant queries that access related, time-sensitive records with minimal fuss.