Если после миграции даты отображаются на несколько часов раньше или позже, нельзя сразу прибавлять одинаковое смещение ко всем строкам. Одни поля описывают точный момент времени, другие — местное время события, третьи — только календарную дату. Неправильное исправление может повторно сдвинуть часть данных и повредить отчеты, платежи или расписания.
Надежная диагностика начинается с определения смысла каждого поля, типа столбца в исходной и целевой базе, часового пояса соединения и преобразований внутри ETL. Только после этого можно построить обратимую процедуру исправления.
Как проявляется ошибка часового пояса
- Все события сдвинуты на одинаковое число часов.
- Старые записи отображаются правильно, а перенесенные — нет.
- Зимние даты отличаются на один час от летних из-за перехода на летнее время.
- В базе значение правильное, но API или интерфейс показывает другое время.
- Экспорт и отчет расходятся, хотя читают одну таблицу.
- Дата рождения, срок договора или день доставки неожиданно меняется на соседний день.
- Повторный запуск миграции увеличивает смещение еще раз.
Остановите повторное повреждение данных
- Приостановите повторный импорт или синхронизацию проблемных полей.
- Сделайте резервную копию целевых таблиц и сохраните исходный экспорт без изменений.
- Зафиксируйте версию ETL-кода, настройки серверов, базы и окружения на момент переноса.
- Выберите несколько контрольных записей, время которых можно подтвердить по первичному источнику.
- Не запускайте массовый 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
- Возьмите запись с известным реальным временем события.
- Посмотрите сырое значение в исходной базе без форматирования интерфейсом.
- Зафиксируйте тип поля и часовую зону исходного соединения.
- Проверьте значение сразу после чтения драйвером и его timezone-информацию.
- Посмотрите промежуточный CSV, JSON, очередь или staging-таблицу.
- Проверьте значение непосредственно перед вставкой и параметры целевой сессии.
- Сравните сырое значение в целевой базе, ответ API и итоговое отображение.
Первый этап, на котором значение меняется без ожидаемого преобразования, и есть вероятный источник ошибки. Если база хранит правильный момент, а проблема появляется только в браузере, исправлять миграцию не нужно — следует проверять сериализацию API и клиентское форматирование.
Частые причины неправильного сдвига
- Строка без зоны была ошибочно принята за UTC, хотя содержала местное время.
- Значение уже было в UTC, но ETL повторно перевел его в UTC.
- Драйвер базы преобразовал дату автоматически, а код выполнил второе преобразование.
- Часовой пояс системы изменился между экспортом и импортом.
- CSV не содержал смещения, и импорт использовал локальную зону сервера.
- В API потерялся суффикс Z или явное смещение.
- Дата без времени прошла через объект DateTime и сменила календарный день.
- Для старых дат применили текущее смещение вместо исторических правил зоны.
Почему нельзя всегда прибавить три часа
Фиксированный сдвиг работает только в узком случае, когда доказано одно и то же ошибочное преобразование для однородного набора данных. Он не учитывает разные исходные зоны, переходы на летнее время, исторические изменения правил и поля с календарной датой.
- Записи могли поступать из нескольких регионов.
- Часть строк могла быть перенесена правильно другим запуском.
- Одинаковый local time может иметь два варианта в день перевода часов.
- Некоторого местного времени не существует во время весеннего перехода.
- Повторный запуск исправления может сдвинуть уже исправленные записи.
Как построить корректное правило исправления
- Определите бизнес-смысл поля и ожидаемую зону в исходной системе.
- Выделите точный диапазон поврежденных строк по batch id, времени импорта или другому надежному признаку.
- Восстановите исходное локальное значение или момент времени из сохраненного экспорта.
- Для момента времени примените именованную исходную зону и преобразуйте результат в выбранный стандарт хранения.
- Для календарной даты перенесите компоненты года, месяца и дня без timezone-конвертации.
- Для расписания сохраните местное время и идентификатор зоны отдельно.
- Запишите старое и новое значения в таблицу аудита до обновления основной строки.
Исправляйте данные через повторяемый скрипт
Ручной 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 и аудит изменений, восстановить поврежденный диапазон и добавить проверки, которые не позволят повторить сдвиг.