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

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

Как проявляется ошибка часового пояса

  • Все события сдвинуты на одинаковое число часов.
  • Старые записи отображаются правильно, а перенесенные — нет.
  • Зимние даты отличаются на один час от летних из-за перехода на летнее время.
  • В базе значение правильное, но API или интерфейс показывает другое время.
  • Экспорт и отчет расходятся, хотя читают одну таблицу.
  • Дата рождения, срок договора или день доставки неожиданно меняется на соседний день.
  • Повторный запуск миграции увеличивает смещение еще раз.

Остановите повторное повреждение данных

  1. Приостановите повторный импорт или синхронизацию проблемных полей.
  2. Сделайте резервную копию целевых таблиц и сохраните исходный экспорт без изменений.
  3. Зафиксируйте версию ETL-кода, настройки серверов, базы и окружения на момент переноса.
  4. Выберите несколько контрольных записей, время которых можно подтвердить по первичному источнику.
  5. Не запускайте массовый UPDATE, пока не определены семантика поля и затронутый диапазон строк.

Сначала разделите даты по смыслу

Одинаковая строка вида 2026-08-01 10:00 может означать принципиально разные вещи. Правило хранения и преобразования зависит не от формата, а от бизнес-смысла.

  • Момент времени: создание заказа, платеж, вход пользователя. Его обычно удобно хранить в UTC и переводить при показе.
  • Местное время: запись к врачу в 10:00 по времени конкретного города. Вместе со значением нужна временная зона события.
  • Календарная дата: день рождения, дата договора, отчетный день. К ней нельзя автоматически применять часовое смещение.
  • Повторяющееся расписание: каждый понедельник в 09:00 по местному времени. Нужна именованная зона, чтобы учитывать сезонные правила.
  • Продолжительность или интервал: это не дата и не должен проходить через timezone-преобразование.

Проверьте типы столбцов в обеих базах

Названия похожих типов могут скрывать разное поведение. Например, одни СУБД преобразуют значение относительно часового пояса сессии, а другие хранят введенные цифры без зоны. При миграции между разными СУБД это особенно важно.

  • Сравните тип исходного и целевого столбца, его точность и допустимость null.
  • Проверьте timezone соединения, сессии базы, ETL-процесса и приложения.
  • Уточните поведение драйвера: он может автоматически превращать значение в объект даты.
  • Проверьте, не теряется ли смещение при выгрузке в CSV, JSON или промежуточную таблицу.
  • Убедитесь, что дата без времени не была преобразована через полночь UTC.

В MySQL типы TIMESTAMP и DATETIME ведут себя по-разному относительно зоны сессии. В PostgreSQL важно различать timestamp with time zone и timestamp without time zone. Ориентироваться только на слово timestamp недостаточно — нужно проверить фактическое поведение конкретной цепочки чтения и записи.

Проследите одно значение по всему ETL

  1. Возьмите запись с известным реальным временем события.
  2. Посмотрите сырое значение в исходной базе без форматирования интерфейсом.
  3. Зафиксируйте тип поля и часовую зону исходного соединения.
  4. Проверьте значение сразу после чтения драйвером и его timezone-информацию.
  5. Посмотрите промежуточный CSV, JSON, очередь или staging-таблицу.
  6. Проверьте значение непосредственно перед вставкой и параметры целевой сессии.
  7. Сравните сырое значение в целевой базе, ответ API и итоговое отображение.

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

Частые причины неправильного сдвига

  • Строка без зоны была ошибочно принята за UTC, хотя содержала местное время.
  • Значение уже было в UTC, но ETL повторно перевел его в UTC.
  • Драйвер базы преобразовал дату автоматически, а код выполнил второе преобразование.
  • Часовой пояс системы изменился между экспортом и импортом.
  • CSV не содержал смещения, и импорт использовал локальную зону сервера.
  • В API потерялся суффикс Z или явное смещение.
  • Дата без времени прошла через объект DateTime и сменила календарный день.
  • Для старых дат применили текущее смещение вместо исторических правил зоны.

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

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

  • Записи могли поступать из нескольких регионов.
  • Часть строк могла быть перенесена правильно другим запуском.
  • Одинаковый local time может иметь два варианта в день перевода часов.
  • Некоторого местного времени не существует во время весеннего перехода.
  • Повторный запуск исправления может сдвинуть уже исправленные записи.

