SELECT: precies de juiste data ophalen

DISTINCT: alleen unieke waarden overhouden

22 min
What you'll learn
  • herhalingen uit de uitvoer verwijderen met SELECT DISTINCT en uitleggen dat de uniekheid over de hele combinatie van kolommen wordt bepaald
  • taken voor DISTINCT («welke waarden komen voor») onderscheiden van taken voor GROUP BY («wat per groep berekenen»)
  • het antipatroon «DISTINCT om dubbele rijen na een JOIN te maskeren» herkennen en de kosten ervan uitleggen

Alleen unieke waarden

In de grote zaal van het magazijn heeft elk geluid een echo: dezelfde categorie, dezelfde status, steeds opnieuw in duizenden rijen. Soms heb je niet de hele stroom nodig, maar een kort woordenboek: «welke waarden komen hier eigenlijk voor». Dan moet de echo worden gedoofd.

SELECT DISTINCT kolommen verwijdert dubbele rijen uit het resultaat. Dat is handig wanneer je niet alle bestellingen nodig hebt, maar een woordenboek van waarden: welke categorieën er in products zijn, welke statussen er in orders voorkomen, welke woonplaatsen er in users zijn ingevuld.

Belangrijk: de uniekheid wordt bepaald over de hele geselecteerde combinatie van kolommen. Eén kolom betekent: de unieke waarden van die kolom. Twee kolommen betekent: de unieke paren.

Tientallen halfdoorzichtige, identieke hologrammen klappen samen tot één scherpe rij
DISTINCT dooft de echo van het archief: duizenden herhalingen klappen samen tot een kort woordenboek met unieke waarden.

Где пишется DISTINCT

DISTINCT пишется сразу после SELECT:

SELECT DISTINCT status
FROM orders;

Это важно.

Не так:

SELECT status DISTINCT
FROM orders;

И не так:

SELECT status
FROM orders
DISTINCT;

Правильная форма:

SELECT DISTINCT столбцы
FROM таблица;

Например, словарь категорий товаров:

SELECT DISTINCT category
FROM products;

Словарь городов пользователей:

SELECT DISTINCT city
FROM users;

Словарь статусов заказов:

SELECT DISTINCT status
FROM orders;

Во всех этих запросах мысль одна:

Покажи, какие значения вообще встречаются, без повторов.

with dupesDISTINCTunique6 values → 3 unique
DISTINCT vergelijkt rijen over de hele geselecteerde combinatie van kolommen: elke combinatie blijft precies één keer in het resultaat staan.
De echo is gedoofd: alle statussen uit orders, elk één keer. Daartussen staat die ene pending, voor altijd bevroren.
Query result
status
cancelled
paid
pending
refunded

DISTINCT по одному столбцу

Самый простой случай — выбрать уникальные значения одного столбца.

Например, таблица products:

idnamecategory
1Корм «Лунный тунец»Корм
2Игрушка «Лазерная мышь»Игрушки
3Книга «SQL для котонавтов»Книги
4Миска антигравитационнаяАксессуары
5Книга «Память старой Земли»Книги
6Игрушка «Мышь-перехватчик»Игрушки

Если написать обычный запрос:

SELECT category
FROM products;

результат покажет категорию каждой строки:

category
Корм
Игрушки
Книги
Аксессуары
Книги
Игрушки

Но если тебе нужен список категорий без повторов:

SELECT DISTINCT category
FROM products;

получится:

category
Корм
Игрушки
Книги
Аксессуары

DISTINCT не выбирает «первый товар категории».
Он не группирует товары для подсчётов.
Он просто отвечает на вопрос:

Какие разные значения есть в этом столбце?

DISTINCT по нескольким столбцам

Главная деталь урока:

уникальность считается по всей выбранной строке результата.

Если выбрать один столбец, DISTINCT убирает повторы по одному столбцу.

SELECT DISTINCT category
FROM products;

Но если выбрать два столбца:

SELECT DISTINCT category, status
FROM products;

то DISTINCT будет искать уникальные пары:

category + status

Например, есть таблица:

idcategorystatus
1Книгиactive
2Книгиactive
3Книгиarchived
4Игрушкиactive
5Игрушкиactive
6Игрушкиarchived

Запрос:

SELECT DISTINCT category
FROM products;

вернёт:

category
Книги
Игрушки

А запрос:

SELECT DISTINCT category, status
FROM products;

вернёт:

categorystatus
Книгиactive
Книгиarchived
Игрушкиactive
Игрушкиarchived

Почему?

Потому что теперь уникальность считается не только по category, а по паре:

