SQLALTER TABLEDDLtutorial

ALTER TABLE в SQL: как безопасно менять структуру таблицы

ALTER TABLE — это команда «поменять структуру существующей таблицы». Простыми словами: добавить колонку, удалить, переименовать, сменить тип, добавить и снять constraint. Плюс главная боль ALTER на проде — длинные блокировки таблицы и паттерн «add → backfill → drop» для безопасных миграций.

15 мин чтенияСправочникSQL · ALTER TABLE · DDL · tutorial

ALTER TABLE — это команда SQL, которая меняет структуру уже существующей таблицы.

Не строки внутри таблицы, а именно её устройство:

  • добавить новую колонку;
  • удалить старую колонку;
  • переименовать колонку или таблицу;
  • поменять тип данных;
  • добавить или убрать ограничение;
  • сделать колонку обязательной;
  • добавить правило, что цена не может быть отрицательной.

Если CREATE TABLE — это момент, когда мы впервые проектируем таблицу, то ALTER TABLE — это ремонт и перепланировка уже работающего дома.

Дом уже построен. В нём живут люди. По нему ходят запросы. Приложение читает и пишет данные. И вот в этот момент мы говорим: «А давайте добавим ещё одну комнату».

Именно поэтому ALTER TABLE нужно понимать не только как синтаксис, но и как инструмент аккуратной работы с живой базой данных.

Чем ALTER TABLE отличается от UPDATE

Новички иногда путают ALTER TABLE и UPDATE.

Разница простая.

UPDATE меняет данные внутри таблицы:

UPDATE users
SET status = 'active'
WHERE id = 10;

То есть строка уже была, колонка уже была, мы просто изменили значение.

А ALTER TABLE меняет саму таблицу:

ALTER TABLE users
ADD COLUMN phone VARCHAR(20);

Здесь мы не меняем конкретного пользователя. Мы добавляем новую колонку phone для всей таблицы users.

Можно представить таблицу как анкету.

CREATE TABLE — мы придумали анкету: имя, email, дата регистрации.

INSERT — кто-то заполнил анкету.

UPDATE — человек поменял email.

ALTER TABLE — мы изменили сам бланк анкеты и добавили новое поле: телефон.

Зачем нужен ALTER TABLE

Схема базы почти никогда не остаётся такой, какой её придумали в первый день.

Сегодня у пользователя есть только email. Завтра бизнес просит добавить телефон.

Сегодня у заказа есть сумма. Завтра нужно хранить валюту.

Сегодня цена может быть любой. Завтра нужно запретить отрицательные значения.

Сегодня колонка называется phone. Завтра команда решила, что понятнее будет phone_number.

Такие изменения и делают через ALTER TABLE.

Например:

ALTER TABLE users
ADD COLUMN phone VARCHAR(20);

Или:

ALTER TABLE products
ADD CONSTRAINT products_price_positive CHECK (price > 0);

ALTER TABLE — это главный инструмент эволюции схемы. Он помогает базе расти вместе с продуктом.

Добавить колонку через ADD COLUMN

Самый простой сценарий — добавить новую колонку.

ALTER TABLE users
ADD COLUMN phone VARCHAR(20);

После этого в таблице users появится новая колонка phone.

Для новых пользователей в неё можно будет записывать телефон. А у старых пользователей значение будет NULL, если мы не указали другое поведение.

Например, было так:

id email
1 anna@example.com
2 boris@example.com

После добавления колонки таблица логически станет такой:

id email phone
1 anna@example.com NULL
2 boris@example.com NULL

Это нормально: раньше телефона в таблице не было, значит у старых строк его значение неизвестно.

Добавить колонку со значением по умолчанию

Иногда хочется, чтобы у новой колонки сразу было значение по умолчанию.

Например, добавим пользователям тариф:

ALTER TABLE users
ADD COLUMN tier VARCHAR(20) NOT NULL DEFAULT 'free';

Здесь мы говорим:

  • добавь колонку tier;
  • тип данных — VARCHAR(20);
  • значение не может быть NULL;
  • если значение не передали, ставь 'free'.

В результате старые и новые пользователи смогут получить значение free.

На маленьких таблицах такие операции обычно проходят незаметно. Но на больших таблицах нужно быть внимательнее.

