Une jointure normale associe des lignes quand les clés sont égales. Beaucoup de vraies questions demandent autre chose : la ligne avec l'horodatage le plus récent égal ou antérieur à un autre horodatage. Le prix d'une action au moment d'un achat. Le taux de change en vigueur le jour d'une facture. L'offre du client quand il a ouvert un ticket. C'est un ASOF JOIN (« as of », c'est-à-dire « à la date de »). Snowflake, DuckDB, ClickHouse et Polars le proposent, et Apache Spark l'a ajouté dans la version 4.3. Ce guide explique l'idée une fois, puis montre la syntaxe dans chaque outil.
L'idée, avec un exemple
Nous utiliserons partout les deux mêmes petites tables. Chaque transaction doit recevoir la dernière cotation du même titre égale ou antérieure à l'heure de la transaction.
| trade_id | sym | heure de transaction | Dernière cotation égale ou antérieure | prix |
|---|---|---|---|---|
| 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 | aucune encore (la première cotation de B est à 10:00:15) | NULL |
| 4 | B | 10:00:30 | 10:00:15 | 20.0 |
Regardez la transaction 2 : il existe aussi une cotation à 10:00:25, mais elle est arrivée après la transaction, donc elle ne doit pas servir. Et la transaction 3 n'a aucune cotation antérieure. Ce qui lui arrive dépend du type de jointure, la source d'erreurs la plus fréquente.
La syntaxe dans chaque moteur
Chaque exemple ci-dessous renvoie les prix 10.0, 10.5, NULL et 20.0. Nous avons exécuté nous-mêmes les versions DuckDB 2.0, Spark 4.3, Polars 2.1 et pandas 3.1.
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 contient l'unique inégalité (>=, >, <= ou <) ; ON contient les clés égales. Les lignes sans correspondance reviennent avec des NULL, comme dans une jointure gauche.
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 place l'inégalité dans ON. Un simple ASOF JOIN est une jointure interne et supprime la transaction 3 ; nous l'avons vérifié.
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 aperçu, désactivé par défaut. Sans LEFT, Spark supprime lui aussi les lignes sans correspondance.
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()Dans notre test, les deux ont gardé la transaction 3 avec un prix nul.
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
)Les deux entrées doivent être triées sur la colonne on. Avec la tolérance de 10 secondes, la transaction 4 obtient NaN car sa cotation date de 15 secondes.
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;Même forme que DuckDB : une inégalité dans ON, et LEFT pour garder les lignes sans correspondance.
Pas d'ASOF JOIN ? Utilisez cette solution de repli
BigQuery et PostgreSQL n'ont pas de mot-clé ASOF, mais deux schémas donnent la même réponse. Dans nos tests, les deux ont renvoyé des résultats identiques aux versions ASOF.
QUALIFY
BigQuery et la plupart- Idée
- joindre toutes les cotations antérieures, puis garder la dernière par transaction
- Court et lisible
- Construit d'abord toutes les correspondances antérieures, donc peut être lent sur de grosses tables
Convient de quelques milliers à quelques millions de lignes.
LATERAL
Le meilleur dans PostgreSQL- Idée
- pour chaque transaction, chercher une ligne avec ORDER BY et LIMIT 1
- Utilise un index sur (sym, ts) dans PostgreSQL, donc passe bien à l'échelle
- Non pris en charge dans BigQuery
La meilleure option dans 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;Quatre erreurs à éviter
- 1
Perdre des lignes par accident
Vérifiez si le simple ASOF JOIN de votre moteur est interne. Dans DuckDB, ClickHouse et Spark, écrivez LEFT si chaque ligne de gauche doit rester.
- 2
Se tromper de sens
">=" signifie la dernière valeur égale ou antérieure ; "<=" la prochaine valeur égale ou postérieure. pandas les appelle backward et forward ; Polars parle de stratégies.
- 3
Associer des valeurs trop anciennes
Une cotation d'hier reste « la dernière ». Ajoutez une tolérance dans pandas ou Polars, ou un filtre comme t.ts - q.ts <= INTERVAL 1 HOUR en SQL.
- 4
Oublier les clés égales
Sans ON t.sym = q.sym, une transaction sur A peut récupérer une cotation de B.
- Correct dans le temps
- N'utiliser que les données qui existaient au moment analysé.
- Correspondance en arrière
- La dernière ligne de droite égale ou antérieure à la ligne de gauche ; le comportement par défaut de la plupart des outils.
- Correspondance en avant
- La première ligne de droite égale ou postérieure à la ligne de gauche, par exemple la prochaine livraison après une commande.
- Tolérance
- L'écart maximal autorisé entre les deux temps ; les correspondances plus anciennes renvoient NULL.
Testez vos connaissances
1/3Que renvoie un ASOF JOIN pour chaque ligne de gauche ?
Il choisit une seule correspondance, la plus proche, par ligne de gauche.
2/3Dans DuckDB, que deviennent les transactions sans cotation antérieure si vous écrivez ASOF JOIN sans LEFT ?
Dans DuckDB, le simple ASOF JOIN est interne ; utilisez ASOF LEFT JOIN pour les garder.
3/3BigQuery n'a pas d'ASOF JOIN. Quelle alternative fonctionne ?
Le schéma QUALIFY garde la dernière correspondance antérieure pour chaque ligne.
Vous avez 0 bonne(s) réponse(s) sur 3.
Plus de SQL : les jointures SQL expliquées, les fonctions de fenêtre expliquées et l'aide-mémoire SQL. Sur les outils : les nouveautés de Polars 2.1 et DuckDB 2.0.
Sources : documentation de Snowflake sur ASOF JOIN ; documentation de DuckDB, ClickHouse, pandas (merge_asof) et Polars (join_asof) ; notes de version d'Apache Spark 4.3.0 (30 octobre 2026). Exemples testés avec DuckDB 2.0.0, PySpark 4.3.0, Polars 2.1.0 et pandas 3.1.0 ; la syntaxe de Snowflake, ClickHouse, BigQuery et PostgreSQL suit leur documentation.
