Python & Data Science

Errores comunes en los JOIN de SQL que duplican silenciosamente tus filas

El problema: Tu conteo se duplicó (y no sabes por qué)

¿Alguna vez has escrito una consulta SQL que parecía estar bien, solo para mirar los resultados y darte cuenta de que algo no cuadra?

Esperabas 1,000 filas —tal vez una por cliente— pero la base de datos te entregó 2,000. O 5,000. O 10,000.

Esta es una de las frustraciones más comunes en el análisis de datos. No es un error de sintaxis. La base de datos no arroja ninguna advertencia. Simplemente multiplica tus filas en silencio, convirtiendo un informe sencillo en un caos. Si tu consulta calcula los ingresos totales y tus filas se han duplicado, tus ingresos serán 2x más altos que la realidad. Esa es la diferencia entre un buen trimestre y un desastre en los reportes.

Ejecutemos una consulta sencilla sobre algunos datos de retail y veamos qué sucede.

import pandas as pd
import sqlite3

# Let's set up a quick in-memory database
conn = sqlite3.connect(':memory:')

# Create an orders table
orders = pd.DataFrame({
    'order_id': [101, 102],
    'customer_name': ['Alice', 'Bob'],
    'total_amount': [50.00, 75.00]
})

# Create an order_items table (Alice bought 2 things, Bob bought 2 things)
items = pd.DataFrame({
    'item_id': [1, 2, 3, 4],
    'order_id': [101, 101, 102, 102],
    'product': ['Apple', 'Banana', 'Cherry', 'Date']
})

orders.to_sql('orders', conn, index=False)
items.to_sql('items', conn, index=False)

# The naive query: We just want to see orders with their items
query = """
SELECT 
    o.order_id, 
    o.total_amount,
    i.product
FROM orders o
JOIN items i ON o.order_id = i.order_id
"""

result = pd.read_sql(query, conn)
print(f"Original Orders Count: {len(orders)}")
print(f"Resulting Rows: {len(result)}")
print(result)

¿Qué sucedió? Empezamos con 2 pedidos pero terminamos con 4 filas. Si intentáramos usar SUM(total_amount) en este resultado, obtendríamos $250 en lugar de los $125 reales. La unión no solo combinó los datos. Los multiplicó.

Cómo funcionan realmente los JOIN: Productos cartesianos bajo el capó

Para solucionar esto, tenemos que cambiar nuestro modelo mental. A menudo pensamos en un JOIN como una forma de “buscar” información, como un VLOOKUP en Excel. Pero en SQL, un join es fundamentalmente una operación de multiplicación.

Cuando unes la Tabla A con la Tabla B, SQL examina cada fila de la Tabla A. Por cada fila, encuentra todas las filas coincidentes en la Tabla B. Si una fila de la Tabla A coincide con tres filas de la Tabla B, SQL crea tres filas en tu salida.

Este es un Producto Cartesiano de los subconjuntos coincidentes.

Lo que esto significa en realidad: Los joins no filtran tus datos a menos que nada coincida. Su comportamiento predeterminado es expandir. Una relación de uno a varios multiplicará tu recuento de filas, y el tipo de join — INNER o LEFT — no cambia eso.

Error #1: Hacer JOIN de uno a muchos sin darte cuenta

Esta es la causa más frecuente. Unes Customers con Orders y piensas: “Solo quiero el email del cliente junto a su pedido”.

Sin embargo, un cliente puede tener muchos pedidos. Al unir la tabla Customer con la tabla Order, obtienes una fila por cada pedido que ese cliente haya realizado. Si unes ese resultado con Order_Items, obtienes una fila por cada artículo en cada pedido.

# Let's add a third layer: order_items to categories
categories = pd.DataFrame({
    'product': ['Apple', 'Apple'], # Oops! Duplicate category entry
    'category': ['Fruit', 'Snack']
})
categories.to_sql('categories', conn, index=False)

multi_join_query = """
SELECT o.order_id, i.product, c.category
FROM orders o
JOIN items i ON o.order_id = i.order_id
JOIN categories c ON i.product = c.product
"""

explosion = pd.read_sql(multi_join_query, conn)
print(f"Row count after joining categories: {len(explosion)}")

La cantidad de filas sigue creciendo en cada paso. Por eso GROUP BY o DISTINCT terminan usándose como botones de pánico, pero solo ocultan una incomprensión más profunda de la relación entre los datos.

Error #2: Hacer JOIN a una tabla con duplicados que no conocías

A veces la lógica está bien, pero los datos están sucios. Digamos que tu tabla Product_Catalog debería tener una fila por ID de producto, pero un error de ETL insertó “Product 501” dos veces.

Únelo a tu tabla Sales y cada venta del producto 501 se duplica.

Cómo detectarlo: Revisa tu tabla de referencia antes de unir:

SELECT join_key, COUNT(*)
FROM reference_table
GROUP BY join_key
HAVING COUNT(*) > 1;

Si esto devuelve algo, tu unión duplicará filas.

