Menu

Первичный ключ в SQLite: INTEGER, AUTOINCREMENT и составной

Разбираемся, как устроены первичные ключи в SQLite: особый INTEGER PRIMARY KEY, составные ключи, AUTOINCREMENT и подводные камни, на которых спотыкаются новички.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Что на самом деле делает первичный ключ

Первичный ключ (primary key) в SQLite — это столбец или набор столбцов, по которому однозначно определяется каждая строка таблицы. Двух строк с одинаковым значением первичного ключа быть не может — SQLite сам следит за этим и заодно использует ключ для быстрого поиска нужных записей.

Самый простой вариант — указать ключ прямо в описании столбца:

Вы не указали id, а SQLite сам его проставил. Это не магия — это особый случай для INTEGER PRIMARY KEY, и его стоит понять до того, как вы напишете что-либо ещё.

Чем особенный INTEGER PRIMARY KEY в SQLite

В большинстве СУБД первичный ключ — это просто уникальный индекс. А вот в SQLite у каждой обычной таблицы уже есть скрытый 64-битный целочисленный rowid, по которому строки идентифицируются внутри движка. Когда вы объявляете колонку именно как INTEGER PRIMARY KEY, эта колонка становится тем самым rowid. Никакого дополнительного индекса, никакой лишней памяти — ваш id и физическое положение строки на диске — это одно и то же.

id и rowid — это один и тот же столбец под двумя именами. Поиск по id ведёт напрямую к строке: никакого второго дерева обходить не нужно. Отсюда и стандартный совет для SQLite: если нужен числовой первичный ключ, пишите INTEGER PRIMARY KEY ровно в таком виде. Ни INT, ни BIGINT, ни INTEGER NOT NULL PRIMARY KEY (ладно, последний вариант сработает, но тип обязательно должен быть INTEGER).

Другие типы тоже работают — просто к ним создаётся отдельный уникальный индекс. Ничего страшного, но получается уже не так компактно.

AUTOINCREMENT в SQLite чаще всего не нужен

По привычке из других СУБД многие пишут id INTEGER PRIMARY KEY AUTOINCREMENT. В SQLite ключевое слово AUTOINCREMENT делает не совсем то, что подсказывает название, и в большинстве случаев без него можно спокойно обойтись.

Без AUTOINCREMENT столбец INTEGER PRIMARY KEY сам подставляет значение на единицу больше максимального существующего rowid. Если удалить последнюю строку, следующая вставка может переиспользовать этот id.

С AUTOINCREMENT SQLite хранит максимальный когда-либо использованный id в служебной таблице sqlite_sequence и никогда не выдаёт повторов — даже после удаления строк.

В таблице plain идентификатор 3 переиспользовался, а вот таблица с AUTOINCREMENT сразу прыгнула на 4. Если у вас нет реальной причины запрещать повторное использование id — например, аудит или внешние ссылки, которые остаются после удаления, — забудьте про AUTOINCREMENT. Он стоит вам лишней записи на каждый INSERT плюс отдельную служебную таблицу для учёта.

Составной первичный ключ в SQLite

Иногда одного столбца просто недостаточно. Возьмём связующую таблицу, где пользователи сопоставляются с ролями: уникальной её записи здесь будет пара (user_id, role_id). В таких случаях ключ объявляется на уровне таблицы:

Пара должна быть уникальной в пределах всей таблицы — (1, 10) встретится максимум один раз. А вот каждый из столбцов по отдельности может повторяться сколько угодно. В этом и весь смысл: у одного пользователя может быть много ролей, у одной роли — много пользователей, но конкретная связка «пользователь — роль» существует ровно в одном экземпляре.

Составной первичный ключ в SQLite создаёт отдельный индекс по перечисленным столбцам. Он не становится rowid — такой привилегии удостаивается только одиночный INTEGER PRIMARY KEY.

Грабли с NULL в первичном ключе

Вот деталь, которая вводит в ступор тех, кто пришёл из PostgreSQL или MySQL: в обычной таблице SQLite столбец первичного ключа (если это не INTEGER PRIMARY KEY) спокойно может содержать NULL. Это давний баг, который авторы SQLite оставили ради обратной совместимости.

Две строки со значением NULL спокойно проскочили через первичный ключ. Лечится это просто: на каждый столбец первичного ключа, который не является INTEGER, нужно явно навесить NOT NULL:

