Curso SQL · Northwind
Tema 10 de 60
Bloque 2 · La consulta PostgreSQL 17 northwind

DISTINCT y duplicados

1. Qué es

DISTINCT elimina filas de resultado repetidas. Es el paso 6 del orden-logico-de-ejecucion: actúa sobre lo que el SELECT ya produjo, no sobre la tabla.

Y ahí está todo lo importante del tema, en dos frases:

  • DISTINCT compara la fila entera del SELECT, no la primera columna.
  • DISTINCT es a veces la respuesta y muchas veces una tirita. Cuando aparece para "arreglar" filas duplicadas que no esperabas, casi siempre está tapando un error en el FROM.

Este último punto es el que hace que este tema cierre el bloque 1 y no lo abra.

2. Sintaxis

SELECT DISTINCT columna1, columna2 FROM tabla;        -- filas únicas
SELECT count(DISTINCT columna) FROM tabla;            -- cuántos valores distintos
SELECT DISTINCT ON (columna) * FROM tabla ORDER BY columna, otra;   -- solo PostgreSQL

DISTINCT va justo después de SELECT y afecta a todas las columnas. No existe SELECT col1, DISTINCT col2.

3. Ejemplos sobre Northwind

Los valores distintos de una columna:

SELECT DISTINCT country FROM customers ORDER BY country;
  country
-----------
 Argentina
 Austria
 Belgium
 Brazil
 ...

21 países para 91 clientes.

Contar distintos sin listarlos:

SELECT count(*)                AS filas,
       count(DISTINCT country) AS paises,
       count(DISTINCT city)    AS ciudades
FROM customers;
 filas | paises | ciudades
-------+--------+----------
    91 |     21 |       69

Esas tres cifras juntas son un perfilado: 91 clientes repartidos en 69 ciudades y 21 países. Con eso ya sabes que la ciudad casi identifica al cliente y el país no.

DISTINCT mira la fila entera

SELECT DISTINCT country, city FROM customers ORDER BY country, city;
  country  |     city
-----------+--------------
 Argentina | Buenos Aires
 Austria   | Graz
 Austria   | Salzburg
 Belgium   | Bruxelles
 Belgium   | Charleroi

69 filas, no 21. Austria sale dos veces porque las parejas son distintas. Si esperabas 21, estabas pensando en DISTINCT country y escribiste otra cosa.

DISTINCT sí junta los NULL

SELECT DISTINCT region FROM customers ORDER BY region NULLS LAST;

Devuelve 19 filas: 18 regiones más una fila vacía. Pero:

SELECT count(DISTINCT region) FROM customers;   -- 18

DISTINCT trata todos los NULL como uno solo y los devuelve; count(DISTINCT ...) los ignora. No es contradictorio —count ignora nulos siempre, null-y-la-logica-de-tres-valores— pero explica por qué la lista tiene una fila más que el conteo. Si comparas las dos cifras sin saberlo, parecen un error.

DISTINCT vs GROUP BY

SELECT DISTINCT country FROM customers;
SELECT country FROM customers GROUP BY country;

Idénticas: mismo resultado y mismo plan de ejecución. La diferencia es de intención:

  • DISTINCT dice "quita repetidos".
  • GROUP BY dice "agrupa", y deja la puerta abierta a añadir un count(*) o un sum().

Usa DISTINCT cuando solo quieras la lista, y GROUP BY en cuanto vayas a agregar algo. Casi siempre acabas queriendo agregar algo.

La parte importante: DISTINCT como tirita

Este es el ejemplo que hay que entender. Un JOIN entre pedidos y sus líneas:

SELECT count(*) FROM orders;                                    --  830
SELECT count(*) FROM orders o
JOIN order_details od ON o.order_id = od.order_id;              -- 2155

El JOIN multiplicó las filas: cada pedido aparece una vez por cada línea que tiene. Es correcto —así funciona un join, inner-join— pero rompe cualquier suma sobre columnas del pedido:

SELECT sum(freight) FROM orders;                                --  64942.69  ← el real
SELECT sum(o.freight) FROM orders o
JOIN order_details od ON o.order_id = od.order_id;              -- 207306.10  ← inflado

Un 219% de más. Y ahora la tentación, que es exactamente lo que hace todo el mundo la primera vez:

SELECT sum(DISTINCT o.freight) FROM orders o
JOIN order_details od ON o.order_id = od.order_id;              --  64526.94

64.526,94. Se parece muchísimo al bueno. Y está mal.

