Как проверить уникальность в SQL
Содержание:
Зачем проверять уникальность
Уникальность — фундаментальная гарантия чистоты данных. 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)
Три способа проверить уникальность
- COUNT vs COUNT DISTINCT — быстро узнать, есть ли дубли
- GROUP BY + HAVING — найти конкретные дубли
- 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;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. Регистр
Alice ≠ alice в большинстве 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 делают это автоматически.