Learn  /  Guides

SQL ASOF JOIN explained: match each row to the latest value, in every engine

What was the price when the trade happened? Which exchange rate applied on the invoice date? What did the model know at prediction time? These are ASOF questions, and in 2026 almost every engine has a join for them. Here is how it works, with examples we ran.

Guide9 min readUpdated 9 November 2026
SQLPythonData analysis
All guides and cheat sheets

A normal join matches rows when keys are equal. Many real questions need something else: the row with the latest timestamp at or before another timestamp. The price of a share at the moment of a trade. The exchange rate in force on the day of an invoice. The customer's plan when they opened a support ticket. That is an ASOF JOIN ("as of" this time). Snowflake, DuckDB, ClickHouse and Polars have it, and Apache Spark added it in version 4.3. This guide explains the idea once, then shows the syntax in each tool.

The idea, with one example

We will use the same two small tables everywhere. Each trade should get the latest quote for the same symbol at or before the trade time.

trade_idsymtrade timeLatest quote at or beforeprice
1A10:00:0510:00:0010.0
2A10:00:2010:00:1010.5
3B10:00:01none yet (first B quote is 10:00:15)NULL
4B10:00:3010:00:1520.0

Notice trade 2: there is also a quote at 10:00:25, but it came after the trade, so it must not be used. And trade 3 has no earlier quote at all. What happens to it depends on the join type, which is the most common source of bugs.

How an ASOF JOIN thinks
Equal keyssame symbol, customer or account
Inequalityright time at or before left time
Pick onethe closest match only
Keep or dropLEFT keeps rows with no match

The syntax in each engine

Every example below returns the prices 10.0, 10.5, NULL and 20.0. We ran the DuckDB 2.0, Spark 4.3, Polars 2.1 and pandas 3.1 versions ourselves.

SQL
SELECT t.trade_id, t.sym, t.ts, q.price
FROM trades t
ASOF JOIN quotes q
  MATCH_CONDITION (t.ts >= q.ts)
  ON t.sym = q.sym;

MATCH_CONDITION holds the one inequality (>=, >, <= or <); ON holds the equal keys. Rows with no match come back with NULLs, like a left join.

No ASOF JOIN? Use this fallback

BigQuery and PostgreSQL have no ASOF keyword, but two patterns give the same answer. Both returned identical results to the ASOF versions in our tests.

QUALIFY

BigQuery and most engines
Idea
join every earlier quote, then keep the latest per trade
  • Short and readable
  • Builds all earlier matches first, so it can be slow on big tables

Fine for thousands to millions of rows.

LATERAL

Best in PostgreSQL
Idea
for each trade, look up one row with ORDER BY and LIMIT 1
  • Uses an index on (sym, ts) in PostgreSQL, so it scales well
  • Not supported in BigQuery

The best option in PostgreSQL.

SQL
-- QUALIFY version (WHERE TRUE is harmless and keeps BigQuery happy,
-- which has long asked for WHERE, GROUP BY or HAVING next to QUALIFY)
SELECT t.trade_id, q.price
FROM trades t
LEFT JOIN quotes q
  ON q.sym = t.sym AND q.ts <= t.ts
WHERE TRUE
QUALIFY ROW_NUMBER() OVER (PARTITION BY t.trade_id ORDER BY q.ts DESC) = 1;

-- LATERAL version (PostgreSQL)
SELECT t.trade_id, q.price
FROM trades t
LEFT JOIN LATERAL (
  SELECT price FROM quotes q
  WHERE q.sym = t.sym AND q.ts <= t.ts
  ORDER BY q.ts DESC
  LIMIT 1
) q ON TRUE;

Four mistakes to avoid

  1. 1

    Dropping rows by accident

    Check whether your engine's plain ASOF JOIN is inner. In DuckDB, ClickHouse and Spark, write LEFT if every left row must survive.

  2. 2

    Using the wrong direction

    ">=" means the latest value at or before; "<=" means the next value at or after. pandas calls these backward and forward; Polars calls them strategies.

  3. 3

    Matching very old values

    A quote from yesterday is still "the latest". Add a tolerance in pandas or Polars, or a filter such as t.ts - q.ts <= INTERVAL 1 HOUR in SQL.

  4. 4

    Forgetting the equal keys

    Without ON t.sym = q.sym, a trade in A can pick up a quote for B.

Point-in-time correct
Using only data that existed at the moment being analysed.
Backward match
The latest right row at or before the left row; the default in most tools.
Forward match
The first right row at or after the left row, for example the next delivery after an order.
Tolerance
The maximum gap allowed between the two times; older matches return NULL.

Check your understanding

1/3What does an ASOF JOIN return for each left row?

2/3In DuckDB, what happens to trades with no earlier quote if you write ASOF JOIN without LEFT?

3/3BigQuery has no ASOF JOIN. What is a working alternative?

More SQL: SQL joins explained, window functions explained and the SQL cheat sheet. On the tools: what's new in Polars 2.1 and DuckDB 2.0.

Sources: Snowflake documentation for ASOF JOIN; DuckDB, ClickHouse, pandas (merge_asof) and Polars (join_asof) documentation; Apache Spark 4.3.0 release notes (30 October 2026). Examples tested with DuckDB 2.0.0, PySpark 4.3.0, Polars 2.1.0 and pandas 3.1.0; Snowflake, ClickHouse, BigQuery and PostgreSQL syntax follows their documentation.

Alessandro Ecclesie AgazziWritten by Alessandro Ecclesie Agazzi, analytics engineer in London. Updated 9 November 2026. About me

Found this useful?

New guides and "what's new" explainers on data and AI land here regularly. Follow along on LinkedIn, or share this one with a colleague.