lección 8
Leer un modelo ajeno sin multiplicar filas
Te dan siete tablas: averigua el grano, cruza sin duplicar y detecta el JOIN que infla el total.
⏱ 55 min
Es tu tercer lunes en la empresa. Te han dado acceso de lectura al almacén, ya conoces la diferencia entre hechos y dimensiones, y sabes que el esquema estrella organiza las tablas como un sol con sus planetas. Te piden un informe: el importe total de ventas del trimestre, desglosado por categoría de producto. Abres tu editor SQL, cruzas la tabla de ventas con la de productos, sumas, y entregas el número. Todo bien. Excepto que no lo está.
Al día siguiente tu responsable te llama: "El total que has dado es un 40% mayor que el que tiene Finanzas. ¿Puedes revisarlo?". Y aquí empieza la pesadilla del analista junior. El número estaba inflado porque la tabla de productos tenía varias filas por producto — una por cada proveedor que lo suministra — y tu JOIN multiplicó cada venta por el número de proveedores. Nadie te avisó. Ningún error saltó. La query corrió perfectamente y devolvió un número con buena pinta. Simplemente, era falso.
Este es el error número uno del primer mes de cualquier analista. No es falta de SQL: es falta de un hábito que nadie te enseña en los tutoriales. Y vamos a instalarlo hoy.
### El fan-out: cuando un JOIN fabrica filas que no existen
Piensa en una fiesta de cumpleaños donde cada invitado trae un regalo. Si montas una lista "invitado + regalo", tienes una fila por regalo. Ahora imagina que quieres cruzar esa lista con otra que dice "invitado + alérgeno alimentario" (porque vas a preparar la comida). Si Ana tiene dos regalos y tres alergias, el cruce genera 2 x 3 = 6 filas para Ana. Has fabricado combinaciones que no representan nada real: "regalo camiseta + alergia gluten" no es un concepto con significado. A eso se le llama fan-out, es decir, la multiplicación de filas que ocurre cuando un JOIN encuentra varias coincidencias en el lado opuesto.
En el almacén ocurre lo mismo. Tienes una tabla de ventas con 1.000 filas. Cruzas contra una tabla de descuentos donde un producto puede tener tres promociones activas. De repente tienes 3.000 filas en tu resultado. Si sumas el importe, lo estás contando tres veces. Y la base de datos no te va a decir nada: para ella, el JOIN es perfectamente válido.
### Por qué ocurre: la relación uno-a-muchos oculta
Cuando estudiaste JOINs, los ejemplos eran limpios: una tabla de pedidos con un cliente_id, y una tabla de clientes con un id único. Cada pedido cruzaba con exactamente un cliente. Eso es una relación muchos-a-uno (muchos pedidos, un cliente), y en ese caso el JOIN no multiplica nada. El resultado tiene las mismas filas que la tabla de la izquierda.
El fan-out aparece cuando la relación es muchos-a-muchos, o cuando crees que es muchos-a-uno pero en realidad es muchos-a-muchos. Cruzas ventas con productos pensando que cada producto_id aparece una vez en la tabla de productos — pero resulta que esa tabla tiene varias filas por producto (versiones, proveedores, traducciones, histórico de precios). Tu JOIN, sin saberlo, se ha convertido en un multiplicador.
Lo insidioso es que nada falla. La query se ejecuta. El resultado tiene columnas razonables. Los números parecen normales (un 40% de más no salta a la vista en un total de millones). Solo lo descubres si sabes que debes buscarlo.
El fan-out pertenece a la peor familia de errores de SQL: los que producen un resultado válido pero incorrecto. Un typo en un nombre de columna rompe la query y te enteras al momento. Un fan-out devuelve un número que parece razonable, y te enteras cuando Finanzas te dice que no le cuadra. A veces ni eso. Ya viste otro de la misma familia cuando promediaste porcentajes en vez de recalcularlos: la query corre, el número sale, y nadie avisa. La diferencia es que el fan-out no falsea un número, falsea todos los de la tabla a la vez.
### La regla de granularidad antes de cada JOIN
Aquí va el hábito que te va a salvar cien veces en tu carrera. Antes de escribir un JOIN, hazte esta pregunta: "¿la tabla a la que voy a cruzar, tiene UNA fila por cada valor de la clave de cruce?". Si la respuesta es sí, el JOIN es seguro. Si la respuesta es no (o "no lo sé"), tienes que investigar antes de cruzar.
Cuando viste la granularidad, aprendiste que cada tabla de hechos tiene un grano declarado: "una fila por línea de pedido", "una fila por visita". Pues bien, la regla de granularidad del JOIN es la misma idea aplicada a la tabla de destino: necesitas que tenga UNA fila por cada valor de la clave que vas a usar en el ON.
1-- ANTES de cruzar ventas con productos, comprueba:2SELECT3 COUNT(*) AS filas_totales,4 COUNT(DISTINCT producto_id) AS claves_unicas5FROM productos;67-- Si filas_totales = claves_unicas → una fila por producto → JOIN seguro8-- Si filas_totales > claves_unicas → hay duplicados → PELIGRO de fan-out
La comprobacion que deberias hacer antes de cada JOIN contra una tabla que no conoces.
Consejo de senior: convierte esto en un reflejo. Cada vez que vayas a escribir JOIN contra una tabla que no has consultado antes, antes ejecuta el COUNT(*) vs COUNT(DISTINCT clave). Son 5 segundos que te ahorran una mañana entera depurando por qué tu número no cuadra con el de otro departamento. En doce años no he visto a un analista senior que no haga esto.
### Detectar el fan-out: contar antes y después
La comprobación preventiva (COUNT vs COUNT DISTINCT) es el hábito. Pero a veces heredas una query de otro analista, o la has escrito tú hace tres meses y no recuerdas si la tabla destino era única. Entonces necesitas el diagnóstico: comparar el número de filas ANTES del JOIN con el número de filas DESPUÉS.
1-- Paso 1: cuantas filas tiene tu tabla base ANTES del JOIN2SELECT COUNT(*) AS filas_antes FROM ventas;3-- Resultado: 1.00045-- Paso 2: cuantas filas tiene el resultado DESPUES del JOIN6SELECT COUNT(*) AS filas_despues7FROM ventas v8JOIN productos p ON v.producto_id = p.producto_id;9-- Resultado: 1.3501011-- Si filas_despues > filas_antes → hay fan-out12-- En este caso: 350 filas extra fabricadas por el JOIN
Comparar filas antes y después del JOIN: si el número crece, el JOIN está multiplicando.
Este patrón de diagnóstico te dice DOS cosas: que hay un problema (filas_despues > filas_antes), y cuánto de grave es (la diferencia te dice cuántas filas extra se están fabricando). Un 2% de inflación puede ser tolerable dependiendo del contexto; un 40% es una catástrofe silenciosa.
### Cómo arreglarlo: tres estrategias
Has detectado el fan-out. Ahora necesitas un resultado correcto. Hay tres formas de resolverlo, y cada una tiene su momento:
### Estrategia 1: agregar primero, cruzar después
Es la solución más limpia y la que usarás el 80% de las veces. La idea: si la tabla problemática tiene varias filas por clave, la conviertes en una tabla con UNA fila por clave usando un GROUP BY dentro de una CTE o subquery, y cruzas contra esa versión agregada.
1-- PROBLEMA: descuentos tiene varias promos por producto2-- Si cruzamos directamente, cada venta se multiplica34-- SOLUCION: agregar descuentos ANTES de cruzar5WITH descuentos_por_producto AS (6 SELECT7 producto_id,8 COUNT(*) AS num_promos,9 MAX(descuento) AS mejor_descuento10 FROM descuentos11 GROUP BY producto_id12 -- Ahora esta CTE tiene UNA fila por producto_id13)14SELECT15 v.venta_id,16 v.importe,17 d.num_promos,18 d.mejor_descuento19FROM ventas v20LEFT JOIN descuentos_por_producto d21 ON v.producto_id = d.producto_id;22-- Resultado: mismas filas que ventas (sin fan-out)
El GROUP BY dentro de la CTE garantiza una fila por producto_id antes del cruce.
### Estrategia 2: ROW_NUMBER para quedarte con una fila
A veces no quieres una métrica agregada: quieres UNA fila concreta de las varias que existen. Por ejemplo, si un producto tiene múltiples precios históricos, quieres el precio vigente (el más reciente). En ese caso, numeras las filas con ROW_NUMBER, filtras por la primera, y el resultado tiene una sola fila por clave.
1-- PROBLEMA: precios_historico tiene varias filas por producto2-- Queremos solo el precio vigente (fecha mas reciente)34WITH precio_vigente AS (5 SELECT6 producto_id,7 precio,8 fecha_desde,9 ROW_NUMBER() OVER (10 PARTITION BY producto_id11 ORDER BY fecha_desde DESC12 ) AS rn13 FROM precios_historico14)15SELECT16 v.venta_id,17 v.importe,18 pv.precio AS precio_vigente19FROM ventas v20LEFT JOIN precio_vigente pv21 ON v.producto_id = pv.producto_id22 AND pv.rn = 1; -- solo la fila mas reciente
ROW_NUMBER numera las filas dentro de cada producto. Filtrar por rn = 1 deja una sola fila por clave.
Consejo de senior: ROW_NUMBER es tu bisturí cuando necesitas "la fila ganadora" entre varias candidatas. Te va a servir para mil cosas: el último login de cada usuario, la dirección principal de cada cliente, el primer pedido de cada mes. Siempre el mismo patrón: PARTITION BY la clave + ORDER BY el criterio de selección + filtrar por rn = 1.
### Un caso real: el informe que costó una decisión equivocada
Esto pasó de verdad en una empresa de comercio electrónico (los nombres están cambiados, el error no). El equipo de marketing pidió el revenue por canal de adquisición para decidir dónde invertir el presupuesto del siguiente trimestre. Un analista junior cruzó la tabla de pedidos con la tabla de atribución de canales. El problema: la tabla de atribución tenía múltiples filas por pedido (un pedido puede atribuirse a varios canales con un modelo multi-touch). El resultado: el revenue total "por canal" sumaba un 73% más que el revenue real de la empresa.
Nadie lo vio porque el informe mostraba el desglose por canal, no el total. Cada canal por separado parecía razonable. Se tomó la decisión de triplicar la inversión en el canal que "más revenue generaba". Tres meses después, cuando los resultados no llegaron, un analista senior revisó la query original y encontró el fan-out. El presupuesto ya estaba gastado.
La lección: el fan-out no solo infla el total, distorsiona las proporciones. Un canal que atribuía 2 veces por pedido parecía generar el doble que uno que solo atribuía una vez. La decisión no solo se basó en un número inflado: se basó en una comparación falsificada.
### El protocolo completo: de tabla desconocida a JOIN seguro
Vamos a juntar todo en un protocolo que puedas seguir cada vez que te enfrentas a un modelo de datos nuevo. Porque el fan-out no es un bug: es la consecuencia natural de cruzar tablas sin haberlas investigado primero. El protocolo convierte la incertidumbre en certeza antes de que el número llegue a ningún informe.
- 01.Identifica la tabla base (la que tiene las filas que quieres contar o sumar).
- 02.Cuenta sus filas: SELECT COUNT(*) FROM tabla_base. Este es tu número de referencia.
- 03.Por cada tabla que vayas a cruzar, ejecuta: SELECT COUNT(*), COUNT(DISTINCT clave_de_cruce) FROM tabla_destino.
- 04.Si ambos números coinciden: JOIN seguro. Adelante.
- 05.Si COUNT(*) > COUNT(DISTINCT clave): hay duplicados. Decide la estrategia (agregar, ROW_NUMBER o EXISTS) ANTES de escribir el JOIN final.
- 06.Después del JOIN, verifica: SELECT COUNT(*) del resultado. Debe ser igual o menor que tu tabla base (menor si es INNER JOIN y hay filas sin coincidencia; igual si es LEFT JOIN sin fan-out).
Esto te lo van a preguntar en la entrevista. Una pregunta clásica de prueba técnica de analista es: "te doy estas tres tablas, escribe la query que calcula X". Si cruzas sin comprobar la granularidad y el resultado está inflado, estás fuera. Los entrevistadores lo ponen a propósito porque es exactamente lo que pasa el primer día en el trabajo real.
### Resumen
- El fan-out es la multiplicación de filas que ocurre cuando un JOIN encuentra varias coincidencias en la tabla destino. Infla sumas y distorsiona proporciones sin que ningún error avise.
- La regla: antes de cada JOIN, comprueba que la tabla destino tiene UNA fila por cada valor de la clave de cruce (COUNT(*) = COUNT(DISTINCT clave)).
- Si hay duplicados, no cruces directamente. Tres estrategias: agregar primero (GROUP BY en CTE), elegir una fila (ROW_NUMBER + filtro rn=1), o comprobar existencia (EXISTS/DISTINCT).
- Después del JOIN, verifica que el número de filas del resultado no supera al de tu tabla base.
- El fan-out no solo infla totales: distorsiona comparaciones entre categorías y lleva a decisiones equivocadas.
Regístrate para guardar tu progreso.
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...