sum(DISTINCT ...) no suma "un flete por pedido": suma valores de flete distintos. Dos pedidos que casualmente costaron 32,38 de flete cuentan una sola vez. La diferencia son 415,75 escondidos entre casi 65.000 — el tipo de error que pasa todas las revisiones, porque el número es plausible.

Regla: si DISTINCT aparece para arreglar filas de más, el problema está en el FROM, no en el SELECT. La solución correcta es agregar al nivel adecuado, no deduplicar al final. Es el tema joins-multiples-y-granularidad, y por eso este ejemplo se planta aquí: para que cuando llegues allí ya te suene el peligro.

Nota complementaria: count(DISTINCT o.order_id) sobre ese mismo join devuelve 830 correctamente. Contar distintos suele ser legítimo; sumar distintos casi nunca lo es.

DISTINCT ON — el atajo de PostgreSQL

El último pedido de cada cliente:

SELECT DISTINCT ON (customer_id)
       customer_id, order_id, order_date
FROM orders
ORDER BY customer_id, order_date DESC, order_id DESC;
 customer_id | order_id | order_date
-------------+----------+------------
 ALFKI       |    11011 | 1998-04-09
 ANATR       |    10926 | 1998-03-04
 ANTON       |    10856 | 1998-01-28
 AROUT       |    11016 | 1998-04-10

Se queda con la primera fila de cada grupo, y "primera" la define el ORDER BY.

Dos reglas que no son negociables:

  1. El ORDER BY debe empezar por las mismas columnas del DISTINCT ON. Si no, error.
  2. Lo que venga después decide cuál se queda. Sin order_date DESC te quedas con un pedido cualquiera del cliente, no con el último.

DISTINCT ON no existe en el estándar SQL ni en SQL Server. Es de las cosas más cómodas de PostgreSQL y de las menos portables. El equivalente universal es ROW_NUMBER() (ranking-row-number-rank-ntile).

Encontrar duplicados, que no es lo mismo que quitarlos

DISTINCT los esconde. Para verlos se usa GROUP BY + HAVING:

SELECT customer_id, order_date, count(*)
FROM orders
GROUP BY customer_id, order_date
HAVING count(*) > 1
ORDER BY count(*) DESC;
 customer_id | order_date | count
-------------+------------+-------
 LACOR       | 1998-03-24 |     2
 LINOD       | 1998-01-19 |     2
 SAVEA       | 1997-10-22 |     2

7 casos de un cliente con dos pedidos el mismo día. ¿Son duplicados? Vamos a mirar:

SELECT order_id, customer_id, order_date, employee_id, freight
FROM orders WHERE customer_id = 'LACOR' AND order_date = '1998-03-24';
 order_id | customer_id | order_date | employee_id | freight
----------+-------------+------------+-------------+---------
    10972 | LACOR       | 1998-03-24 |           4 |    0.02
    10973 | LACOR       | 1998-03-24 |           6 |   15.17

No son duplicados. Distinto id, distinto empleado, distinto flete: son dos pedidos reales del mismo cliente el mismo día. Y esa es la lección del ejercicio:

GROUP BY ... HAVING count(*) > 1 no encuentra duplicados. Encuentra repeticiones de la clave que tú elegiste. Si eliges mal la clave, todo parece duplicado. Decidir qué combinación de columnas debería ser única es una pregunta de negocio, no de SQL.

4. Errores comunes

  1. Creer que DISTINCT aplica solo a la primera columna. Aplica a la fila entera.
  2. Usar DISTINCT para tapar un JOIN que multiplica filas. Ver arriba. Arregla el FROM.
  3. sum(DISTINCT x) o avg(DISTINCT x). Casi siempre da un número equivocado y creíble.
  4. Comparar DISTINCT col con count(DISTINCT col) y asustarse por la diferencia de uno. Es el NULL.
  5. DISTINCT ON sin el ORDER BY correcto. O da error, o te devuelve una fila arbitraria del grupo.
  6. Poner DISTINCT "por si acaso". Cuesta una ordenación o un hash sobre todo el resultado, y sobre todo te quita el aviso: si sobran filas, quieres enterarte.

5. Diferencias con SQL Server

PostgreSQL SQL Server
Filas únicas SELECT DISTINCT SELECT DISTINCT — igual
Contar distintos count(DISTINCT col) COUNT(DISTINCT col) — igual
Primera fila por grupo DISTINCT ON ❌ No existe → ROW_NUMBER() OVER (PARTITION BY ...)
count(DISTINCT) con ventana No permitido No permitido

