Curso SQL · Northwind
Tema 12 de 64
Bloque 2 · La consulta PostgreSQL 17 northwind

DISTINCT y duplicados

distinct

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

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

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

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

3.4 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).

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ó?

? RespuestaRespóndela antes de abrir

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