В PostgreSQL 11+ добавление колонки с константным значением по умолчанию обычно выполняется быстро: база не переписывает каждую строку сразу. Но если значение по умолчанию вычисляется отдельно для строк, например через функцию, операция может стать гораздо тяжелее.

Пример потенциально тяжёлой операции:

ALTER TABLE users
ADD COLUMN created_token UUID DEFAULT gen_random_uuid();

Здесь значение не одно и то же для всех строк. Его нужно вычислять. На большой таблице такая миграция может оказаться дорогой.

Безопасный способ добавить обязательную колонку

Допустим, мы хотим добавить колонку signup_source и сделать её обязательной.

Плохая идея — сразу пытаться добавить её как NOT NULL, если в таблице уже есть данные и непонятно, чем заполнить старые строки.

Надёжнее идти по шагам.

Шаг первый: добавляем колонку как необязательную.

ALTER TABLE users
ADD COLUMN signup_source VARCHAR(50);

Теперь колонка есть, но в старых строках там NULL.

Шаг второй: заполняем старые данные.

UPDATE users
SET signup_source = 'web'
WHERE signup_source IS NULL
  AND id BETWEEN 1 AND 100000;

На большой таблице такой UPDATE лучше делать не одной огромной командой, а небольшими партиями. Например, сначала пользователи с id от 1 до 100000, потом от 100001 до 200000 и так дальше.

Зачем так осторожно?

Потому что большой UPDATE может долго держать блокировки, раздувать журнал изменений и мешать обычной работе приложения.

Шаг третий: когда все строки заполнены, делаем колонку обязательной.

ALTER TABLE users
ALTER COLUMN signup_source SET NOT NULL;

Теперь база будет запрещать строки без signup_source.

Такой подход часто называют: добавить, заполнить, включить ограничение.

Он длиннее, зато безопаснее для живого проекта.

Удалить колонку через DROP COLUMN

Если колонка больше не нужна, её можно удалить.

ALTER TABLE users
DROP COLUMN phone;

После этого колонка phone исчезнет из таблицы.

Но здесь важно не торопиться.

Удаление колонки — опасная операция не потому, что её сложно написать. Она как раз пишется очень просто. Опасность в том, что данные пропадают из обычного доступа, а код приложения, отчёты или интеграции могут всё ещё ожидать эту колонку.

Например, где-то в приложении остался запрос:

SELECT phone
FROM users;

После удаления колонки он начнёт падать.

Поэтому на проде колонку обычно удаляют не сразу.

Безопасный порядок такой:

  1. Сначала перестать использовать колонку в коде.
  2. Выложить новую версию приложения.
  3. Подождать, чтобы убедиться, что откат на старую версию уже не нужен.
  4. Проверить отчёты, фоновые задачи и интеграции.
  5. Только потом удалить колонку.

В PostgreSQL DROP COLUMN обычно быстро помечает колонку как удалённую. Но это не значит, что к операции можно относиться легкомысленно: с точки зрения продукта данные уже потеряны.

Переименовать колонку через RENAME COLUMN

Колонку можно переименовать.

ALTER TABLE users
RENAME COLUMN phone TO phone_number;

Сама операция обычно быстрая. База меняет метаданные: раньше колонка называлась phone, теперь называется phone_number.

Но для приложения это может быть резкое изменение.

Если код всё ещё пишет так:

SELECT phone
FROM users;

после переименования он получит ошибку: такой колонки больше нет.

Поэтому переименование на проде часто делают не одной командой, а через более мягкий переход.

Например:

  1. Добавить новую колонку phone_number.
  2. Начать записывать данные и в старую, и в новую колонку.
  3. Перенести старые данные.
  4. Переключить чтение приложения на новую колонку.
  5. Убедиться, что старая колонка больше не нужна.
  6. Удалить старую колонку phone.

Это кажется сложнее, чем обычный RENAME, зато позволяет не ломать приложение в момент миграции.

Переименовать таблицу через RENAME TO

Иногда нужно переименовать всю таблицу.

ALTER TABLE clients
RENAME TO customers;

После этого таблица clients будет называться customers.

С технической стороны операция простая. Но с точки зрения приложения это тоже breaking change: все запросы к старому имени таблицы перестанут работать.

