Изменить столбец в небольшой базе данных может быть просто. В большой активно используемой таблице та же операция может потребовать блокировки, перестроения индексов или конкурировать за ресурсы с production-запросами. Эффект зависит от СУБД, её версии, типа изменения и конфигурации: не стоит считать, что операция будет мгновенной или не заблокирует доступ.
Миграции схемы больших таблиц отделяют структурные изменения от преобразования данных. Цель — сохранить совместимость версий приложения на протяжении перехода, измерять влияние и иметь возможность остановить процесс. Универсального рецепта нет: проверяйте возможности СУБД и фактическое поведение приложения.
Почему небольшое изменение может блокировать операции

Расширение столбца, добавление ограничения или изменение типа могут потребовать объёма работ, пропорционального размеру таблицы. В зависимости от СУБД операция может удерживать блокировки, создавать нагрузку на диск, влиять на реплики или ждать завершения открытых транзакций. Даже операция, считающаяся online, может ненадолго блокировать доступ или иметь ограничения.
Оцените размер и рост таблицы, нагрузку на чтение и запись, длительные транзакции, индексы и доступное пространство. Изучите документацию для конкретной версии и проведите испытания в репрезентативной среде. Установите эксплуатационные пороговые значения для задержки, блокировок, хранилища и отставания репликации.
Обычное обновление схемы изменяет структуры, например добавляет столбец. Миграция данных преобразует или копирует существующие значения, часто построчно. Эти действия могут входить в одно функциональное изменение, но сопряжены с разными рисками, поэтому их лучше выполнять и отслеживать отдельно.
Инвентаризация читателей, писателей и зависимостей
Выясните, кто читает и записывает данные: PHP-код, SQL-запросы, задачи очередей, плановые команды, импорты, отчёты и внешние сервисы. Проверьте, какие версии могут работать одновременно во время поэтапного развёртывания. Динамические запросы и потребители за пределами репозитория тоже имеют значение.
- Документируйте текущий и целевой форматы, включая NULL, значения по умолчанию и правила преобразования.
- Определите зависимые индексы, внешние ключи, ограничения и представления.
- Проверьте, какие компоненты частично обновляют поля и какие записывают эти данные.
- Определите, как обнаруживать и исправлять недопустимые значения.
Эта инвентаризация определяет порядок развёртывания. Старая версия может завершиться с ошибкой, если удалить столбец, к которому она всё ещё обращается. Если вы не можете определить всех потребителей, исходите из того, что старый код может оставаться активным дольше.
Разделение изменения на совместимые этапы
Распространённый подход — сначала расширить, затем сократить: добавить новую структуру, не удаляя старую, развернуть совместимый код, скопировать исторические данные и переключить чтение. Старую структуру удаляют только после проверки результата.
- Расширение: добавьте новый столбец или таблицу, не нарушая работу версий в production. Рассмотрите возможность создания индексов и ограничений отдельной операцией.
- Развёртывание совместимого кода: разверните компоненты, выполняющие чтение и допускающие наличие необработанных данных, а также компоненты, выполняющие запись и поддерживающие согласованность обоих представлений.
- Перенос исторических данных: выполните backfill пакетами, отслеживая ход выполнения.
- Переключение использования: преимущественно читайте из новой структуры и наблюдайте за ошибками, задержкой и расхождениями.
- Сокращение: сначала прекратите запись в старую структуру, а затем, в рамках последующего развёртывания, удалите её.
Развёртывание кода не обязывает сразу активировать функциональность. Убедитесь, что каждая версия работает со схемой на каждом этапе, в том числе если потребуется откатить код.
Контролируемый backfill из PHP
Не загружайте всю таблицу в память и не удерживайте одну глобальную транзакцию. Консольная PHP-команда позволяет контролировать размер пакета, записывать ход выполнения и останавливать процесс, не привязывая его к жизненному циклу HTTP-запроса. В этом примере с PDO используется оптимистическая проверка по версии. progressStore обозначает запись о прогрессе, сохранённую в той же базе данных и подтверждаемую в одной транзакции со строками пакета.
$limit = 200;
$maxAttempts = 5;
$cursor = (int) $progressStore->load('backfill');
// Начальная верхняя граница; сама по себе она не учитывает поздние вставки.
$upperId = (int) $pdo->query('SELECT MAX(id) FROM records')->fetchColumn();
while ($cursor < $upperId) {
$done = false;
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
$select = $pdo->prepare(
'SELECT id, old_value, version FROM records
WHERE id > :cursor AND id <= :upper_id
ORDER BY id LIMIT ' . (int) $limit
);
$select->execute([':cursor' => $cursor, ':upper_id' => $upperId]);
$rows = $select->fetchAll(PDO::FETCH_ASSOC);
if (!$rows) {
$cursor = $upperId;
$done = true;
break;
}
try {
$pdo->beginTransaction();
$update = $pdo->prepare(
'UPDATE records SET new_value = :value, version = version + 1
WHERE id = :id AND version = :version'
);
foreach ($rows as $row) {
$update->execute([
':value' => transform($row['old_value']),
':id' => $row['id'], ':version' => $row['version'],
]);
if ($update->rowCount() !== 1) {
throw new VersionConflict('Строка изменилась во время backfill');
}
}
$next = (int) end($rows)['id'];
$progressStore->saveWithinTransaction('backfill', $next);
$pdo->commit();
$cursor = $next;
$done = true;
break;
} catch (Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
$retryable = $e instanceof VersionConflict
|| ($e instanceof PDOException && isRetryableDatabaseError($e));
if (!$retryable || $attempt === $maxAttempts) throw $e;
usleep(min(100000 * (2 ** ($attempt - 1)), 2000000));
// Повторно прочитать пакет с тем же курсором; прогресс ещё не продвинут.
}
}
if (!$done) throw new RuntimeException('Пакет не обработан; курсор не сдвинут.');
}VersionConflict должно быть отдельным исключением, а не общим исключением, объединяющим ошибки преобразования. Так постоянные сбои в transform() остановят команду для диагностики. isRetryableDatabaseError() должна распознавать только временные ошибки, классифицированные для используемых СУБД и драйвера, например взаимные блокировки или превышение времени ожидания. При других ошибках базы данных процесс останавливается. Максимальное число попыток и ограниченный backoff не допускают бесконечных повторов; после исчерпания попыток команда завершается с ошибкой, не продвигая курсор. Проверьте, как rowCount() работает в используемом сочетании PDO и СУБД.
Оптимистическая защита требует, чтобы все писатели увеличивали version в той же транзакции, в которой обновляют данные. Это правило распространяется на PHP-код, очереди, импорты и внешние сервисы; его необходимо внедрить и проверить до начала backfill. Если какой-либо писатель ему не следует, сравнение может не обнаружить гонку: обновите его, направьте записи через общий механизм, временно отключите или используйте подходящие блокировки. Не считайте защиту надёжной, пока не проверили соблюдение этого правила каждым писателем.
Начальная верхняя граница сокращает объём работы, но не гарантирует включение вставок, которые будут подтверждены позднее. По завершении прохода выполните сверку, явно выявляющую необработанные строки и расхождения, не полагаясь на то, что их ID больше курсора. Исправьте и перепроверьте такие записи; повторяйте проход, пока не останется необработанных данных, используя проверяемый критерий и сохраняя совместимость со стороны писателей. Если надёжно обнаружить или сверить такие строки невозможно, рассмотрите отслеживание изменений или окно обслуживания. Не объявляйте перенос исторических данных завершённым только потому, что курсор достиг начальной границы.
Преобразование должно быть идемпотентным, а прогресс необходимо подтверждать атомарно со строками. Если хранилище курсора не участвует в одной транзакции с данными, предусмотрите безопасный повторно запускаемый процесс, который сможет повторно обработать уже подтверждённые строки. Добавьте возможность приостановки, ограничения на размер пакетов, структурированные логи и код завершения, отражающий ошибки; подбирайте размер по результатам измерений, а не предположений.
Предотвращение гонок при параллельной записи
Одной двойной записи недостаточно для предотвращения всех гонок. Backfill может прочитать старое значение; параллельная запись может обновить оба поля; после этого backfill может перезаписать новое поле устаревшим результатом. Условие по версии в примере не позволяет выполнить такое обновление, если параллельная запись увеличила версию; конфликт сохраняется, потому что курсор сдвигается только после подтверждения всего пакета.
В зависимости от гарантий СУБД и рабочей нагрузки можно также использовать условное обновление на основе исходного значения или подходящие блокировки. У каждого варианта свои издержки и семантика. Проверьте выбранную стратегию на конкретной СУБД при чередующихся записях, взаимных блокировках и превышениях времени ожидания; прежде чем утверждать, что перенос исторических данных завершён успешно, проверьте и затронутые строки, и результаты сверки.
Проверка перед удалением старой структуры
Завершение команды не доказывает корректность данных. Убедитесь, что необработанных строк не осталось, проверьте ограничения и сравните результаты с ожидаемым преобразованием. Проверьте значения NULL и граничные случаи, а также убедитесь, что чтение использует новое поле и не ухудшает функциональность или производительность.
Следите также за редко запускаемыми процессами, например отчётами или периодическими задачами. Сохраняйте старую структуру, пока от неё зависят читатели или писатели: её наличие служит мерой совместимости, а не доказательством завершения миграции.
Определение условий приостановки, отката и окна обслуживания

Определите условия для приостановки: ошибки или задержка выше согласованного порога, блокировки, чрезмерное отставание реплик, нехватка ресурсов или растущие расхождения. Установите, кто останавливает процесс и как возобновить его с последнего подтверждённого пакета. Отслеживайте длительность обработки пакетов, ошибки, CPU, диск и блокировки.
Откат кода не равнозначен откату данных. После перехода на запись только в новом формате обратное преобразование может привести к потере информации. Восстановление может заключаться в возврате к совместимой версии с сохранением обеих структур, а не в отмене изменений данных. Окно обслуживания может быть предпочтительнее, если невозможно поддерживать согласованность между версиями, необходимые блокировки неприемлемы или нельзя проверить результат при работающем приложении.
- Позволяют ли СУБД и её версия выполнить изменение с приемлемыми блокировками?
- Могут ли старые читатели и писатели работать с промежуточной схемой?
- Можно ли приостанавливать и возобновлять PHP-команду, не дублируя эффекты и не пропуская строки?
- Проверена ли стратегия для параллельных писателей, временных ошибок и расхождений?
- Будут ли проверены целостность и влияние на систему до удаления старой структуры?



