SELECT: extraindo os dados certos

DISTINCT: mantendo só o que é único

22 min
O que você vai aprender
  • remover repetições da saída com SELECT DISTINCT
  • entender que a unicidade não é avaliada por uma única coluna «principal», e sim pela combinação inteira de colunas escolhidas
  • obter um dicionário de valores: quais categorias, status, cidades ou outras variantes realmente aparecem na tabela
  • distinguir as tarefas do DISTINCT das tarefas do GROUP BY
  • entender que o DISTINCT, sozinho, não ordena o resultado
  • explicar por que o DISTINCT pode sair caro em tabelas grandes
  • reconhecer o antipadrão: um DISTINCT acrescentado só para mascarar duplicatas depois de um JOIN

Só os valores únicos

No grande salão do depósito, cada som tem eco. O mesmo status se repete em milhares de pedidos. A mesma categoria atravessa centenas de itens. A mesma cidade acende no perfil de vários usuários.

Quando você precisa de todo o fluxo de dados, as repetições são normais.

Por exemplo, se você olha os pedidos, o status paid pode aparecer muitas vezes:

SELECT id, status
FROM orders;

O resultado pode ser assim:

idstatus
1paid
2paid
3pending
4cancelled
5paid
6pending

Aqui cada linha é um pedido separado. Os status repetidos não são erro. Vários pedidos simplesmente têm o mesmo status.

Mas às vezes você não precisa do fluxo de pedidos, e sim de um pequeno dicionário:

Quais status existem nesta tabela, afinal?

Nesse caso não precisamos de todas as repetições de paid e pending. Basta manter cada valor uma única vez.

Para isso usamos o DISTINCT:

SELECT DISTINCT status
FROM orders;

Resultado:

status
paid
pending
cancelled

O DISTINCT remove as linhas repetidas do resultado da consulta.

Ele não altera os dados da tabela.
Ele não apaga linhas do banco.
Ele só limpa as duplicatas da saída.

QUERY: O eco do arquivo nem sempre é ruído. Às vezes ele mostra a frequência dos acontecimentos. Mas, se o que você quer é um dicionário e não um coral de repetições, ligue o silenciador: DISTINCT.

Dezenas de hologramas-repetições idênticos e semitransparentes colapsam num único registro nítido
O DISTINCT silencia o eco do arquivo: milhares de repetições se condensam em um pequeno dicionário de valores únicos.

Onde se escreve o DISTINCT

DISTINCT vem logo depois do SELECT:

SELECT DISTINCT status
FROM orders;

Essa posição importa.

Assim não:

SELECT status DISTINCT
FROM orders;

E assim também não:

SELECT status
FROM orders
DISTINCT;

A forma correta é:

SELECT DISTINCT colunas
FROM tabela;

Por exemplo, um dicionário das categorias dos itens:

SELECT DISTINCT category
FROM products;

Um dicionário das cidades dos usuários:

SELECT DISTINCT city
FROM users;

Um dicionário dos status dos pedidos:

SELECT DISTINCT status
FROM orders;

Em todas essas consultas a ideia é a mesma:

Mostre quais valores aparecem, sem repetições.

with dupesDISTINCTunique6 values → 3 unique
O DISTINCT compara as linhas pela combinação inteira das colunas selecionadas: cada combinação permanece no resultado exatamente uma vez.
O eco foi silenciado: todos os status de orders, cada um uma vez. Entre eles está aquele mesmo pending, congelado para sempre.

DISTINCT em uma única coluna

O caso mais simples é escolher os valores únicos de uma coluna.

Por exemplo, a tabela products:

idnamecategory
1Ração "Atum Lunar"Ração
2Brinquedo "Rato Laser"Brinquedos
3Livro "SQL para catonautas"Livros
4Tigela antigravitacionalAcessórios
5Livro "Memória da velha Terra"Livros
6Brinquedo "Rato Interceptador"Brinquedos

Se você escrever uma consulta comum:

SELECT category
FROM products;

o resultado mostra a categoria de cada linha:

category
Ração
Brinquedos
Livros
Acessórios
Livros
Brinquedos

Mas se você precisa da lista de categorias sem repetições:

SELECT DISTINCT category
FROM products;

o resultado fica assim:

category
Ração
Brinquedos
Livros
Acessórios

