Saltar al contenido

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.

El fan-out en acción: 3 ventas se convierten en 5 filas porque P01 tiene dos promociones. La suma pasa de 160 a 240 sin que ningún error avise.

### 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.

Dos números: filas totales y claves únicas. Si coinciden, la tabla tiene una fila por clave y el JOIN no multiplicará nada.
1-- ANTES de cruzar ventas con productos, comprueba:
2SELECT
3 COUNT(*) AS filas_totales,
4 COUNT(DISTINCT producto_id) AS claves_unicas
5FROM productos;
6
7-- Si filas_totales = claves_unicas → una fila por producto → JOIN seguro
8-- 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 JOIN
2SELECT COUNT(*) AS filas_antes FROM ventas;
3-- Resultado: 1.000
4
5-- Paso 2: cuantas filas tiene el resultado DESPUES del JOIN
6SELECT COUNT(*) AS filas_despues
7FROM ventas v
8JOIN productos p ON v.producto_id = p.producto_id;
9-- Resultado: 1.350
10
11-- Si filas_despues > filas_antes → hay fan-out
12-- 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:

Cada estrategia resuelve un caso distinto. La primera es la más común y la más segura como punto de partida.

### 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 producto
2-- Si cruzamos directamente, cada venta se multiplica
3
4-- SOLUCION: agregar descuentos ANTES de cruzar
5WITH descuentos_por_producto AS (
6 SELECT
7 producto_id,
8 COUNT(*) AS num_promos,
9 MAX(descuento) AS mejor_descuento
10 FROM descuentos
11 GROUP BY producto_id
12 -- Ahora esta CTE tiene UNA fila por producto_id
13)
14SELECT
15 v.venta_id,
16 v.importe,
17 d.num_promos,
18 d.mejor_descuento
19FROM ventas v
20LEFT JOIN descuentos_por_producto d
21 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 producto
2-- Queremos solo el precio vigente (fecha mas reciente)
3
4WITH precio_vigente AS (
5 SELECT
6 producto_id,
7 precio,
8 fecha_desde,
9 ROW_NUMBER() OVER (
10 PARTITION BY producto_id
11 ORDER BY fecha_desde DESC
12 ) AS rn
13 FROM precios_historico
14)
15SELECT
16 v.venta_id,
17 v.importe,
18 pv.precio AS precio_vigente
19FROM ventas v
20LEFT JOIN precio_vigente pv
21 ON v.producto_id = pv.producto_id
22 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 fan-out no solo infla el total: distorsiona las proporciones entre categorías. La decisión de inversión se basó en proporciones falsas.

### 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.

  1. 01.Identifica la tabla base (la que tiene las filas que quieres contar o sumar).
  2. 02.Cuenta sus filas: SELECT COUNT(*) FROM tabla_base. Este es tu número de referencia.
  3. 03.Por cada tabla que vayas a cruzar, ejecuta: SELECT COUNT(*), COUNT(DISTINCT clave_de_cruce) FROM tabla_destino.
  4. 04.Si ambos números coinciden: JOIN seguro. Adelante.
  5. 05.Si COUNT(*) > COUNT(DISTINCT clave): hay duplicados. Decide la estrategia (agregar, ROW_NUMBER o EXISTS) ANTES de escribir el JOIN final.
  6. 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...