Как построить корректное правило исправления

  1. Определите бизнес-смысл поля и ожидаемую зону в исходной системе.
  2. Выделите точный диапазон поврежденных строк по batch id, времени импорта или другому надежному признаку.
  3. Восстановите исходное локальное значение или момент времени из сохраненного экспорта.
  4. Для момента времени примените именованную исходную зону и преобразуйте результат в выбранный стандарт хранения.
  5. Для календарной даты перенесите компоненты года, месяца и дня без timezone-конвертации.
  6. Для расписания сохраните местное время и идентификатор зоны отдельно.
  7. Запишите старое и новое значения в таблицу аудита до обновления основной строки.

Исправляйте данные через повторяемый скрипт

Ручной UPDATE трудно проверить и безопасно повторить. Лучше подготовить отдельный скрипт с режимом dry run, ограничением по идентификаторам миграции и идемпотентной отметкой исправленных строк.

  • Сначала выводите предполагаемые изменения без записи.
  • Обрабатывайте данные небольшими партиями и фиксируйте прогресс.
  • Не меняйте строку, если она уже имеет отметку исправления нужной версии.
  • Проверяйте допустимый диапазон результата и неожиданно большие смещения.
  • Храните исходное значение или ссылку на архив для обратного отката.
  • После каждой партии сравнивайте контрольные выборки и бизнес-агрегаты.

Учитывайте летнее время и неоднозначные даты

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

  • Не заменяйте Europe/Berlin или America/New_York постоянным UTC+1 или UTC-5.
  • Для неоднозначного времени используйте дополнительные данные: порядок события, исходное смещение или timestamp журнала.
  • Если определить вариант невозможно, пометьте запись для ручной проверки вместо случайного выбора.
  • Зафиксируйте версию timezone-базы в воспроизводимой среде миграции.

Проверьте API и интерфейс

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

  • Для момента времени передавайте ISO 8601 с Z или явным смещением.
  • Для календарной даты используйте отдельный формат даты без полуночи и зоны.
  • Не добавляйте букву Z к строке, которая фактически содержит местное время.
  • Укажите, откуда берется зона показа: профиль пользователя, организация, объект или браузер.
  • Проверьте серверные отчеты и фоновые задачи, которые могут форматировать даты иначе, чем веб-интерфейс.

Как проверить результат исправления

  • Контрольные события совпадают с первичным источником и журналами.
  • Летняя и зимняя даты отображаются с правильным историческим смещением.
  • Календарные даты не переходят на соседний день.
  • API, интерфейс, экспорт и отчет показывают согласованный результат.
  • Сортировка и фильтрация по периоду включают ожидаемые граничные записи.
  • Повторный запуск скрипта не изменяет исправленные строки.
  • Новая миграция тестовой партии больше не создает сдвиг.

Типичные ошибки при восстановлении

  • Сдвинуть все даты на одинаковое число часов без разделения типов полей.
  • Исправить отображение CSS или JavaScript-костылем, оставив неверные данные в базе.
  • Применить текущее смещение к историческим датам.
  • Запустить обновление без резервной копии и журнала старых значений.
  • Исправить всю таблицу, хотя поврежден только один batch миграции.
  • Повторно конвертировать уже нормализованные значения.
  • Тестировать только одну сегодняшнюю дату и не проверять переходы суток и сезонные правила.

Как предотвратить повторение

  • Опишите семантику каждого временного поля в схеме данных.
  • Храните точные моменты в едином стандарте, а зону отображения — отдельно.
  • Не превращайте date-only в datetime без явной необходимости.
  • Передавайте в обмене явное смещение или Z и валидируйте входной формат.
  • Устанавливайте timezone соединения и процесса явно, не полагаясь на настройки сервера.
  • Добавьте тесты для полуночи, разных регионов и переходов летнего времени.
  • Прогоняйте миграцию на контрольной выборке и сравнивайте данные на каждом этапе ETL.

Итог

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

Если нужно безопасно исправить даты после миграции, я могу разобрать схему полей, настройки баз и ETL, подготовить dry run и аудит изменений, восстановить поврежденный диапазон и добавить проверки, которые не позволят повторить сдвиг.