DISTINCT não escolhe "o primeiro item da categoria".
Ele não agrupa itens para fazer contas.
Ele apenas responde a uma pergunta:

Quais valores diferentes existem nesta coluna?

DISTINCT em várias colunas

O detalhe principal desta aula:

a unicidade é avaliada sobre a linha inteira do resultado.

Se você escolhe uma coluna, o DISTINCT remove as repetições dessa coluna.

SELECT DISTINCT category
FROM products;

Mas se você escolhe duas colunas:

SELECT DISTINCT category, status
FROM products;

o DISTINCT passa a procurar pares únicos:

category + status

Por exemplo, considere esta tabela:

idcategorystatus
1Livrosactive
2Livrosactive
3Livrosarchived
4Brinquedosactive
5Brinquedosactive
6Brinquedosarchived

A consulta:

SELECT DISTINCT category
FROM products;

devolve:

category
Livros
Brinquedos

Já a consulta:

SELECT DISTINCT category, status
FROM products;

devolve:

categorystatus
Livrosactive
Livrosarchived
Brinquedosactive
Brinquedosarchived

Por quê?

Porque agora a unicidade não é avaliada só por category, e sim pelo par:

category + status

As linhas:

Livros + active
Livros + archived

são diferentes para o SQL, mesmo tendo a mesma categoria.

Por isso DISTINCT não significa "torne a primeira coluna única". Significa:

Remova do resultado as linhas repetidas por inteiro.

A unicidade é avaliada sobre a combinação inteira das colunas escolhidas. Um mesmo país pode aparecer várias vezes — ao lado dele, cidades diferentes.

Como o DISTINCT trata o NULL

NULL significa ausência de valor, mas, para o DISTINCT, várias linhas com NULL na mesma coluna contam como repetição.

Por exemplo, a tabela users:

idnamecity
1AnnaMoscou
2Boris
3VeraKazan
4GlebNULL
5DanaMoscou

A consulta:

SELECT DISTINCT city
FROM users;

devolve um conjunto parecido com este:

city
Moscou
Kazan
NULL

As duas linhas com NULL não aparecem duas vezes. No dicionário de cidades fica uma única linha NULL.

Isso não contradiz a aula anterior sobre NULL.

Em uma condição WHERE, comparar com NULL usando =UNKNOWN. Mas o DISTINCT resolve outro problema: ele remove as repetições de um resultado já pronto. Na deduplicação, vários valores ausentes na mesma coluna se juntam em uma linha só.

Se você não quer ver NULL no dicionário, acrescente um filtro:

SELECT DISTINCT city
FROM users
WHERE city IS NOT NULL
ORDER BY city;

Assim o resultado mostra apenas as cidades preenchidas.

O DISTINCT não ordena o resultado

DISTINCT remove as repetições, mas não define a ordem das linhas.

Por exemplo:

SELECT DISTINCT status
FROM orders;

pode devolver:

status
pending
paid
cancelled

E, em outra situação, a ordem pode ser diferente.

Isso é normal: uma tabela SQL não é obrigada a devolver as linhas em uma ordem "natural" se você não pediu a ordenação explicitamente.

Se você quer uma lista caprichada, acrescente o ORDER BY:

SELECT DISTINCT status
FROM orders
ORDER BY status;

Agora a consulta diz duas coisas separadas:

SELECT DISTINCT status

— remova as repetições;

ORDER BY status

— ordene o resultado por status.

Não pense que o DISTINCT "coloca ordem" sozinho. Ele silencia as duplicatas, não ordena.

DISTINCT vs. GROUP BY

Para um dicionário simples de valores, estas duas consultas podem dar o mesmo resultado:

SELECT DISTINCT category
FROM products;

e:

SELECT category
FROM products
GROUP BY category;

As duas retornam a lista de categorias sem repetições.

Mas o sentido de cada uma é diferente.

O DISTINCT responde à pergunta:

Quais valores existem?

O GROUP BY responde a outra:

Como dividir as linhas em grupos para depois calcular algo em cada um deles?

Por exemplo, se você só precisa da lista de categorias, basta o DISTINCT:

SELECT DISTINCT category
FROM products;

Já se você precisa saber quantos produtos há em cada categoria, aí entra o GROUP BY:

SELECT category, COUNT(*) AS products_count
FROM products
GROUP BY category;

