Introducción — El bug que casi nadie ve en SQL
Hace años, en una entrevista técnica, me apareció un problema muy parecido al que vamos a resolver aquí. Era uno de tres ejercicios de SQL, todos bastante manejables, pero este en particular tenía una trampa silenciosa que casi nadie había detectado. Giraba alrededor de algo tan común como obtener la primera transacción del día en SQL con timestamp.
A partir de esa experiencia, quiero mostrar un error muy común cuando trabajamos con fechas y horas en SQL: creer que estamos trabajando “por día”, aunque en realidad tratamos con un timestamp completo. Ese pequeño detalle puede convertir una consulta aparentemente correcta en un desastre silencioso.
La tabla DAILY_TRANSACTIONS — Datos mínimos para reproducir el caso
Ilustraré el problema de la primera transacción del día en SQL con timestamp usando un CTE llamado DAILY_TRANSACTIONS, que nos permitirá simular la tabla física. Estas son las columnas que usaremos:
- merchant_id
- transaction_ts (un TIMESTAMP con fecha y hora)
- transaction_id
WITH daily_transactions AS (
SELECT * FROM (
VALUES
(12, TIMESTAMP '2024-01-01 00:45:00', 'A1'),
(12, TIMESTAMP '2024-01-01 07:12:00', 'A2'),
(12, TIMESTAMP '2024-01-01 14:37:00', 'A3'),
(12, TIMESTAMP '2024-01-02 10:35:00', 'B1'),
(34, TIMESTAMP '2024-01-01 09:10:00', 'C1'),
(34, TIMESTAMP '2024-01-01 11:55:00', 'C2')
) AS t(merchant_id, transaction_ts, transaction_id)
)
El enunciado del problema — ¿Qué queremos responder?
Esto es lo que resolveremos con SQL:
Para cada merchant, devuelve la primera transacción del día (es decir, la que tiene la hora más temprana de cada día).
Dicho de otra forma:
- La unidad de análisis es:
merchant_id+ día. - Dentro de cada día, buscaremos la transacción con la hora más temprana.
- El campo
transaction_tsincluye fecha y hora, pero el análisis es por día.
Ese desajuste entre “lo que queremos” (día) y “lo que tenemos” (timestamp completo) es donde nace el error.
La solución incorrecta a la primera transacción del día en SQL con timestamp — Agrupar por TIMESTAMP completo
Esta es la consulta que casi todos escriben:
ROW_NUMBER() OVER (
PARTITION BY merchant_id, transaction_ts
ORDER BY transaction_ts
)
¿Qué problema tiene?
- Cada
transaction_tses único. - Cada fila queda sola en su partición.
ROW_NUMBER()siempre devuelve 1.
El resultado puede parecer correcto, pero no responde la pregunta. En vez de obtener “la primera transacción del día”, obtenemos todas las transacciones, cada una aislada en su propio grupo.
Es un error silencioso: no lanza excepción, no rompe la consulta, pero distorsiona el análisis.
La solución correcta a la primera transacción del día en SQL con timestamp — Convertir TIMESTAMP a DATE antes de particionar
La clave es que la granularidad del análisis y la de la partición deben coincidir. Si el análisis es por día, la partición también debe ser por día.
WITH daily_transactions AS (
SELECT * FROM (
VALUES
(12, TIMESTAMP '2024-01-01 00:45:00', 'A1'),
(12, TIMESTAMP '2024-01-01 07:12:00', 'A2'),
(12, TIMESTAMP '2024-01-01 14:37:00', 'A3'),
(12, TIMESTAMP '2024-01-02 10:35:00', 'B1'),
(34, TIMESTAMP '2024-01-01 09:10:00', 'C1'),
(34, TIMESTAMP '2024-01-01 11:55:00', 'C2')
) AS t(merchant_id, transaction_ts, transaction_id)
),
correct_solution AS (
SELECT
merchant_id,
CAST(transaction_ts AS DATE) AS transaction_date,
transaction_id,
ROW_NUMBER() OVER (
PARTITION BY merchant_id, CAST(transaction_ts AS DATE)
ORDER BY transaction_ts
) AS rn
FROM daily_transactions
)
SELECT
merchant_id,
transaction_date,
transaction_id
FROM correct_solution
WHERE rn = 1
ORDER BY merchant_id, transaction_date;
Ahora sí estamos respondiendo la pregunta original:
Traer la primera transacción del día que hizo cada merchant.
Lección clave — La granularidad temporal define la consulta
Este es el mensaje central:
- Si el análisis es por día, no puede particionarse por timestamp.
- Si es por mes, es un error agrupar por fecha exacta.
- Finalmente, si el análisis es por hora, no puede agruparse por minuto.
La granularidad correcta no es un detalle cosmético: es la diferencia entre una métrica confiable y una engañosa.
Bonus — Otros errores silenciosos
Este tipo de trampas no se limita al tiempo. También aparecen cuando:
- se usa un nombre como si fuera una llave única
- se asume que un campo identifica a una persona cuando no es así
- se hace join por columnas ambiguas
- se mezclan granularidades sin declararlo
En otra entrevista, donde la relación entre dos tablas era el nombre, comenté que si dos personas se llamaban igual, el join propuesto iba a ser un desastre. El entrevistador se rió y me dijo: “Asume que no se llaman igual”.
Ese tipo de respuestas es más común de lo que parece. Muchos entrevistadores introducen deliberadamente estas pequeñas “trampas” para medir qué tan detallista es uno y si realmente piensa en la integridad de los datos más allá del caso feliz.
Detalles que parecen triviales —como asumir unicidad donde no existe— son precisamente los que separan una consulta robusta de una que se rompe con facilidad.
Conclusión — Pensar antes de escribir SQL
Siempre vale la pena detenerse y preguntar:
- ¿Qué pregunta de negocio debo responder?
- ¿Cuál es el grano correcto: día, hora, mes, transacción?
- ¿Estoy particionando y agrupando en esa misma granularidad?
- ¿Las columnas que uso para el join identifican a la entidad?
SQL no es solo sintaxis: es una forma de pensar. Cuando la pregunta y la granularidad están claras, el código deja de ser un obstáculo y se convierte en una herramienta natural.
Puede descargar la solución aquí.
Foto de kaboompics.com – pexels.com