Или используйте таблицу STRICT — там этот баг с NULL в PK уже исправлен. Привычка дописывать NOT NULL к каждому столбцу первичного ключа — дешёвая страховка, которая никогда не помешает.

Primary key vs UNIQUE в SQLite

И то, и другое защищает от дубликатов. Но разница есть:

  • У таблицы может быть только один первичный ключ, а вот UNIQUE-ограничений — сколько угодно.
  • Первичный ключ — это «главный» идентификатор таблицы: внешние ключи по умолчанию ссылаются именно на него.
  • INTEGER PRIMARY KEY становится rowid, а просто UNIQUE-столбец с целыми числами — нет.
  • В UNIQUE-столбцах спокойно уживается сколько угодно NULL (каждый NULL считается уникальным).

id — это идентификатор строки. Поля email и username тоже уникальны, но это бизнес-атрибуты: они могут поменяться, а вот id — нет.

Как добавить первичный ключ позже (спойлер: лучше не надо)

Возможности ALTER TABLE в SQLite ограничены. Команды вроде ALTER TABLE ... ADD PRIMARY KEY попросту не существует. Если вы забыли указать первичный ключ, а в таблице уже есть данные, единственный путь — пересоздать её:

Это стандартный приём миграции в SQLite. В реальном коде оборачивайте всё в транзакцию и временно отключайте внешние ключи, если на эту таблицу ссылаются другие. Главный урок: правильный первичный ключ нужно задавать сразу на этапе CREATE TABLE.

Короткий чек-лист

Когда создаёте новую таблицу, задайте себе несколько вопросов:

  • Есть ли у строки естественный уникальный идентификатор? Если это одно целое число — используйте INTEGER PRIMARY KEY.
  • А может, идентичность строки — это сочетание нескольких колонок (таблица-связка)? Тогда нужен составной первичный ключ на уровне таблицы: PRIMARY KEY (col_a, col_b).
  • Ключ текстовый или другого нецелочисленного типа? Обязательно добавьте NOT NULL явно.
  • Действительно ли вам нужен AUTOINCREMENT? Скорее всего, нет.
  • Таблица небольшая, в основном на чтение, и первичный ключ — не целое число? Присмотритесь к WITHOUT ROWID (об этом — в материале про rowid).

Что дальше: rowid

INTEGER PRIMARY KEY мы уже мельком упомянули как «псевдоним для rowid». Но rowid — это фундамент любой обычной таблицы в SQLite, и его стоит разобрать отдельно. Этим и займёмся на следующей странице.

Часто задаваемые вопросы

Как объявить первичный ключ в SQLite?

Достаточно дописать PRIMARY KEY к нужной колонке в CREATE TABLE — например, id INTEGER PRIMARY KEY. Если ключ должен состоять из нескольких колонок, используйте ограничение на уровне таблицы: PRIMARY KEY (col_a, col_b). Главное условие — комбинация значений должна быть уникальной по всем строкам.

Чем INTEGER PRIMARY KEY отличается от других первичных ключей?

INTEGER PRIMARY KEY — особый случай: он становится псевдонимом встроенного rowid и хранится прямо в B-дереве таблицы, без отдельного индекса. Любой другой тип, как и составной ключ, потребует отдельного уникального индекса. Поэтому для обычного числового идентификатора INTEGER PRIMARY KEY работает быстрее и занимает меньше места.

Нужен ли AUTOINCREMENT для первичного ключа в SQLite?

В большинстве случаев — нет. INTEGER PRIMARY KEY и так автоматически назначит уникальный rowid, если вставить NULL. AUTOINCREMENT лишь гарантирует, что идентификаторы не будут переиспользоваться после удаления строк — но за это придётся платить служебной таблицей sqlite_sequence. Использовать его стоит, только если вам реально нужны строго возрастающие id.

Почему первичный ключ в SQLite допускает NULL?

Это исторический баг, который оставили ради совместимости: в обычных таблицах колонка первичного ключа любого типа, кроме INTEGER, может содержать NULL, если не указать NOT NULL явно. Исключение — INTEGER PRIMARY KEY: он NULL не примет никогда. Чтобы не наступить на эти грабли, всегда добавляйте NOT NULL к колонкам первичного ключа или используйте STRICT-таблицы, где правило соблюдается корректно.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