O resultado:

categoryproducts_count
Acessórios5
Brinquedos8
Livros4
Ração12

Aqui o GROUP BY faz mais do que remover repetições. Ele reúne as linhas em grupos para que a função de agregação COUNT(*) conte quantas linhas há dentro de cada grupo.

Por isso, a regra prática é esta:

Se você precisa de um dicionário de valores, use o DISTINCT.

SELECT DISTINCT category
FROM products;

Se você precisa calcular algo por grupo, use o GROUP BY.

SELECT category, COUNT(*)
FROM products
GROUP BY category;

O preço do silêncio

DISTINCT parece uma palavrinha pequena, mas o banco faz um trabalho real por trás dela.

Para remover as repetições, o SGBD precisa descobrir quais linhas do resultado são iguais. Para isso, ele precisa comparar as linhas entre si. Dependendo da situação, o banco pode:

  • ordenar o resultado e remover as repetições que ficarem lado a lado;
  • construir uma tabela hash de valores únicos;
  • aproveitar um índice adequado, se ele ajudar.

Em uma tabela pequena, é quase impossível notar a diferença.

Por exemplo, se products tem só 20 linhas, a consulta:

SELECT DISTINCT category
FROM products;

vai rodar rápido.

Mas se a tabela tem milhões de linhas e você seleciona muitas colunas, a deduplicação pode virar uma fatia considerável do custo da consulta.

Uma consulta como esta sai especialmente cara:

SELECT DISTINCT *
FROM orders;

Aqui o banco precisa comparar linhas inteiras, em todas as colunas selecionadas. Quanto mais linhas e mais colunas, mais trabalho.

Por isso não vale a pena acrescentar DISTINCT “por via das dúvidas”.

Uma boa pergunta antes de usá-lo:

Que repetições exatamente eu quero remover, e por que elas apareceram?

Se a resposta for:

Preciso de um dicionário de status.

então o DISTINCT é a ferramenta certa.

SELECT DISTINCT status
FROM orders;

Se a resposta for:

Depois de juntar as tabelas, apareceram duplicatas estranhas, então eu acrescentei DISTINCT.

isso já é um sinal de alerta. Talvez o problema não esteja no resultado, e sim na lógica da junção.

DISTINCT depois do JOIN: quando ele mascara um problema

O cenário mais perigoso é acrescentar DISTINCT para “consertar” as duplicatas que aparecem depois de um JOIN.

Ainda não estudamos as junções em detalhe, mas vale guardar a ideia desde já.

Suponha que existam usuários e pedidos.

Um mesmo usuário pode fazer muitos pedidos.

Se você juntar usuários com pedidos, a linha do usuário se multiplica: uma linha para cada pedido dele.

Por exemplo:

user_idnameorder_id
1Anna101
1Anna102
1Anna103
2Boris104

Se você quer apenas os usuários, vai ver a Anna três vezes.

Às vezes, nessa situação, a pessoa escreve:

SELECT DISTINCT users.id, users.name
FROM users
JOIN orders ON orders.user_id = users.id;

E, à primeira vista, o resultado fica “bonito”:

idname
1Anna
2Boris

Mas é importante entender: aqui o DISTINCT não explicou por que a Anna se multiplicou. Ele apenas eliminou as linhas repetidas depois da junção.

Às vezes isso é aceitável, se você quer mesmo a lista de usuários que têm pelo menos um pedido.

Mas se o DISTINCT foi acrescentado só porque “sem ele aparecem duplicatas, sei lá por quê”, isso é um mau cheiro na consulta.

A abordagem correta é entender a natureza dos dados:

  • qual é a relação entre as tabelas: um para um ou um para muitos;
  • por que uma linha da esquerda corresponde a várias linhas da direita;
  • se você realmente precisa da lista de usuários únicos;
  • ou se precisa contar os pedidos;
  • ou se precisa escolher um pedido específico;
  • ou se precisa mudar a condição da junção.

DISTINCT depois de um JOIN pode ser uma ferramenta legítima, mas não deve virar um curativo para um problema que você não entendeu.

QUERY: Se o eco apareceu depois que você abriu o salão vizinho, não corra para silenciar o arquivo inteiro. Primeiro veja qual porta você abriu.

Bônus do PostgreSQL: DISTINCT ON