category + status

Строки:

Книги + active
Книги + archived

для SQL разные, хотя категория у них одинаковая.

Поэтому DISTINCT не означает «сделай уникальным первый столбец». Он означает:

Убери повторяющиеся строки результата целиком.

Уникальность считается по всей комбинации выбранных столбцов. Одна категория может встретиться несколько раз, если рядом с ней разные статусы.
Query result
countrycity
БеларусьМинск
КазахстанАлматы
РоссияЕкатеринбург
РоссияКазань
РоссияМосква
РоссияНижний Новгород
РоссияНовосибирск
РоссияСанкт-Петербург

Как DISTINCT относится к NULL

NULL означает отсутствие значения, но для DISTINCT несколько строк с NULL в одном и том же столбце считаются повтором.

Например, таблица users:

idnamecity
1АннаМосква
2Борис
3ВераКазань
4ГлебNULL
5ДанаМосква

Запрос:

SELECT DISTINCT city
FROM users;

вернёт примерно такой набор:

city
Москва
Казань
NULL

Две строки с NULL не появятся дважды. В словаре городов будет одна строка NULL.

Это не противоречит прошлому уроку про NULL.

В условиях WHERE сравнение с NULL через = даёт UNKNOWN. Но DISTINCT решает другую задачу: он убирает повторы в готовой выдаче. Для несколько отсутствующих значений в одном столбце схлопываются в одну строку.

Если не хочешь видеть NULL в словаре, добавь фильтр:

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

Так результат покажет только заполненные города.

DISTINCT не сортирует результат

DISTINCT убирает повторы, но не отвечает за порядок строк.

Например:

SELECT DISTINCT status
FROM orders;

может вернуть:

status
pending
paid
cancelled

А в другой ситуации порядок может оказаться другим.

Это нормально: SQL-таблица не обязана возвращать строки в «естественном» порядке, если ты явно не попросил сортировку.

Если нужен аккуратный список, добавляй ORDER BY:

SELECT DISTINCT status
FROM orders
ORDER BY status;

Теперь запрос говорит две отдельные вещи:

SELECT DISTINCT status

— убери повторы;

ORDER BY status

— отсортируй результат по статусу.

Не стоит думать, что DISTINCT сам «наводит порядок». Он гасит дубли, а не сортирует.

DISTINCT против GROUP BY

Для простого словаря значений эти два запроса могут дать одинаковый результат:

SELECT DISTINCT category
FROM products;

и:

SELECT category
FROM products
GROUP BY category;

Оба вернут список категорий без повторов.

Но смысл у них разный.

DISTINCT отвечает на вопрос:

Какие значения бывают?

GROUP BY отвечает на вопрос:

На какие группы разбить строки, чтобы потом что-то посчитать по каждой группе?

Например, если тебе нужен просто список категорий, достаточно DISTINCT:

SELECT DISTINCT category
FROM products;

А если нужно узнать, сколько товаров в каждой категории, нужен GROUP BY:

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

Результат:

categoryproducts_count
Аксессуары5
Игрушки8
Книги4
Корм12

Здесь GROUP BY уже не просто убирает повторы. Он собирает строки в группы, чтобы COUNT(*) посчитала количество строк внутри каждой группы.

Поэтому практическое правило такое:

Если нужен словарь значений — используй DISTINCT.

SELECT DISTINCT category
FROM products;

Если нужно что-то посчитать по группам — используй GROUP BY.

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

Цена тишины

DISTINCT выглядит маленьким словом, но база выполняет за ним реальную работу.

Чтобы убрать повторы, должна понять, какие строки результата одинаковые. Для этого ей нужно сравнить строки между собой. В зависимости от ситуации база может:

  • отсортировать результат и убрать соседние повторы;
  • построить уникальных значений;
  • использовать уже подходящий , если он помогает.

На маленькой таблице разницу почти невозможно заметить.

Например, если в products всего 20 строк, запрос:

SELECT DISTINCT category
FROM products;

будет работать быстро.

Но если в таблице миллионы строк, а выбранных столбцов много, может стать заметной частью стоимости запроса.

Особенно дорого выглядит такой запрос:

SELECT DISTINCT *
FROM orders;

Здесь база должна сравнить целые строки по всем выбранным столбцам. Если строк много и столбцов много, работы становится больше.

Поэтому DISTINCT не стоит добавлять «на всякий случай».

Хороший вопрос перед использованием:

Какие именно повторы я хочу убрать и почему они появились?

Если ответ такой:

Мне нужен словарь статусов.

DISTINCT уместен.