Error #3: Hacer JOIN con claves no únicas (el asesino silencioso)

Esta es la parte más difícil de la depuración causal en SQL. Haces un join en last_name, pensando que es lo suficientemente único para tu pequeño conjunto de prueba. Luego lo ejecutas sobre la base de datos completa, y “Smith” coincide con 5,000 personas.

Incluso las columnas que parecen IDs pueden no ser únicas. Hacer un join en zip_code o store_id puede parecer seguro, hasta que te das cuenta de que esos IDs se comparten entre regiones o períodos de tiempo.

Error #4: Múltiples JOIN que crean una multiplicación acumulada

La multiplicación se acumula. Si el primer join duplica tus filas (2x) y el segundo triplica cada una de ellas (3x), estás en 6x tus datos iniciales.

En una consulta compleja con 5 o 6 joins, una sola relación de uno a muchos al principio de la cadena puede convertir 100 filas en 100,000 al final. Ahora tus datos son incorrectos y tu consulta es lenta.

Detectando el error: Cómo saber si tu JOIN está duplicando filas

Antes de confiar en tus resultados, ejecuta este diagnóstico de 30 segundos. Compara el recuento de tu clave primaria con el recuento de claves primarias distintas.

diagnostic_query = """
SELECT 
    COUNT(*) as total_rows,
    COUNT(DISTINCT order_id) as unique_orders
FROM (
    SELECT o.order_id
    FROM orders o
    JOIN items i ON o.order_id = i.order_id
)
"""
check = pd.read_sql(diagnostic_query, conn)
print(check)

Si total_rows es 4 y unique_orders es 2, tienes una duplicación de 2x.

Solución #1: Usar DISTINCT para eliminar duplicados (la solución rápida)

SELECT DISTINCT es el último recurso. Le dice a SQL: después de todas las uniones y multiplicaciones, revisa las filas finales y colapsa las que sean idénticas.

Funciona, pero es costoso. La base de datos tiene que ordenar todo el conjunto de resultados para encontrar los duplicados. Úsalo solo cuando realmente tengas filas idénticas que no puedas evitar mediante una mejor lógica.

Solución #2: Usar GROUP BY para agregar antes del JOIN

Esta es la solución estándar para la mayoría de las duplicaciones. Si necesitas que los datos de una tabla “varios” (como Order_Items) vivan en una tabla “uno” (como Orders), agrégalos primero.

Haz un join con un resumen de los elementos, no con las filas sin procesar.

fix_query = """
SELECT 
    o.order_id, 
    o.total_amount,
    item_summary.item_count
FROM orders o
JOIN (
    SELECT order_id, COUNT(*) as item_count 
    FROM items 
    GROUP BY order_id
) item_summary ON o.order_id = item_summary.order_id
"""
print(pd.read_sql(fix_query, conn))

Ahora tenemos 2 filas para 2 órdenes. El recuento es correcto porque convertimos el “varios” en un “uno” antes del join.

Solución #3: Usar subconsultas o CTE para controlar el orden del JOIN

Las expresiones de tabla comunes (CTEs) mantienen tu lógica legible. Defines tus tablas “limpias” al inicio de la consulta, por lo que no tienes que lidiar con duplicados dentro de un bloque de código de 50 líneas.

cte_query = """
WITH clean_items AS (
    SELECT order_id, COUNT(*) as total_items
    FROM items
    GROUP BY order_id
)
SELECT o.order_id, o.total_amount, ci.total_items
FROM orders o
JOIN clean_items ci ON o.order_id = ci.order_id
"""
print(pd.read_sql(cte_query, conn))

Solución #4: Usar funciones de ventana para asignar rangos y filtrar filas

¿Qué sucede si quieres el artículo más reciente de cada pedido, sin usar agregaciones?

ROW_NUMBER() asigna un rango a las filas dentro de un grupo para que puedas conservar solo la primera.

window_query = """
WITH ranked_items AS (
    SELECT 
        order_id, 
        product, 
        ROW_NUMBER() OVER(PARTITION BY order_id ORDER BY item_id DESC) as rn
    FROM items
)
SELECT order_id, product
FROM ranked_items
WHERE rn = 1
"""
print(pd.read_sql(window_query, conn))

Una fila por pedido. El artículo específico que solicitamos, sin duplicados.

Poniéndolo todo junto: Un ejemplo del mundo real

Digamos que te piden “Ingreso total por categoría.”

  1. Enfoque ingenuo: Join Orders -> Items -> Categories. Los ingresos resultan inflados, porque un artículo pertenece a dos categorías.
  2. Diagnóstico: El total marca $500. Tu cuenta bancaria dice $250.
  3. La solución: La tabla Categories tiene duplicados. Escribes un CTE para deduplicar primero y luego ejecutar el join.

Cómo prevenir errores de JOIN en el futuro

  • Revisa los conteos desde el principio: Ejecuta SELECT COUNT(*) después de cada nuevo join que añadas.
  • Conoce tus claves: No asumas que una columna es única. Verifícalo con GROUP BY...HAVING COUNT(*) > 1.
  • Visualiza la relación: ¿Es 1:1, 1:N, o N:N? Con 1:N, si deseas obtener una sola fila, debes agregar o filtrar.

