Un join normale abbina le righe quando le chiavi sono uguali. Molte domande reali chiedono altro: la riga con il timestamp più recente uguale o precedente a un altro timestamp. Il prezzo di un'azione al momento di un acquisto. Il cambio valido il giorno di una fattura. Il piano del cliente quando ha aperto un ticket di assistenza. È un ASOF JOIN ("as of", cioè "alla data di"). Snowflake, DuckDB, ClickHouse e Polars lo hanno, e Apache Spark lo ha aggiunto nella versione 4.3. Questa guida spiega l'idea una volta, poi mostra la sintassi in ogni strumento.
L'idea, con un esempio
Useremo sempre le stesse due piccole tabelle. Ogni transazione deve ricevere l'ultima quotazione dello stesso titolo uguale o precedente all'ora della transazione.
| trade_id | sym | ora transazione | Ultima quotazione uguale o precedente | prezzo |
|---|---|---|---|---|
| 1 | A | 10:00:05 | 10:00:00 | 10.0 |
| 2 | A | 10:00:20 | 10:00:10 | 10.5 |
| 3 | B | 10:00:01 | nessuna ancora (la prima quotazione di B è alle 10:00:15) | NULL |
| 4 | B | 10:00:30 | 10:00:15 | 20.0 |
Guarda la transazione 2: c'è anche una quotazione alle 10:00:25, ma è arrivata dopo la transazione, quindi non va usata. E la transazione 3 non ha nessuna quotazione precedente. Cosa le succede dipende dal tipo di join, che è la fonte di errori più comune.
La sintassi in ogni motore
Ogni esempio qui sotto restituisce i prezzi 10.0, 10.5, NULL e 20.0. Le versioni per DuckDB 2.0, Spark 4.3, Polars 2.1 e pandas 3.1 le abbiamo eseguite noi.
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 contiene l'unica disuguaglianza (>=, >, <= o <); ON contiene le chiavi uguali. Le righe senza corrispondenza tornano con NULL, come in un left join.
SELECT t.trade_id, t.sym, t.ts, q.price
FROM trades t
ASOF LEFT JOIN quotes q
ON t.sym = q.sym AND t.ts >= q.ts;DuckDB mette la disuguaglianza in ON. Il semplice ASOF JOIN è un inner join ed elimina la transazione 3; l'abbiamo verificato.
SET spark.sql.join.asofJoin.enabled = true;
SELECT t.trade_id, q.price
FROM trades AS t
LEFT ASOF JOIN quotes AS q
MATCH_CONDITION (t.ts >= q.ts)
ON t.sym = q.sym;È un'anteprima, disattivata di default. Senza LEFT, anche Spark elimina le righe senza corrispondenza.
import polars as pl
# DataFrame API: both frames sorted by the time column
result = trades.sort("ts").join_asof(
quotes.sort("ts"), on="ts", by="sym", strategy="backward"
)
# SQL, Snowflake style (Polars 2.1+)
pl.SQLContext(trades=trades, quotes=quotes).execute("""
SELECT trades.trade_id, price
FROM trades
ASOF JOIN quotes MATCH_CONDITION (trades.ts >= quotes.ts)
ON trades.sym = quotes.sym
""").collect()Nel nostro test entrambi hanno tenuto la transazione 3 con prezzo nullo.
import pandas as pd
result = pd.merge_asof(
trades.sort_values("ts"),
quotes.sort_values("ts"),
on="ts", by="sym",
direction="backward", # latest at or before
tolerance=pd.Timedelta("10s"), # optional: ignore stale quotes
)Entrambi gli input devono essere ordinati per la colonna on. Con la tolleranza di 10 secondi, la transazione 4 riceve NaN perché la sua quotazione ha 15 secondi.
SELECT t.trade_id, q.price
FROM trades AS t
ASOF LEFT JOIN quotes AS q
ON t.sym = q.sym AND t.ts >= q.ts;Stessa forma di DuckDB: una disuguaglianza in ON, e LEFT per tenere le righe senza corrispondenza.
Niente ASOF JOIN? Usa questa alternativa
BigQuery e PostgreSQL non hanno la parola chiave ASOF, ma due schemi danno la stessa risposta. Nei nostri test entrambi hanno restituito risultati identici alle versioni ASOF.
QUALIFY
BigQuery e quasi tutti- Idea
- unisci tutte le quotazioni precedenti, poi tieni l'ultima per transazione
- Breve e leggibile
- Costruisce prima tutte le corrispondenze precedenti, quindi può essere lento su tabelle grandi
Va bene da migliaia a milioni di righe.
LATERAL
Il migliore in PostgreSQL- Idea
- per ogni transazione, cerca una riga con ORDER BY e LIMIT 1
- In PostgreSQL usa un indice su (sym, ts), quindi scala bene
- Non supportato in BigQuery
La scelta migliore in PostgreSQL.
-- 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;Quattro errori da evitare
- 1
Perdere righe per sbaglio
Verifica se il semplice ASOF JOIN del tuo motore è inner. In DuckDB, ClickHouse e Spark scrivi LEFT se ogni riga di sinistra deve restare.
- 2
Sbagliare direzione
">=" significa l'ultimo valore uguale o precedente; "<=" il prossimo valore uguale o successivo. pandas li chiama backward e forward; Polars li chiama strategie.
- 3
Abbinare valori troppo vecchi
Una quotazione di ieri resta "l'ultima". Aggiungi una tolleranza in pandas o Polars, o un filtro come t.ts - q.ts <= INTERVAL 1 HOUR in SQL.
- 4
Dimenticare le chiavi uguali
Senza ON t.sym = q.sym, una transazione su A può prendere una quotazione di B.
- Corretto nel tempo
- Usare solo i dati che esistevano nel momento analizzato.
- Abbinamento all'indietro
- L'ultima riga di destra uguale o precedente alla riga di sinistra; il comportamento predefinito nella maggior parte degli strumenti.
- Abbinamento in avanti
- La prima riga di destra uguale o successiva alla riga di sinistra, per esempio la prossima consegna dopo un ordine.
- Tolleranza
- Lo scarto massimo ammesso tra i due tempi; le corrispondenze più vecchie restituiscono NULL.
Verifica cosa hai capito
1/3Cosa restituisce un ASOF JOIN per ogni riga di sinistra?
Sceglie una sola corrispondenza, la più vicina, per ogni riga di sinistra.
2/3In DuckDB, cosa succede alle transazioni senza quotazione precedente se scrivi ASOF JOIN senza LEFT?
In DuckDB il semplice ASOF JOIN è un inner join; usa ASOF LEFT JOIN per tenerle.
3/3BigQuery non ha ASOF JOIN. Qual è un'alternativa che funziona?
Lo schema con QUALIFY tiene l'ultima corrispondenza precedente per ogni riga.
Hai risposto correttamente a 0 domande su 3.
Altro SQL: i join SQL spiegati, le funzioni finestra spiegate e la guida rapida a SQL. Sugli strumenti: le novità di Polars 2.1 e DuckDB 2.0.
Fonti: documentazione di Snowflake su ASOF JOIN; documentazione di DuckDB, ClickHouse, pandas (merge_asof) e Polars (join_asof); note di rilascio di Apache Spark 4.3.0 (30 ottobre 2026). Esempi provati con DuckDB 2.0.0, PySpark 4.3.0, Polars 2.1.0 e pandas 3.1.0; la sintassi di Snowflake, ClickHouse, BigQuery e PostgreSQL segue la loro documentazione.
