rzaripov.kz

Миграция базы данных с 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.