SELECT DISTINCT status
FROM orders;

Если ответ такой:

У меня после появились странные дубли, поэтому я добавил DISTINCT.

Это уже тревожный сигнал. Возможно, проблема не в выдаче, а в логике соединения.

DISTINCT после JOIN: когда он маскирует проблему

Самый опасный сценарий — добавлять DISTINCT, чтобы «починить» дубли после JOIN.

Пока мы ещё не разбирали соединения подробно, но идею стоит запомнить заранее.

Допустим, есть пользователи и заказы.

Один пользователь может сделать много заказов.

Если соединить пользователей с заказами, строка пользователя размножится: по одной строке на каждый его заказ.

Например:

user_idnameorder_id
1Анна101
1Анна102
1Анна103
2Борис104

Если тебе нужны только пользователи, можно увидеть Анну три раза.

Иногда в такой ситуации человек пишет:

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

И внешне результат становится «красивым»:

idname
1Анна
2Борис

Но важно понимать: DISTINCT здесь не объяснил, почему Анна размножилась. Он просто схлопнул одинаковые строки после соединения.

Иногда это допустимо, если ты осознанно хочешь получить список пользователей, у которых есть хотя бы один заказ.

Но если DISTINCT добавлен только потому, что «без него почему-то дубли», это плохой запах запроса.

Правильный подход — понять природу данных:

  • какая связь между таблицами: один к одному или один ко многим;
  • почему одна строка слева находит несколько строк справа;
  • нужен ли тебе действительно список уникальных пользователей;
  • или нужно считать заказы;
  • или нужно выбрать один конкретный заказ;
  • или нужно изменить условие соединения.

DISTINCT после JOIN может быть нормальным инструментом, но не должен быть пластырем на непонятную проблему.

КВЕРИ: Если эхо появилось после открытия соседнего зала, не спеши глушить весь архив. Сначала проверь, какую дверь ты открыл.

Бонус PostgreSQL: DISTINCT ON

В PostgreSQL есть особая конструкция:

SELECT DISTINCT ON (выражение) ...

Она отличается от обычного DISTINCT.

Обычный DISTINCT убирает полностью одинаковые строки результата.

Например:

SELECT DISTINCT category, status
FROM products;

оставляет уникальные комбинации category + status.

А DISTINCT ON говорит:

Оставь только одну строку на каждое значение вот этого выражения.

Например, нужно выбрать по одному товару из каждой категории:

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

Такой запрос можно читать так:

  1. разбей строки по category;
  2. внутри каждой категории отсортируй товары по цене от дорогих к дешёвым;
  3. оставь первую строку каждой категории.

Результат может быть таким:

categorynameprice
АксессуарыДомик-портал3000
ИгрушкиИгрушка «Лазерная мышь»1700
КнигиКнига «SQL для котонавтов»2500
КормКорм «Лунный тунец»1900

Здесь DISTINCT ON (category) оставляет одну строку на категорию, а ORDER BY category, price DESC определяет, какая именно строка будет первой внутри категории.

Важно:

DISTINCT ON. В стандартном SQL и во многих других его нет.

Для базового уровня главное не запоминать его как обязательный инструмент, а понять отличие:

  • DISTINCT оставляет уникальные строки результата;
  • DISTINCT ON в PostgreSQL оставляет первую строку для каждой группы по указанному выражению.
Interview question

Sollicitatievraag: waarin verschilt SELECT DISTINCT x FROM t van SELECT x FROM t GROUP BY x? En wanneer is een DISTINCT in een query een teken van een fout?

Sterk antwoord: zonder aggregaten is het resultaat hetzelfde: beide verwijderen herhalingen op x, en het maakt er vaak hetzelfde plan voor. GROUP BY heb je nodig wanneer er per groep iets wordt berekend. Het alarmsignaal is een DISTINCT die is toegevoegd om dubbele rijen na een JOIN te «repareren»: hij maskeert de vermenigvuldiging van rijen in de samenvoeging en dwingt de database het hele resultaat te dedupliceren. De juiste aanpak is de samenvoeging zelf onderzoeken, niet de gevolgen ervan dempen.

Check yourself
Что делает DISTINCT в SELECT?
Check yourself
Как считается уникальность в запросе SELECT DISTINCT category, status FROM products?
Check yourself
Какой запрос лучше выражает мысль «покажи, какие категории вообще есть в товарах»?
Check yourself
Что верно про DISTINCT и ORDER BY?

КВЕРИ: DISTINCT нужен, когда ты осознанно гасишь эхо. Если не знаешь, откуда эхо взялось, сначала найди источник.