Un join normal une filas cuando las claves son iguales. Muchas preguntas reales necesitan otra cosa: la fila con la marca de tiempo más reciente igual o anterior a otra. El precio de una acción en el momento de una compra. El tipo de cambio vigente el día de una factura. El plan del cliente cuando abrió una incidencia. Eso es un ASOF JOIN ("as of", es decir, "a fecha de"). Snowflake, DuckDB, ClickHouse y Polars lo tienen, y Apache Spark lo añadió en la versión 4.3. Esta guía explica la idea una vez y luego muestra la sintaxis en cada herramienta.
La idea, con un ejemplo
Usaremos siempre las mismas dos tablas pequeñas. Cada operación debe recibir la última cotización del mismo valor igual o anterior a la hora de la operación.
| trade_id | sym | hora de la operación | Última cotización igual o anterior | precio |
|---|---|---|---|---|
| 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 | ninguna aún (la primera cotización de B es a las 10:00:15) | NULL |
| 4 | B | 10:00:30 | 10:00:15 | 20.0 |
Fíjate en la operación 2: también hay una cotización a las 10:00:25, pero llegó después de la operación, así que no debe usarse. Y la operación 3 no tiene ninguna cotización anterior. Qué le pasa depende del tipo de join, la fuente de errores más habitual.
La sintaxis en cada motor
Todos los ejemplos de abajo devuelven los precios 10.0, 10.5, NULL y 20.0. Las versiones de DuckDB 2.0, Spark 4.3, Polars 2.1 y pandas 3.1 las ejecutamos nosotros.
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 la única desigualdad (>=, >, <= o <); ON contiene las claves iguales. Las filas sin coincidencia vuelven con NULL, como en 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 pone la desigualdad en ON. Un ASOF JOIN simple es un inner join y elimina la operación 3; lo comprobamos.
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;Es una vista previa, desactivada por defecto. Sin LEFT, Spark también elimina las filas sin coincidencia.
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()En nuestra prueba, los dos mantuvieron la operación 3 con precio nulo.
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
)Las dos entradas deben estar ordenadas por la columna on. Con la tolerancia de 10 segundos, la operación 4 recibe NaN porque su cotización tiene 15 segundos.
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;La misma forma que DuckDB: una desigualdad en ON, y LEFT para mantener las filas sin coincidencia.
¿Sin ASOF JOIN? Usa esta alternativa
BigQuery y PostgreSQL no tienen la palabra clave ASOF, pero dos patrones dan la misma respuesta. En nuestras pruebas, los dos devolvieron resultados idénticos a las versiones ASOF.
QUALIFY
BigQuery y la mayoría- Idea
- unir todas las cotizaciones anteriores y quedarse con la última por operación
- Corto y legible
- Construye antes todas las coincidencias anteriores, así que puede ser lento con tablas grandes
Válido para miles o millones de filas.
LATERAL
El mejor en PostgreSQL- Idea
- para cada operación, buscar una fila con ORDER BY y LIMIT 1
- En PostgreSQL usa un índice sobre (sym, ts), así que escala bien
- No disponible en BigQuery
La mejor opción en 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;Cuatro errores que evitar
- 1
Perder filas sin querer
Comprueba si el ASOF JOIN simple de tu motor es inner. En DuckDB, ClickHouse y Spark escribe LEFT si todas las filas de la izquierda deben quedarse.
- 2
Equivocarse de dirección
">=" significa el último valor igual o anterior; "<=" el siguiente valor igual o posterior. pandas los llama backward y forward; Polars habla de estrategias.
- 3
Asociar valores demasiado viejos
Una cotización de ayer sigue siendo "la última". Añade una tolerancia en pandas o Polars, o un filtro como t.ts - q.ts <= INTERVAL 1 HOUR en SQL.
- 4
Olvidar las claves iguales
Sin ON t.sym = q.sym, una operación en A puede recoger una cotización de B.
- Correcto en el tiempo
- Usar solo los datos que existían en el momento analizado.
- Coincidencia hacia atrás
- La última fila de la derecha igual o anterior a la fila de la izquierda; lo predeterminado en la mayoría de herramientas.
- Coincidencia hacia delante
- La primera fila de la derecha igual o posterior a la fila de la izquierda, por ejemplo la siguiente entrega tras un pedido.
- Tolerancia
- La distancia máxima permitida entre los dos tiempos; las coincidencias más antiguas devuelven NULL.
Pon a prueba lo que has aprendido
1/3¿Qué devuelve un ASOF JOIN para cada fila de la izquierda?
Elige una sola coincidencia, la más cercana, por cada fila de la izquierda.
2/3En DuckDB, ¿qué pasa con las operaciones sin cotización anterior si escribes ASOF JOIN sin LEFT?
En DuckDB, el ASOF JOIN simple es un inner join; usa ASOF LEFT JOIN para conservarlas.
3/3BigQuery no tiene ASOF JOIN. ¿Qué alternativa funciona?
El patrón con QUALIFY conserva la última coincidencia anterior de cada fila.
Has acertado 0 de 3.
Más SQL: los joins de SQL explicados, las funciones de ventana explicadas y la chuleta de SQL. Sobre las herramientas: novedades de Polars 2.1 y DuckDB 2.0.
Fuentes: documentación de Snowflake sobre ASOF JOIN; documentación de DuckDB, ClickHouse, pandas (merge_asof) y Polars (join_asof); notas de versión de Apache Spark 4.3.0 (30 de octubre de 2026). Ejemplos probados con DuckDB 2.0.0, PySpark 4.3.0, Polars 2.1.0 y pandas 3.1.0; la sintaxis de Snowflake, ClickHouse, BigQuery y PostgreSQL sigue su documentación.
