Миграция базы данных с MySQL на PostgreSQL в PHP-проекте
Перенос базы данных редко сводится к замене одного драйвера другим. В PHP-проекте меняются типы данных, синтаксис SQL-запросов, правила сортировки, работа с последовательностями и поведение транзакций. Поэтому миграцию с MySQL на PostgreSQL я рассматривал как поэтапную переработку слоя хранения, а не как разовую конвертацию дампа.
Задача появилась в проекте, где база постепенно выросла вместе с функциональностью приложения. Первоначально MySQL полностью устраивал систему, однако со временем стали заметны ограничения в сложных выборках, контроле целостности и работе с типизированными данными. Хотелось получить более предсказуемое поведение запросов и строгую структуру, которую проще поддерживать в долгосрочной перспективе.
Проект написан на PHP и использует реляционную модель: пользователи, роли, документы, события и связанные записи. Часть интерфейса обращается к серверу через AJAX, поэтому изменения в API требовалось проводить без нарушения клиентских сценариев. Отдельное внимание пришлось уделить совместимости с существующими фоновыми задачами и административной панелью.
Я разбил работу на несколько независимых этапов: аудит схемы, подготовка PostgreSQL, перенос структуры, загрузка данных, адаптация PHP-кода и проверка производительности. Такой порядок позволил быстро находить ошибки на небольшом объёме данных и не превращать финальное переключение в рискованный эксперимент.
Почему исходная база перестала устраивать
В MySQL часть решений была принята ещё на раннем этапе разработки. Некоторые поля имели слишком универсальные типы, даты хранились с разной точностью, а проверка внешних ключей была непоследовательной. Приложение компенсировало эти недостатки на уровне PHP, но такой подход постепенно усложнял код.
Проблемы проявлялись в запросах с несколькими соединениями и агрегациями. MySQL позволял долго оставлять неоднозначные конструкции, которые PostgreSQL отвергает сразу. Это стало полезным сигналом: новая СУБД заставила явно описать намерения разработчика и обнаружить скрытые ошибки в логике выборок.
Дополнительным мотивом была необходимость использовать более выразительные средства PostgreSQL: качественную работу с JSONB, проверочные ограничения, оконные функции и продуманные индексы. Я не переносил старую модель механически, а пересматривал её там, где прежние компромиссы уже мешали развитию приложения.
Аудит схемы и зависимостей
Сначала я составил перечень таблиц, индексов, представлений и процедур, а затем отметил связи между ними. Для каждой колонки зафиксировал тип, допустимость NULL, значение по умолчанию и фактический диапазон значений. Такой аудит показал несколько расхождений между документацией и реальным содержимым базы.
Особенно внимательно пришлось проверить идентификаторы. В MySQL часто использовался INT AUTO_INCREMENT, тогда как в PostgreSQL аналогом стал GENERATED BY DEFAULT AS IDENTITY. Для уже существующих записей требовалось после импорта синхронизировать последовательности, иначе следующая вставка могла получить занятый ключ.
Я также проверил код PHP на скрытые зависимости от MySQL. В проекте встретились обратные кавычки вокруг имён таблиц, функции IFNULL, GROUP_CONCAT, условные вставки и сравнения дат в строковом формате. Эти места были вынесены в отдельный список, чтобы после миграции не искать ошибки по всему приложению.
Подготовка окружения PostgreSQL
Для разработки я поднял PostgreSQL в отдельном контейнере, близком к будущим параметрам сервера. В конфигурации заранее создал базу, пользователя приложения и отдельную роль для миграционных операций. Это помогло проверить права доступа и исключить ситуацию, когда локальная среда скрывает проблемы благодаря привилегиям администратора.
Кодировка базы была выбрана UTF-8, часовой пояс задан явно. Для денежных значений я использовал NUMERIC, для флагов — BOOLEAN, а для больших текстовых полей — TEXT. Тип JSONB применил только там, где структура действительно могла меняться; обычные атрибуты оставил отдельными колонками, чтобы сохранить контроль и удобную индексацию.
Перед переносом я подготовил миграции в системе контроля версий. Каждая операция стала повторяемой: создание таблиц, добавление ограничений, индексов и начальных данных выполнялось в определённой последовательности. Это оказалось удобнее, чем редактировать схему вручную через графический клиент.
Перенос структуры и SQL-запросов
Структуру я создавал заново, а не переносил результатом автоматической конвертации. Для каждой таблицы сначала добавлял поля и первичный ключ, затем внешние ключи, ограничения CHECK и индексы. Такой порядок позволил загрузить данные без временного отключения важных правил или сохранить их контроль на этапе импорта.
В SQL-запросах пришлось заменить несколько привычных конструкций. IFNULL стал COALESCE, конкатенация строк выполнялась через ||, а операции с датами были переписаны с учётом типов PostgreSQL. Для выборок с группировкой я явно указал все неагрегированные поля в GROUP BY, поскольку новая СУБД строже проверяет корректность запроса.
Часть запросов была переведена на именованные параметры PDO. Это повысило читаемость и одновременно уменьшило зависимость от конкретного драйвера. В местах, где различия между СУБД всё же были неизбежны, я добавил небольшой слой адаптеров, а бизнес-логику оставил общей для всех окружений.
Перенос данных и контроль результата
Для экспорта я написал промежуточный PHP-скрипт, который читал данные порциями и формировал подготовленные записи для PostgreSQL. Такой вариант оказался надёжнее прямого копирования: можно было преобразовать даты, очистить некорректные значения и отдельно обработать старые поля, объединённые в одну новую колонку.
Основные различия между двумя системами выглядели так:
| Область | MySQL | PostgreSQL | Что изменил |
|---|---|---|---|
| Автонумерация | AUTO_INCREMENT |
IDENTITY или sequence |
Перенёс ключи и синхронизировал последовательности |
| Логический флаг | TINYINT(1) |
BOOLEAN |
Преобразовал 0/1 в false/true |
| Длинный текст | TEXT |
TEXT |
Оставил тип, проверив кодировку |
| JSON-документы | JSON |
JSONB |
Добавил индексы для часто используемых полей |
| Обработка NULL | IFNULL |
COALESCE |
Переписал выражения в запросах |
| Дата и время | Зависит от режима | Строгие типы timestamp | Убрал неявные преобразования |
После импорта я сравнил количество строк в каждой таблице, максимальные идентификаторы, суммы по финансовым полям и число связных записей. Отдельно проверил строки с кириллицей, пустыми значениями и нестандартными часовыми поясами. Для крупных таблиц контроль выполнялся по хешам отдельных диапазонов, чтобы не загружать всю базу в память.
Ошибки загрузки не игнорировались. Каждая проблемная запись попадала в журнал с исходным идентификатором и причиной отказа. В результате удалось обнаружить несколько старых строк с датой 0000-00-00, которая допустима в некоторых конфигурациях MySQL, но не принимается PostgreSQL.
Адаптация PHP-приложения
Подключение к новой СУБД я перевёл на PDO с драйвером pdo_pgsql. В конфигурации приложения разделил параметры соединения, чтобы среда разработки, тестовый сервер и production использовали одинаковый код при разных переменных окружения. После подключения включил исключения PDO и запретил тихое игнорирование ошибок.
Большинство репозиториев продолжило работать после небольших правок, однако динамические фильтры потребовали отдельной проверки. В PostgreSQL чувствительнее проявились ошибки с типами параметров, особенно при сравнении числовых идентификаторов со строковыми значениями из HTTP-запросов. Я добавил явную валидацию и приведение данных до формирования SQL.
Для клиентской части контракт API сохранился прежним: названия полей и формат JSON не менялись без необходимости. Поэтому браузерный код не пришлось переписывать целиком. При проверке асинхронных запросов помог разбор асинхронных запросов JavaScript, особенно при анализе последовательности ответов и обработки ошибок на интерфейсе.
Транзакции стали использоваться активнее. Операции, которые раньше выполнялись несколькими независимыми запросами, я объединил в атомарные блоки. Для фоновых задач добавил повторный запуск при временных сбоях соединения, но ограничил число попыток, чтобы ошибка не создавала бесконечную нагрузку.
Тестирование и переключение
До переноса production я подготовил копию базы и прогнал полный набор функциональных тестов. Проверялись регистрация, вход, права доступа, поиск, фильтры, загрузка файлов, отчёты и удаление связанных сущностей. Для каждой операции сравнивались HTTP-статус, структура ответа и изменения в базе.
Отдельно провёл нагрузочное тестирование наиболее частых запросов. В некоторых местах PostgreSQL выбрал другой план выполнения, поэтому пришлось добавить составные индексы и изменить порядок соединений. Для диагностики использовал EXPLAIN ANALYZE, сравнивая фактическое количество строк и время выполнения, а не ориентируясь только на ощущения интерфейса.
Финальное переключение состояло из короткого окна обслуживания. Сначала приложение перевело запись в режим ожидания, затем был выполнен последний экспорт изменившихся данных, проверены контрольные показатели и изменена строка подключения. После запуска я наблюдал журнал PHP, ошибки PostgreSQL и ключевые метрики API.
В течение первых часов особенно важны были не общие показатели сервера, а редкие сценарии: восстановление пароля, массовое редактирование, удаление пользователя и формирование отчёта за большой период. Именно такие операции чаще всего выявляют расхождения в типах, сортировке или транзакционной логике.
Что дала миграция проекту
После перехода схема стала строже и понятнее. Внешние ключи, ограничения и типы данных перенесли часть проверок из PHP непосредственно в базу. Это снизило вероятность появления повреждённых связей и сделало поведение приложения более предсказуемым при ошибках в клиентских запросах.
Работа показала, что перенос СУБД лучше планировать как аудит всей системы хранения. Автоматический конвертер может ускорить старт, но он не заменяет анализ запросов, тестирование данных и проверку бизнес-правил. Наиболее полезными оказались повторяемые миграции, журнал проблемных строк и отдельная проверка производительности.
В результате приложение сохранило публичный API и привычный пользовательский сценарий, а серверная часть получила более строгую основу для дальнейшего развития. Главный практический вывод для меня прост: сначала нужно понять реальные зависимости проекта, затем переносить данные и только после этого оптимизировать запросы под возможности PostgreSQL.