Поэтому таблицы на проде переименовывают особенно аккуратно. Чем больше проект, тем выше шанс, что старое имя используется не только в основном приложении, но и в аналитике, админке, скриптах, ETL-процессах или ручных отчётах.

Поменять тип колонки через ALTER COLUMN TYPE

Иногда выбранный тип данных перестаёт подходить.

Например, сначала название товара хранили как VARCHAR(50), а потом поняли, что ограничение слишком маленькое.

ALTER TABLE products
ALTER COLUMN name TYPE TEXT;

PostgreSQL попробует сам привести старые значения к новому типу.

Если преобразование очевидное, всё пройдёт спокойно.

Но иногда базе нужно явно объяснить, как переводить данные.

Например, цена раньше хранилась текстом, а теперь мы хотим хранить её как число:

ALTER TABLE products
ALTER COLUMN price TYPE NUMERIC(12, 2)
USING price::NUMERIC;

Часть USING говорит базе: «Чтобы получить новое значение, возьми старое значение price и приведи его к типу NUMERIC».

Без USING база может не понять, как безопасно преобразовать данные.

Почему изменение типа может быть опасным

Смена типа колонки на большой таблице может оказаться тяжёлой операцией.

Иногда базе приходится переписать всю таблицу: прочитать старые строки, преобразовать значения и записать их в новом формате.

На маленькой таблице это незаметно.

На таблице с миллионами или миллиардами строк это может занять минуты или часы и создать серьёзные блокировки.

Поэтому для больших таблиц часто используют безопасный путь:

  1. Добавить новую колонку нужного типа.
  2. Заполнить её данными небольшими партиями.
  3. Переключить приложение на новую колонку.
  4. Проверить, что всё работает.
  5. Удалить старую колонку.

Например:

ALTER TABLE products
ADD COLUMN price_numeric NUMERIC(12, 2);

Потом заполняем партиями:

UPDATE products
SET price_numeric = price::NUMERIC
WHERE price_numeric IS NULL
  AND id BETWEEN 1 AND 10000;

Когда все данные перенесены и код переключён, старую колонку можно удалить.

Да, это дольше. Но для продакшена часто важнее не скорость написания миграции, а отсутствие простоя.

Добавить ограничение через ADD CONSTRAINT

Ограничение, или constraint, — это правило, которое база обязуется проверять.

Например, сумма заказа должна быть положительной:

ALTER TABLE orders
ADD CONSTRAINT orders_amount_positive CHECK (amount > 0);

Теперь база не даст вставить заказ с отрицательной суммой.

INSERT INTO orders (id, amount)
VALUES (1, -100);

Такой запрос должен закончиться ошибкой, потому что нарушает правило orders_amount_positive.

Ограничения полезны тем, что защищают данные на уровне базы. Даже если приложение ошиблось, база не пропустит явно плохое значение.

Добавить внешний ключ через FOREIGN KEY

Внешний ключ связывает одну таблицу с другой.

Например, заказ должен ссылаться на существующего клиента:

ALTER TABLE orders
ADD CONSTRAINT orders_customer_id_fkey
FOREIGN KEY (customer_id) REFERENCES customers(id);

Теперь нельзя будет создать заказ с customer_id, которого нет в таблице customers.

Это очень важная защита целостности данных.

Без внешнего ключа в базе легко появляются «сиротские» строки: заказ вроде есть, но клиента, к которому он относится, уже нет или никогда не было.

Удалить ограничение через DROP CONSTRAINT

Ограничение можно удалить.

ALTER TABLE orders
DROP CONSTRAINT orders_customer_id_fkey;

После этого база перестанет проверять соответствующее правило.

Но удалять ограничения нужно осторожно. Если убрать внешний ключ, приложение может начать записывать неконсистентные данные. Если убрать CHECK, в таблицу могут попасть значения, которые раньше были запрещены.

Особенно осторожно стоит относиться к CASCADE.

ALTER TABLE orders
DROP CONSTRAINT orders_customer_id_fkey CASCADE;

CASCADE означает: удалить не только это ограничение, но и зависимые объекты.

Иногда это удобно. Но на проде такая команда может снести больше, чем ты ожидал: связанные ограничения, представления или другие зависимости. Перед CASCADE нужно точно понимать, что именно будет удалено.

Сделать колонку обязательной через SET NOT NULL