No PostgreSQL existe uma construção especial:

SELECT DISTINCT ON (expressão) ...

Ela é diferente do DISTINCT comum.

O DISTINCT comum remove as linhas do resultado que são totalmente iguais.

Por exemplo:

SELECT DISTINCT category, status
FROM products;

mantém as combinações únicas de category + status.

Já o DISTINCT ON diz:

Deixe apenas uma linha para cada valor desta expressão.

Por exemplo, você precisa escolher um produto de cada categoria:

SELECT DISTINCT ON (category)
       category, name, price
FROM products
ORDER BY category, price DESC;

Essa consulta pode ser lida assim:

  1. separe as linhas por category;
  2. dentro de cada categoria, ordene os produtos do mais caro para o mais barato;
  3. fique com a primeira linha de cada categoria.

O resultado pode ser assim:

categorynameprice
AcessóriosCasinha-portal3000
BrinquedosBrinquedo “Rato Laser”1700
LivrosLivro “SQL para catonautas”2500
RaçãoRação “Atum Lunar”1900

Aqui o DISTINCT ON (category) deixa uma linha por categoria, e o ORDER BY category, price DESC define exatamente qual linha vem primeiro dentro de cada categoria.

Importante:

DISTINCT ON é uma extensão do PostgreSQL. No SQL padrão e em muitos outros SGBDs ele não existe.

No nível básico, o importante não é decorá-lo como ferramenta obrigatória, e sim entender a diferença:

  • o DISTINCT mantém as linhas únicas do resultado;
  • o DISTINCT ON do PostgreSQL mantém a primeira linha de cada grupo definido pela expressão indicada.
Interview question

Pergunta de entrevista:
O que o DISTINCT faz no SELECT?

Resposta forte:
O DISTINCT remove as linhas repetidas do resultado da consulta. Ele não altera os dados da tabela nem apaga duplicatas do banco em si. Ele age só no nível da saída. Se várias colunas forem selecionadas, a unicidade é avaliada pela combinação inteira das colunas escolhidas.


Pergunta de entrevista:
O que SELECT DISTINCT category, status FROM products retorna?

Resposta forte:
Ele retorna os pares únicos de category + status. Não é a lista de categorias únicas nem a lista de status únicos, tomadas separadamente. Se uma mesma categoria aparece com status diferentes, ela vai surgir várias vezes no resultado — uma vez para cada par único.


Pergunta de entrevista:
Em que SELECT DISTINCT x FROM t difere de SELECT x FROM t GROUP BY x?

Resposta forte:
Sem funções de agregação, o resultado costuma ser o mesmo: as duas consultas retornam os valores únicos de x. Mas a finalidade é diferente. O DISTINCT é usado quando você quer um dicionário de valores: quais valores existem, afinal. O GROUP BY é usado quando você precisa dividir as linhas em grupos e calcular algo em cada grupo, por exemplo COUNT(*), SUM(...), AVG(...).


Pergunta de entrevista:
O DISTINCT garante a ordenação do resultado?

Resposta forte:
Não. O DISTINCT remove as repetições, mas não define a ordem das linhas. Se você precisa de uma ordem previsível, use o ORDER BY.


Pergunta de entrevista:
Quando um DISTINCT na consulta pode ser sinal de erro?

Resposta forte:
O sinal de alerta é um DISTINCT acrescentado só para tirar as duplicatas que apareceram depois de um JOIN. Muitas vezes essas duplicatas vêm da multiplicação de linhas numa junção um-para-muitos ou de uma condição de junção errada. Nessa situação, o DISTINCT mascara o sintoma e ainda dá ao banco o trabalho extra de deduplicar. O certo é entender por que as linhas se multiplicaram e corrigir a lógica da consulta.

Check yourself
O que o DISTINCT faz em um SELECT?
Check yourself
Como a unicidade é avaliada na consulta SELECT DISTINCT category, status FROM products?
Check yourself
Qual consulta expressa melhor a ideia “mostre quais categorias existem entre os produtos”?
Check yourself
O que é verdade sobre DISTINCT e ORDER BY?

QUERY: O DISTINCT serve para quando você silencia o eco de propósito. Se você não sabe de onde o eco veio, procure primeiro a fonte.

Practice: solve the tasks
Solved 0 of 3 · any 2 is enough to pass