Resumen: Lo que ya sabes

  • Los joins son multiplicación, no solo búsquedas.
  • Las relaciones de uno a muchos son la causa #1 de las “filas fantasma”.
  • DISTINCT es una solución rápida, pero GROUP BY y los CTE son más robustos.
  • Siempre valida tus recuentos de filas contra tus tablas de origen.

La próxima vez, veremos cómo hacer que estos joins sean rápidos — y no solo correctos.

Comprueba tu comprensión

Las preguntas a continuación progresan desde recuerdo simple hasta diseño abierto, siguiendo aproximadamente la Taxonomía de Bloom.

Recordar ¿Cuál es el “diagnóstico de 30 segundos” que el artículo recomienda para comprobar si un join duplicó filas?

Comprender En tus propias palabras, explica por qué el artículo dice que un JOIN es fundamentalmente una “operación de multiplicación” en lugar de una “búsqueda,” usando el ejemplo de orders/items (2 pedidos convirtiéndose en 4 filas).

Aplicar Usando el ejemplo del artículo, si orders tiene 2 filas y cada pedido tiene 3 filas coincidentes en items, y luego unes ese resultado a una tabla categories donde cada producto coincide con 2 filas de categorías, ¿cuántas filas totales esperarías en el resultado final?

Analizar El artículo distingue el Error #1 (unir una relación genuina de uno a muchos) del Error #2 (unir a una tabla de referencia con filas duplicadas accidentales). Explica paso a paso por qué la consulta de diagnóstico del Error #2 (GROUP BY join_key HAVING COUNT(*) > 1 en la tabla de referencia) no detectaría el Error #1 — ¿qué es fundamentalmente diferente entre las dos situaciones?

Evaluar El artículo llama a SELECT DISTINCT un “parche” y recomienda GROUP BY/agregación como la corrección “correcta”. Critica esto como una regla general: ¿existe un escenario donde recurrir a DISTINCT sea en realidad la opción más correcta, y no solo la más perezosa?

Crear Diseña un diagnóstico y una corrección para un escenario nuevo: un reporte que une Employees con Department_History (que tiene una fila por cada cambio de departamento, por lo que un empleado tiene múltiples filas si cambió de departamentos) debe mostrar “el departamento actual de cada empleado.” Usando el patrón de Corrección #4 del artículo (funciones de ventana), esboza la lógica de consulta que usarías para obtener exactamente una fila por empleado.

Aplica lo que aprendiste

Despliegas y mantienes un pipeline de provisión de características que une `orders → items → categories` para producir características de "ingresos totales por categoría" para un modelo de churn en producción. El equipo de ML acaba de reportar que las características de ingresos arrojan valores aproximadamente 2× por encima del valor correcto — el mismo síntoma que describe el artículo ($500 reportado vs \$250 real). El servicio está en vivo y la próxima extracción de datos de entrenamiento es en dos horas. Encuentra la duplicación silenciosa, demuéstrala y escribe la corrección.

Entregable: Un reporte de incidente (150-250 palabras) que contenga: (1) qué tabla está causando la duplicación y por qué, (2) el SQL de diagnóstico exacto que lo demuestra — tanto la verificación GROUP BY … HAVING COUNT(*) > 1 en la tabla culpable como la comparación de conteo de filas COUNT(*) vs COUNT(DISTINCT order_id), (3) la consulta corregida usando una CTE o subconsulta para arreglar el problema antes del join, y (4) por qué un SELECT DISTINCT general fallaría aquí.

Rúbrica (lista de verificación):

  • Nombra categories como la fuente de la duplicación — la columna product tiene entradas duplicadas que mapean ‘Apple’ tanto a ‘Fruit’ como a ‘Snack’ (filas no idénticas, no una duplicación por copiar y pegar)
  • Ejecuta GROUP BY product HAVING COUNT(*) > 1 en categories y reporta que devuelve filas
  • Ejecuta el diagnóstico de 30 segundos (COUNT(*) vs COUNT(DISTINCT order_id)) y reporta el ratio de inflación de 2× (por ejemplo, 4 filas totales vs 2 órdenes únicas)
  • Escribe una corrección usando una CTE o subconsulta que deduplica o agrega antes del join — no un SELECT DISTINCT aplicado después
  • Explica que DISTINCT en el resultado final falla porque las filas duplicadas llevan diferentes valores de category (‘Fruit’ vs ‘Snack’), por lo que no son filas idénticas
  • Confirma que el conteo de filas de la consulta corregida coincide con el conteo original de orders (2 filas para 2 órdenes, no 4)

Esta traducción fue generada automáticamente y puede contener errores. Si el idioma inglés es tu preferencia, puedes leer el artículo original en inglés .

¿Buscas otra cosa?

Busca en todos los artículos por título, resumen o tema.