Если колонка не должна быть пустой, можно добавить правило NOT NULL.

ALTER TABLE users
ALTER COLUMN email SET NOT NULL;

Теперь база не позволит вставить пользователя без email.

Но если в таблице уже есть строки с NULL в колонке email, команда не пройдёт. База честно скажет: сначала исправь существующие данные.

Правильный порядок такой:

Сначала найти проблемные строки:

SELECT id
FROM users
WHERE email IS NULL;

Потом заполнить или удалить их, в зависимости от бизнес-логики.

Например:

UPDATE users
SET email = 'unknown@example.com'
WHERE email IS NULL;

И только после этого включать NOT NULL:

ALTER TABLE users
ALTER COLUMN email SET NOT NULL;

В реальном проекте значение вроде 'unknown@example.com' не всегда хорошее решение. Иногда лучше запросить данные у пользователя, иногда удалить тестовые строки, иногда оставить колонку nullable. Главное — не включать NOT NULL вслепую.

Снять обязательность через DROP NOT NULL

Иногда правило становится мягче. Например, раньше email был обязателен, а теперь пользователи могут регистрироваться по телефону.

Тогда можно разрешить NULL:

ALTER TABLE users
ALTER COLUMN email DROP NOT NULL;

После этого база позволит создавать строки без email.

Такая операция обычно проще, чем SET NOT NULL, потому что базе не нужно проверять, что все старые строки уже заполнены.

NOT VALID: как добавлять ограничения на больших таблицах

Когда мы добавляем обычное ограничение, PostgreSQL должен проверить все существующие строки.

Например:

ALTER TABLE orders
ADD CONSTRAINT orders_amount_positive CHECK (amount > 0);

Если таблица большая, такая проверка может идти долго.

Для больших продакшен-таблиц в PostgreSQL часто используют двухшаговый подход через NOT VALID.

Сначала добавляем ограничение, но не проверяем старые строки:

ALTER TABLE orders
ADD CONSTRAINT orders_amount_positive CHECK (amount > 0) NOT VALID;

С этого момента новые или изменяемые строки уже должны соответствовать правилу. Но старые строки пока не проверены.

Потом отдельно запускаем проверку:

ALTER TABLE orders
VALIDATE CONSTRAINT orders_amount_positive;

Такой подход помогает уменьшить боль от долгих блокировок. Проверка всё равно нужна, но она выполняется отдельным шагом и обычно с более мягкими блокировками.

Этот же подход можно использовать и для внешних ключей:

ALTER TABLE orders
ADD CONSTRAINT orders_customer_id_fkey
FOREIGN KEY (customer_id) REFERENCES customers(id)
NOT VALID;

А затем:

ALTER TABLE orders
VALIDATE CONSTRAINT orders_customer_id_fkey;

Для новичка главный вывод такой: если таблица большая, не спеши добавлять тяжёлые ограничения одним движением. В PostgreSQL часто есть более аккуратный путь.

Паттерн add -> backfill -> switch -> drop

Для сложных миграций есть полезный рабочий паттерн:

add -> backfill -> switch -> drop

Расшифруем.

add — добавить новую структуру: колонку, таблицу или ограничение в мягком виде.

backfill — перенести или заполнить старые данные небольшими партиями.

switch — переключить приложение на новую структуру.

drop — удалить старую структуру, когда она точно больше не нужна.

Например, мы хотим добавить в таблицу заказов email клиента, чтобы не ходить каждый раз в таблицу customers.

Шаг первый: добавляем новую колонку.

ALTER TABLE orders
ADD COLUMN customer_email TEXT;

Шаг второй: заполняем её данными.

UPDATE orders
SET customer_email = (
  SELECT c.email
  FROM customers AS c
  WHERE c.id = orders.customer_id
)
WHERE customer_email IS NULL
  AND id BETWEEN 1 AND 10000;

На большой таблице повторяем такие обновления партиями.

Шаг третий: меняем приложение, чтобы оно записывало customer_email в новые заказы.

Шаг четвёртый: когда всё заполнено, можно включить NOT NULL, если это нужно.

ALTER TABLE orders
ALTER COLUMN customer_email SET NOT NULL;

Шаг пятый: если какая-то старая колонка больше не нужна, удаляем её отдельной миграцией.

