UNIQUE — значит «дубликаты запрещены»
Ограничение UNIQUE в SQLite говорит движку, что значения в столбце (или в группе столбцов) не должны повторяться от строки к строке. Именно так вы сообщаете базе: «два пользователя не могут иметь одинаковый email» или «код товара встречается не больше одного раза».
Третий INSERT падает с ошибкой UNIQUE constraint failed: users.email. SQLite проверяет ограничение при каждой записи и отклоняет всё, что создало бы дубликат. Первые две строки сохранятся, а третья просто не доедет до таблицы.
Под капотом UNIQUE — это уникальный индекс, та же структура данных, которую SQLite использует для быстрого поиска. Поэтому проверка обходится дёшево, а столбец автоматически получает индекс.
Синтаксис: на уровне столбца или на уровне таблицы
Объявить UNIQUE можно двумя способами: прямо рядом со столбцом или отдельным блоком в конце определения таблицы:
Для одного столбца оба варианта равноценны — выбирайте тот, который читается лучше. А вот табличная форма становится по-настоящему необходимой, как только уникальность нужна сразу по нескольким столбцам.
Составной уникальный ключ в SQLite: UNIQUE по нескольким столбцам
Бывает, что отдельный столбец сам по себе не уникален, но комбинация значений должна быть таковой. Один пользователь может записаться на много курсов, а на один курс — много пользователей, но одна и та же пара (user_id, course_id) не должна встречаться дважды:
Ограничение действует на пару значений, а не на каждый столбец по отдельности. Пользователь 1 может записаться на сколько угодно курсов, курс 100 может собрать сколько угодно слушателей — но конкретная связка «пользователь–курс» может встретиться только один раз.
Это классический приём для таблиц-связок в отношениях «многие ко многим».
UNIQUE vs PRIMARY KEY в SQLite
Звучат похоже, и они действительно родственники, но это не одно и то же:
- В таблице может быть максимум один
PRIMARY KEY, а вотUNIQUE-ограничений — сколько угодно. PRIMARY KEY— это идентичность строки: на него ссылаются внешние ключи, его дублируетrowid.UNIQUE— это просто «такое значение (или комбинация) не повторяется».- В обычной таблице столбец с
UNIQUEспокойно принимаетNULL; уPRIMARY KEYтак не получится (есть одно историческое исключение, его мы здесь не разбираем).
Типичный пример:
id — это то, на что ссылается вся остальная база. email и username уникальны потому, что так требует приложение, а не потому, что они идентифицируют запись. Если пользователь сменит email, id останется прежним — ради этого их и разделяют.
Нюанс с NULL
На этом спотыкаются почти все, кто сталкивается с UNIQUE впервые. Столбец с ограничением UNIQUE в SQLite спокойно принимает сколько угодно значений NULL:
Три NULL — без проблем. А вот два 'ada@example.com' — уже конфликт.
Дело вот в чём: SQL воспринимает NULL как «неизвестно», а два неизвестных значения не считаются равными — поэтому проверка уникальности просто не может объявить их дубликатами. Если нужен максимум один NULL, проще всего поставить NOT NULL UNIQUE. А если NULL допустимы, но только по одному на каждую комбинацию остальных столбцов — пригодится частичный индекс (про это поговорим позже, в главе про индексы).
Обработка конфликтов: ON CONFLICT
По умолчанию нарушение UNIQUE прерывает выполнение запроса. Но иногда хочется поведения поинтереснее — заменить старую строку, проигнорировать новую или обновить отдельные столбцы. В SQLite для этого есть два способа.
Первый — встроить логику прямо в ограничение через ON CONFLICT:
При повторной вставке ключа theme старая строка удаляется, а на её место встаёт новая. Из других вариантов доступны IGNORE (молча пропустить), ABORT (поведение по умолчанию), FAIL и ROLLBACK.
Второй подход работает на уровне отдельного запроса — это синтаксис upsert. Обычно он удобнее, потому что позволяет обновлять конкретные столбцы:
Первый INSERT создаёт строку. Следующие два натыкаются на ограничение UNIQUE и проваливаются в ветку DO UPDATE, увеличивая count. Это и есть паттерн upsert через INSERT ... ON CONFLICT — ему посвящена отдельная страница чуть дальше.
UNIQUE-ограничение и UNIQUE-индекс: в чём разница
CREATE UNIQUE INDEX решает ту же задачу, что и ограничение UNIQUE. По сути, ограничение UNIQUE под капотом само создаёт уникальный индекс — это практически один и тот же механизм, просто в разной обёртке.
Когда что выбирать:
- Ограничение (CONSTRAINT) — если уникальность является частью определения таблицы. Тогда правило задокументировано прямо рядом со столбцами.
- Уникальный индекс — если нужен частичный индекс (с условием
WHERE), хочется задать конкретное имя или добавить ограничение к уже существующей таблице, не переписывая её.ALTER TABLEв SQLite не умеет добавлять ограничения, но индекс можно навесить всегда.
При записи поведение абсолютно одинаковое. Разница только в том, где именно в схеме будет жить это правило.
Как добавить UNIQUE к существующей таблице
ALTER TABLE в SQLite намеренно урезан — никакого ALTER TABLE ... ADD CONSTRAINT тут нет. Остаются два рабочих варианта:
Вариант 2 — если очень хочется, чтобы UNIQUE был зашит прямо в определении таблицы — это танец с пересозданием таблицы: создаём новую таблицу с нужным ограничением, переливаем туда данные, удаляем старую, переименовываем. Об этом — на следующей странице.
Маленькое предупреждение: если вы пытаетесь навесить уникальность на столбец, в котором уже есть дубликаты, CREATE UNIQUE INDEX упадёт с ошибкой. Сначала почистите дубли, и только потом создавайте индекс.
Когда UNIQUE срабатывает: разбираем ошибку
Сообщение об ошибке прямо говорит, какое именно ограничение нарушено:
Error: UNIQUE constraint failed: users.email
Error: UNIQUE constraint failed: enrollments.user_id, enrollments.course_id
Первая форма — это ограничение на одиночный столбец users.email. Вторая — составное: оба столбца указаны потому, что уже существует именно их комбинация. Когда видите такое:
- Найдите строку, в которой лежит конфликтующее значение (
SELECT ... WHERE email = '...'). - Решите, что делать дальше: обновить эту строку, пропустить вставку или подставить другое значение.
- Если дубликаты ожидаемы и вы хотите их объединять, используйте
INSERT ... ON CONFLICT DO UPDATE.
Ошибка кричит громко, и это правильно — в большинстве случаев вы действительно хотите о ней знать. Молчаливые дубликаты обошлись бы дороже, чем упавшая вставка.
Дальше: удаление и изменение таблиц
Ограничение UNIQUE нельзя просто так навесить на существующую таблицу через ALTER TABLE. Именно из-за этого ограничения в SQLite есть особый ритуал для изменения схемы — переписывание таблицы. Об этом и поговорим на следующей странице, заодно разберём, как аккуратно удалять таблицы.
Часто задаваемые вопросы
Как добавить ограничение UNIQUE в SQLite?
Допишите UNIQUE к описанию столбца (email TEXT UNIQUE) или используйте конструкцию уровня таблицы — UNIQUE(col1, col2) — для уникальности по нескольким полям. Под капотом SQLite создаёт уникальный индекс и отклоняет любой INSERT или UPDATE, который привёл бы к дубликату.
Чем UNIQUE отличается от PRIMARY KEY в SQLite?
PRIMARY KEY в таблице может быть только один, а ограничений UNIQUE — сколько угодно. Кроме того, первичный ключ автоматически подразумевает NOT NULL (в strict-таблицах и для INTEGER PRIMARY KEY), тогда как столбец с UNIQUE спокойно хранит несколько NULL. Иначе говоря: PRIMARY KEY — это идентификатор строки, а UNIQUE — для остальных полей, которые не должны повторяться.
Почему SQLite разрешает несколько NULL в столбце с UNIQUE?
Дело в том, что в SQL NULL означает «неизвестно», а два неизвестных значения не считаются равными между собой. Поэтому в столбец с UNIQUE можно вставить сколько угодно строк с NULL — уникальными должны быть только конкретные значения. Если хочется ограничиться максимум одним NULL, добавьте NOT NULL или сделайте частичный уникальный индекс.
Как исправить ошибку «UNIQUE constraint failed»?
Эта ошибка означает, что INSERT или UPDATE пытается записать дубликат в столбец с UNIQUE (или PRIMARY KEY). Варианты: поменять вставляемое значение, удалить уже существующую строку или применить INSERT ... ON CONFLICT (upsert) — так вы сами скажете SQLite, что делать при конфликте.