Как проверить уникальность в SQL

Проверь себя · 1/3разбор после ответа
Нужно сгруппировать события по календарному месяцу в PostgreSQL, чтобы все дни одного месяца попали в одну группу. Какой подход правильный?

Зачем проверять уникальность

Уникальность — фундаментальная гарантия чистоты данных. user_id должен быть уникальным в таблице users, (order_id, product_id) — уникальной парой в order_items. Если уникальность нарушена, ломается всё: агрегация завышает цифры, JOIN создаёт дубликаты, BI-отчёт показывает «150% конверсии».

При приёмке данных от ETL или при тестировании dbt-моделей уникальность — один из первых sanity-чеков. Также это популярный вопрос на собеседовании: «как быстро проверить, что в колонке все значения уникальны?».

В статье — все способы:

  • COUNT(*) vs COUNT(DISTINCT) — есть ли дубли вообще
  • GROUP BY + HAVING — конкретные дубли
  • Проверка по комбинации колонок
  • Case-insensitive и с обрезкой пробелов
  • UNIQUE constraint и уникальный индекс — защита от появления дублей
  • INSERT с ON CONFLICT (UPSERT)

Три способа проверить уникальность

  1. COUNT vs COUNT DISTINCT — быстро узнать, есть ли дубли
  2. GROUP BY + HAVING — найти конкретные дубли
  3. UNIQUE constraint — запретить дубли на уровне БД

1. Есть ли вообще дубликаты

SELECT
    COUNT(*) AS total,
    COUNT(DISTINCT email) AS unique_emails,
    COUNT(*) - COUNT(DISTINCT email) AS duplicates
FROM users;

Если duplicates = 0 — уникальность соблюдается.

2. Найти дубли по одной колонке

SELECT email, COUNT(*) AS cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1;

3. Найти дубли по комбинации

SELECT first_name, last_name, birth_date, COUNT(*)
FROM users
GROUP BY first_name, last_name, birth_date
HAVING COUNT(*) > 1;

4. Уникальность без учёта регистра

SELECT LOWER(email), COUNT(*) AS cnt
FROM users
GROUP BY LOWER(email)
HAVING COUNT(*) > 1;

5. Уникальность без пробелов

SELECT TRIM(name), COUNT(*) AS cnt
FROM users
GROUP BY TRIM(name)
HAVING COUNT(*) > 1;
Прокачай SQL для собеса
500+ задач по SQL: оконные функции, JOIN, CTE — с разбором каждой
Тренировать SQL в Telegram

6. Уникальность с учётом NULL

-- NULL группируется вместе (не все хотят)
SELECT email, COUNT(*)
FROM users
WHERE email IS NOT NULL
GROUP BY email
HAVING COUNT(*) > 1;

7. UNIQUE constraint (запрет дублей)

-- при создании таблицы
CREATE TABLE users (
    id INT PRIMARY KEY,
    email VARCHAR(255) UNIQUE
);

-- добавить к существующей
ALTER TABLE users
ADD CONSTRAINT uq_users_email UNIQUE (email);

-- UNIQUE на несколько колонок
ALTER TABLE order_items
ADD CONSTRAINT uq_order_product UNIQUE (order_id, product_id);

После UNIQUE constraint INSERT дубля даст ошибку.

8. Посмотреть существующие unique constraints

Postgres

SELECT conname, contype, pg_get_constraintdef(oid)
FROM pg_constraint
WHERE contype = 'u' AND conrelid = 'users'::regclass;

MySQL

SHOW INDEX FROM users WHERE Non_unique = 0;

9. Проверка перед INSERT

-- INSERT только если такой email не существует
INSERT INTO users (email, name)
SELECT '[email protected]', 'Alice'
WHERE NOT EXISTS (
    SELECT 1 FROM users WHERE email = '[email protected]'
);

Или ON CONFLICT (Postgres):

INSERT INTO users (email, name)
VALUES ('[email protected]', 'Alice')
ON CONFLICT (email) DO NOTHING;

Частые ошибки

1. NULL при сравнении

COUNT(*) и COUNT(col) возвращают разные числа. COUNT(col) игнорирует NULL, COUNT(DISTINCT col) — тоже. Это приводит к неожиданным расхождениям при проверке.

2. UNIQUE с NULL

В UNIQUE несколько NULL разрешены (так в большинстве СУБД, кроме MSSQL). Если это проблема — используйте NOT NULL UNIQUE или добавьте WHERE col IS NOT NULL в частичный индекс.

3. Регистр

Alicealice в большинстве collations. Для case-insensitive используйте LOWER() в GROUP BY или тип CITEXT в Postgres. Иначе пропустите половину дубликатов.

4. Пробелы вокруг значений

'[email protected] ' и '[email protected]' считаются разными. Всегда оборачивайте в TRIM() при проверке email и имён.

5. COUNT DISTINCT по выражению, а не колонке

COUNT(DISTINCT LOWER(email)) делает full scan и не использует индекс. Для больших таблиц лучше создать функциональный индекс на LOWER(email) заранее.

Связанные темы

FAQ

Как проверить uniqueness в огромной таблице?

COUNT DISTINCT на индексированной колонке — относительно быстро. Approximate count (uniq в ClickHouse) — ещё быстрее.

UNIQUE и PRIMARY KEY — это одно?

PK = UNIQUE + NOT NULL + один на таблицу. UNIQUE может быть несколько, с NULL.

Чем проверять уникальность в ETL?

Встраивайте в pipeline COUNT vs COUNT(DISTINCT) как sanity check. dbt tests делают это автоматически.