В этом подходе нет героизма. Зато есть спокойная инженерная работа: маленькие безопасные шаги вместо одной рискованной операции.

Почему ALTER TABLE может быть страшен на проде

На учебной базе ALTER TABLE кажется безобидным.

Добавил колонку — готово.

Переименовал — готово.

Поменял тип — готово.

Но на продакшене таблица может быть огромной. По ней постоянно идут запросы. Пользователи открывают страницы. Сервисы пишут события. Отчёты читают данные. Фоновые задачи обновляют статусы.

И в этот момент миграция может взять блокировку.

Блокировка — это когда база временно ограничивает доступ к таблице, чтобы безопасно изменить её структуру.

В PostgreSQL многие формы ALTER TABLE берут строгую блокировку уровня ACCESS EXCLUSIVE. Это самый сильный режим блокировки для таблицы. Пока такая блокировка держится, другие запросы могут ждать.

Если операция длится миллисекунды, никто ничего не заметит.

Если операция длится минуты, приложение может начать получать таймауты, очередь запросов будет расти, а пользователи увидят ошибки.

Поэтому на проде важно думать не только «работает ли команда», но и «как долго она будет держать блокировку».

Какие операции обычно легче

Обычно более спокойными считаются операции, которые меняют только метаданные и не переписывают всю таблицу.

Например:

ALTER TABLE users
ADD COLUMN phone VARCHAR(20);

Добавление nullable-колонки обычно быстрое.

Переименование колонки тоже обычно быстрое:

ALTER TABLE users
RENAME COLUMN phone TO phone_number;

Удаление колонки в PostgreSQL обычно тоже быстро помечает колонку как удалённую:

ALTER TABLE users
DROP COLUMN phone;

Добавление ограничения через NOT VALID тоже часто безопаснее, чем немедленная полная проверка:

ALTER TABLE orders
ADD CONSTRAINT orders_amount_positive CHECK (amount > 0) NOT VALID;

Но «обычно быстрее» не значит «можно не проверять». На проде миграции всё равно нужно тестировать.

Какие операции могут быть тяжёлыми

Некоторые изменения могут потребовать долгой работы с данными.

Например, смена типа:

ALTER TABLE products
ALTER COLUMN price TYPE NUMERIC(12, 2)
USING price::NUMERIC;

Добавление колонки с вычисляемым значением по умолчанию:

ALTER TABLE users
ADD COLUMN token UUID DEFAULT gen_random_uuid();

Добавление ограничения без NOT VALID на большую таблицу:

ALTER TABLE orders
ADD CONSTRAINT orders_amount_positive CHECK (amount > 0);

Создание индекса обычной командой на большой таблице:

CREATE INDEX orders_customer_id_idx
ON orders (customer_id);

Индекс — это не совсем ALTER TABLE, но в жизни миграций он стоит рядом. Обычный CREATE INDEX может мешать записям. В PostgreSQL для больших продакшен-таблиц часто используют вариант:

CREATE INDEX CONCURRENTLY orders_customer_id_idx
ON orders (customer_id);

Он создаёт индекс дольше, зато позволяет таблице продолжать принимать чтение и запись. Важно помнить: CREATE INDEX CONCURRENTLY в PostgreSQL нельзя выполнять внутри обычной транзакции миграции.

Почему нельзя смешивать долгий UPDATE и ALTER в одной транзакции

Опасный вариант:

BEGIN;

ALTER TABLE users
ADD COLUMN signup_source VARCHAR(50);

UPDATE users
SET signup_source = 'web'
WHERE signup_source IS NULL;

COMMIT;

На вид удобно: всё в одной транзакции.

Но если UPDATE идёт долго, блокировки, взятые внутри транзакции, могут держаться до COMMIT. В итоге короткий ALTER превращается в часть длинной операции.

Лучше разделять:

  1. Короткая миграция структуры.
  2. Отдельный фоновый backfill данных.
  3. Отдельная миграция для включения строгих ограничений.

Например:

ALTER TABLE users
ADD COLUMN signup_source VARCHAR(50);

Потом отдельно:

UPDATE users
SET signup_source = 'web'
WHERE signup_source IS NULL
  AND id BETWEEN 1 AND 100000;

И только после полного заполнения:

ALTER TABLE users
ALTER COLUMN signup_source SET NOT NULL;

