Схемы меняются. SQLite позволяет их менять — почти всегда
Рано или поздно таблицу захочется переименовать, добавить в неё колонку, выкинуть лишнее или вообще переделать структуру с нуля. Для большинства таких случаев в SQLite есть DROP TABLE и ALTER TABLE, а на остальные — задокументированный обходной приём.
Загвоздка вот в чём: ALTER TABLE в SQLite заметно урезаннее, чем в Postgres или MySQL. Главный навык — понимать, что он умеет, чего не умеет, и как делать пересборку таблицы там, где напрямую не получится.
DROP TABLE — удалить таблицу sqlite вместе со всем содержимым
Команда DROP TABLE удаляет таблицу целиком: строки, индексы и все триггеры, которые на ней висят. Откатить это нельзя:
Таблицы больше нет. Если сейчас попробовать к ней обратиться, получим ошибку no such table: scratch.
Когда вы не уверены, существует ли таблица (типичная ситуация в скриптах инициализации), добавьте IF EXISTS — тогда команда просто молча отработает, если таблицы нет:
Без IF EXISTS второй DROP свалится с ошибкой. С ним оба запроса отработают спокойно.
Внешние ключи могут заблокировать DROP
Если включена проверка внешних ключей (PRAGMA foreign_keys = ON;) и какая-то другая таблица ссылается на ту, которую вы пытаетесь удалить, то drop table завершится ошибкой:
sqlite> PRAGMA foreign_keys = ON;
sqlite> DROP TABLE users;
Runtime error: FOREIGN KEY constraint failed
Вариантов несколько: либо сначала удалить ссылающуюся таблицу, либо убрать строки, которые на неё ссылаются, либо при создании внешнего ключа сразу прописать ON DELETE CASCADE. SQLite не станет молча ломать ссылочную целостность за вас.
ALTER TABLE в SQLite: четыре доступные операции
В SQLite команда ALTER TABLE умеет ровно четыре вещи:
Каждая из этих команд — это одно SQL-выражение. Первые две практически бесплатны: меняется только схема. ADD COLUMN тоже отрабатывает быстро — SQLite не переписывает таблицу, а просто фиксирует определение новой колонки в схеме. А вот DROP COLUMN уже тяжелее: SQLite приходится пройтись по всем строкам и физически удалить данные этой колонки.
ADD COLUMN со значением по умолчанию
Когда вы добавляете новую колонку в существующую таблицу, во всех строках там по умолчанию будет NULL — если, конечно, вы сами не зададите значение по умолчанию:
Обе существующие строки получат значение 'active'. Значение по умолчанию обязано быть константой — SQLite не позволит использовать CURRENT_TIMESTAMP или любое другое неконстантное выражение в качестве default при ADD COLUMN, потому что движку нужно одно значение, которое можно применить ко всем существующим строкам без вычислений для каждой.
Если нужен NOT NULL без значения по умолчанию, придётся сначала добавить колонку как nullable, заполнить её через UPDATE, а затем пересобрать таблицу, чтобы навесить ограничение. И тут мы плавно подходим к ограничениям.
Чего ALTER TABLE сделать не может
Вещи, которые работают в Postgres или MySQL, но не в SQLite:
- Изменить тип колонки (
ALTER COLUMN ... TYPE ...). - Изменить значение по умолчанию у существующей колонки.
- Добавить или убрать
NOT NULL,CHECK,UNIQUEилиPRIMARY KEYу существующей колонки. - Добавить внешний ключ к существующей колонке.
- Поменять порядок колонок.
Любая из этих попыток приведёт к синтаксической ошибке. В SQLite вообще нет конструкции ALTER COLUMN. Официальный ответ для всех таких случаев один: пересобрать таблицу.
Паттерн пересборки таблицы
Когда ALTER TABLE не справляется, схема меняется так: создаём новую таблицу с нужной структурой, переливаем в неё данные, удаляем старую, а новую переименовываем на её место. Всё это оборачиваем в транзакцию — чтобы либо прошло целиком, либо не прошло вовсе:
Теперь users.age — это целое число с CHECK-ограничением, а email помечен как NOT NULL. Данные при этом никуда не делись — они переехали вместе со схемой.
Несколько моментов, о которых стоит помнить, когда будете делать это на боевой базе:
- Отключайте внешние ключи на время операции. Если на вашу таблицу ссылаются другие, выполните
PRAGMA foreign_keys = OFF;перед транзакцией иPRAGMA foreign_keys = ON;после. ИначеDROP TABLEупадёт с ошибкой. Учтите: эту прагму нельзя менять внутри транзакции, поэтому ставьте её снаружи. - Пересоздавайте индексы и триггеры. Когда вы удаляете старую таблицу, её индексы и триггеры исчезают вместе с ней. После переименования навесьте их заново на новую таблицу.
- Проверьте представления (VIEW). Представления, которые ссылаются на таблицу, всё ещё хранят старое имя в своём SQL. Те из них, что зависят от изменённых колонок, придётся пересобрать.
Такой паттерн пересборки таблицы получается громоздким, зато надёжным. Именно так миграционные инструменты вроде Alembic и Rails работают «под капотом», когда им нужно изменить схему в SQLite.
Как удалить несколько таблиц SQLite
Отдельной команды для удаления нескольких таблиц сразу в SQLite нет — DROP TABLE придётся выполнить для каждой. Если хочется сгруппировать их в одну операцию, оберните всё в транзакцию:
Если завернуть всё это в транзакцию, то либо все три DROP пройдут, либо ни один — удобно, когда сносишь связанные таблицы и боишься застрять на полпути из-за внешних ключей.
Что стоит запомнить
DROP TABLEсносит таблицу вместе с её индексами и триггерами. Для идемпотентных скриптов добавляйтеIF EXISTS.ALTER TABLEв SQLite умеет ровно четыре вещи: переименовать таблицу, переименовать колонку, добавить колонку, удалить колонку.- Всё остальное — смена типа, новые ограничения, внешние ключи на уже существующих колонках — делается через пересборку таблицы внутри транзакции.
- При пересборке не забывайте про внешние ключи, индексы, триггеры и представления. За данными они сами не переедут.
Дальше: наполняем таблицы данными
Целую главу мы возились с таблицами и ограничениями, которые задают им форму. Пора положить туда что-нибудь полезное — следующая глава начинается с INSERT: вставка нескольких строк за раз, значения по умолчанию и то, как SQLite ведёт себя, когда вставка конфликтует с вашими ограничениями.
Часто задаваемые вопросы
Как удалить таблицу в SQLite?
Пишем DROP TABLE имя_таблицы;. Чтобы запрос не падал, если таблицы уже нет, добавьте IF EXISTS: DROP TABLE IF EXISTS users;. Вместе с таблицей улетают её индексы и триггеры. И учтите: если включены внешние ключи, удаление не пройдёт, пока на таблицу ссылаются другие.
Что вообще умеет ALTER TABLE в SQLite?
Всего четыре операции: RENAME TO — переименовать таблицу, RENAME COLUMN ... TO ... — переименовать колонку, ADD COLUMN — добавить новый столбец и DROP COLUMN — удалить столбец (доступно с SQLite 3.35). И всё. Поменять тип колонки, изменить значение по умолчанию или навесить ограничение на уже существующий столбец нельзя.
Как тогда поменять тип или ограничения колонки в SQLite?
Напрямую — никак, такой возможности просто нет. Стандартный приём — пересборка таблицы: создаём новую таблицу с нужной схемой, переливаем данные через INSERT INTO new SELECT ... FROM old, затем DROP TABLE old и ALTER TABLE new RENAME TO old. Всю эту последовательность оборачивайте в транзакцию, чтобы операция была атомарной.