DISTINCT ON es el único punto de divergencia, y es grande: una consulta con DISTINCT ON no se puede pegar en SQL Server. Si el material tiene que valer para los dos motores, escribe la versión con ROW_NUMBER().

6. Ejercicios

  1. Los cargos (title) distintos que existen en employees, y cuántos hay.
  2. Cuántos países distintos aparecen como destino de un pedido (ship_country), y si coinciden con los países de los clientes.
  3. Las parejas país-ciudad distintas de suppliers, ordenadas.
  4. El pedido más antiguo de cada cliente, en una sola consulta.
  5. Explica por qué estas dos consultas dan números distintos y cuál es la correcta para "cuántos clientes han comprado": sql SELECT count(*) FROM orders; SELECT count(DISTINCT customer_id) FROM orders;

7. Pregunta de negocio

Te piden "cuántos clientes distintos compraron en 1997 y de qué países". Un compañero ya la escribió y le salen 89 clientes, pero la empresa solo tiene 91 clientes en total y sabe que muchos no compraron ese año. ¿Qué pasó?

SolucionesResuélvelos antes de abrir

1.

SELECT DISTINCT title FROM employees ORDER BY title;
SELECT count(DISTINCT title) FROM employees;    -- 4
          title
--------------------------
 Inside Sales Coordinator
 Sales Manager
 Sales Representative
 Vice President, Sales

4 cargos para 9 empleados. Y como aquí sí quieres saber cuántos hay de cada uno, la consulta que de verdad quieres es un GROUP BY, no un DISTINCT.

2.

SELECT count(DISTINCT ship_country) FROM orders;    -- 21
SELECT count(DISTINCT country)      FROM customers; -- 21

Coinciden: 21 y 21. Pero coincidir en el número no es coincidir en la lista — eso se comprueba con EXCEPT, que está en union-intersect-except:

SELECT ship_country FROM orders EXCEPT SELECT country FROM customers;   -- 0 filas

Comprobarlo importa: ship_country es la dirección de envío, que puede ser distinta del país del cliente. Que aquí coincidan es un hecho de estos datos, no una garantía del modelo.

3.

SELECT DISTINCT country, city FROM suppliers ORDER BY country, city;

29 filas para 29 proveedores: ningún proveedor comparte ciudad con otro.

4.

SELECT DISTINCT ON (customer_id)
       customer_id, order_id, order_date
FROM orders
ORDER BY customer_id, order_date, order_id;

Igual que el ejemplo del último pedido, pero sin DESC. Devuelve 89 filas, no 91: dos clientes no han comprado nunca y no aparecen en orders. Ese detalle es el corazón de la pregunta de negocio.

5. count(*) sobre orders cuenta pedidos: 830. count(DISTINCT customer_id) cuenta clientes que aparecen en la tabla de pedidos: 89.

La correcta para "cuántos clientes han comprado" es la segunda. La primera responde a otra pregunta. Cuando una métrica se llama "clientes" y sale un número parecido al de pedidos, hay un DISTINCT olvidado.

Pregunta de negocio.

Lo que pasó: le falta el filtro de año. La consulta de tu compañero es esta o equivalente:

SELECT count(DISTINCT customer_id) FROM orders;   -- 89, de todos los años

La correcta:

SELECT count(DISTINCT customer_id) AS clientes_1997
FROM orders
WHERE order_date >= '1997-01-01' AND order_date < '1998-01-01';
 clientes_1997
---------------
            86

Y con países:

SELECT c.country, count(DISTINCT o.customer_id) AS clientes
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
WHERE o.order_date >= '1997-01-01' AND o.order_date < '1998-01-01'
GROUP BY c.country
ORDER BY clientes DESC;

Las tres cosas que enseña este caso:

  1. 89 no era un error de DISTINCT, era un WHERE que faltaba. El DISTINCT estaba bien puesto. Cuando un número sale raro, la primera sospecha no siempre es la última cláusula que escribiste.
  2. 86 de 91 clientes compraron en 1997. Que casi todos los clientes de la tabla compren cada año dice algo del dataset: customers no es un histórico de altas, es la cartera activa.
  3. El país es del cliente, no del pedido. He usado c.country y no o.ship_country a propósito, porque la pregunta era "de qué países son los clientes". Si la pregunta hubiera sido "a dónde enviamos", la columna sería la otra. Son dos informes distintos con nombres casi iguales, y equivocarse no da ningún error.