Так база меньше страдает, а команда лучше контролирует процесс.

Как думать перед ALTER TABLE

Перед изменением таблицы полезно задать себе несколько вопросов.

Первый: таблица маленькая или большая?

На таблице из тысячи строк многие операции пройдут мгновенно. На таблице из сотен миллионов строк та же команда может стать проблемой.

Второй: операция меняет только схему или переписывает данные?

Добавить nullable-колонку — одно.

Поменять тип колонки с преобразованием всех значений — совсем другое.

Третий: приложение уже готово к новой структуре?

Если ты переименуешь колонку, старый код может упасть.

Если добавишь NOT NULL, старый код может продолжить отправлять пустые значения и начнёт получать ошибки.

Четвёртый: есть ли план отката?

Если миграция удаляет колонку, откат может быть сложным. Поэтому удаление часто откладывают до тех пор, пока команда не убедится, что старая структура больше не нужна.

Пятый: проверяли ли миграцию на staging?

На проде не стоит впервые узнавать, что команда держит блокировку 40 минут.

Пример: добавляем телефон пользователю безопасно

Допустим, приложение раньше знало только email пользователя, а теперь нужно добавить телефон.

Начинаем с простой nullable-колонки:

ALTER TABLE users
ADD COLUMN phone VARCHAR(20);

После этого новая версия приложения может начать записывать телефон для новых пользователей.

Старые пользователи пока останутся с NULL.

Если потом бизнес решит, что телефон обязателен, нельзя просто включать NOT NULL вслепую. Сначала нужно проверить старые строки:

SELECT COUNT(*) AS users_without_phone
FROM users
WHERE phone IS NULL;

Если таких пользователей много, нужно решить, что с ними делать. Запросить телефон? Оставить поле необязательным? Заполнить из другого источника?

Только когда данных без телефона не осталось, можно включить ограничение:

ALTER TABLE users
ALTER COLUMN phone SET NOT NULL;

Пример: меняем текстовую цену на числовую

Допустим, в таблице products цена раньше хранилась как текст.

id name price
1 Keyboard 99.90
2 Mouse 49.50

Технически это плохо: цена должна быть числом, чтобы её можно было правильно сравнивать, складывать и округлять.

На маленькой таблице можно сделать так:

ALTER TABLE products
ALTER COLUMN price TYPE NUMERIC(12, 2)
USING price::NUMERIC;

Но на большой таблице безопаснее добавить новую колонку:

ALTER TABLE products
ADD COLUMN price_amount NUMERIC(12, 2);

Потом заполнить её:

UPDATE products
SET price_amount = price::NUMERIC
WHERE price_amount IS NULL
  AND id BETWEEN 1 AND 10000;

Потом переключить приложение на price_amount.

И уже потом, когда всё проверено, удалить старую колонку:

ALTER TABLE products
DROP COLUMN price;

При необходимости новую колонку можно переименовать:

ALTER TABLE products
RENAME COLUMN price_amount TO price;

Такой путь длиннее, но он позволяет контролировать каждый шаг.

Частые ошибки новичков

Путать изменение структуры и изменение данных

Если нужно поменять значение в строках, нужен UPDATE.

UPDATE users
SET status = 'active'
WHERE id = 10;

Если нужно добавить, удалить или изменить колонку, нужен ALTER TABLE.

ALTER TABLE users
ADD COLUMN status VARCHAR(20);

Добавлять NOT NULL без подготовки

Плохая идея:

ALTER TABLE users
ADD COLUMN source VARCHAR(50) NOT NULL;

Если в таблице уже есть строки и у новой колонки нет значения по умолчанию, база не сможет честно заполнить старые данные.

Лучше так:

ALTER TABLE users
ADD COLUMN source VARCHAR(50);

Потом заполнить:

UPDATE users
SET source = 'web'
WHERE source IS NULL;

И только потом:

ALTER TABLE users
ALTER COLUMN source SET NOT NULL;

Менять тип большой колонки одной командой

Команда может выглядеть простой:

ALTER TABLE products
ALTER COLUMN price TYPE NUMERIC(12, 2)
USING price::NUMERIC;

Но на большой таблице она может оказаться тяжёлой.

Перед такой миграцией нужно проверить объём данных, время выполнения и блокировки. Часто лучше использовать новую колонку и backfill.

Забывать USING при сложном преобразовании типа

Если база не понимает, как превратить старое значение в новый тип, нужно явно написать выражение.

ALTER TABLE products
ALTER COLUMN price TYPE NUMERIC(12, 2)
USING price::NUMERIC;

Без USING миграция может упасть.

Удалять колонку сразу после отказа от неё в коде

Сегодня ты выложил новую версию приложения без старой колонки. Через час нашёл баг и решил откатиться. А колонки уже нет.

Поэтому удаление старых колонок часто откладывают. Сначала код перестаёт их использовать, потом проходит время, и только после этого колонку удаляют.

Использовать CASCADE без понимания последствий

ALTER TABLE orders
DROP CONSTRAINT orders_customer_id_fkey CASCADE;

CASCADE может удалить зависимые объекты. Перед такой командой нужно проверить, что именно зависит от ограничения.

Создавать индекс без CONCURRENTLY на большой таблице

Обычный индекс:

CREATE INDEX orders_customer_id_idx
ON orders (customer_id);

На большой таблице может мешать записи.

В PostgreSQL для продакшена часто безопаснее:

CREATE INDEX CONCURRENTLY orders_customer_id_idx
ON orders (customer_id);

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

Держать ALTER TABLE в одной транзакции с долгим обновлением

Плохо:

BEGIN;

ALTER TABLE orders
ADD COLUMN processed_at TIMESTAMP;

UPDATE orders
SET processed_at = created_at
WHERE processed_at IS NULL;

COMMIT;

Если обновление идёт долго, транзакция тоже живёт долго. А вместе с ней могут дольше держаться блокировки.

Лучше разделять изменение схемы и массовую обработку данных.

Короткая шпаргалка по ALTER TABLE

Добавить колонку:

ALTER TABLE users
ADD COLUMN phone VARCHAR(20);

Удалить колонку:

ALTER TABLE users
DROP COLUMN phone;

Переименовать колонку:

ALTER TABLE users
RENAME COLUMN phone TO phone_number;

Переименовать таблицу:

ALTER TABLE clients
RENAME TO customers;

Поменять тип колонки:

ALTER TABLE products
ALTER COLUMN price TYPE NUMERIC(12, 2)
USING price::NUMERIC;

Сделать колонку обязательной:

ALTER TABLE users
ALTER COLUMN email SET NOT NULL;

Снова разрешить NULL:

ALTER TABLE users
ALTER COLUMN email DROP NOT NULL;

Добавить проверку:

ALTER TABLE products
ADD CONSTRAINT products_price_positive CHECK (price > 0);

Добавить проверку без немедленной проверки старых строк в PostgreSQL:

ALTER TABLE products
ADD CONSTRAINT products_price_positive CHECK (price > 0) NOT VALID;

Проверить ограничение позже:

ALTER TABLE products
VALIDATE CONSTRAINT products_price_positive;

Удалить ограничение:

ALTER TABLE products
DROP CONSTRAINT products_price_positive;

Главное из статьи

ALTER TABLE меняет структуру существующей таблицы: добавляет и удаляет колонки, меняет типы, переименовывает объекты, добавляет и снимает ограничения.

Это не команда для изменения значений в строках. Для данных используется UPDATE, для структуры — ALTER TABLE.

На маленьких учебных таблицах многие изменения кажутся мгновенными. На больших продакшен-таблицах те же команды могут держать блокировки, переписывать данные и мешать приложению.

Самые спокойные операции обычно те, которые меняют только метаданные: добавить nullable-колонку, переименовать колонку, удалить колонку, добавить ограничение через NOT VALID.

Более опасные операции — смена типа большой колонки, добавление вычисляемого значения по умолчанию, добавление ограничения с полной проверкой старых строк, создание индекса обычной командой на большой таблице.

Для серьёзных миграций лучше использовать подход add -> backfill -> switch -> drop: сначала добавить новую структуру, потом заполнить данные партиями, затем переключить приложение и только после этого удалить старое.

Главная мысль: ALTER TABLE — не просто синтаксис. Это инструмент изменения живой системы. Хороший разработчик не только знает, как написать команду, но и понимает, что произойдёт с данными, приложением и пользователями в момент её выполнения.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр