Для большинства веб-приложений справится любая из двух баз данных, и выбор важен меньше, чем схема, индексы и запросы, которые вы напишете. При этом они не взаимозаменяемы. У PostgreSQL богаче SQL: транзакционный DDL, RETURNING, частичные индексы, настоящие типы BOOLEAN и UUID, массивы, диапазонные типы, более глубокая работа с JSON с индексами GIN и система расширений, добавляющая вещи вроде PostGIS. MySQL проще в эксплуатации, дешевле в расчёте на соединение, и именно его ожидает значительная часть мира PHP - WordPress без него не работает. Если ваш фреймворк поддерживает обе и ничто не тянет вас ни в одну сторону, PostgreSQL - более сильный выбор по умолчанию для новых проектов. Если вы запускаете WordPress или другое приложение, ориентированное на MySQL, или хотите, чтобы база данных была самой неинтересной частью вашего стека, MySQL - вполне хороший выбор, и никто не должен вас от него отговаривать.
Остальная часть статьи проходит по различиям, которые действительно проявляются при разработке и эксплуатации приложения, с MySQL 8.4 LTS и PostgreSQL 17 в качестве точек отсчёта.
Коротко#
| MySQL 8.4 | PostgreSQL 17 | |
|---|---|---|
| Изоляция по умолчанию | REPEATABLE READ | READ COMMITTED |
| Соединения | Потоки; простаивающие дёшевы | Процессы; пулер нужен раньше |
| DDL в транзакциях | Нет - каждый DDL коммитит | Да |
RETURNING | Нет | Да |
| Upsert | ON DUPLICATE KEY UPDATE | ON CONFLICT ... DO UPDATE |
| Частичные индексы | Нет | Да |
| Индексы по выражениям | Функциональные индексы, 8.0.13+ | Да |
| JSON | Тип JSON, индекс через генерируемые столбцы | jsonb с индексами GIN |
| Сравнение текста по умолчанию | Без учёта регистра | С учётом регистра |
| Булевы значения | Псевдоним TINYINT(1) | Настоящий boolean |
| Обслуживание | Purge работает сам | VACUUM / autovacuum, в которых надо разобраться |
| Лицензия | GPLv2 (Community), принадлежит Oracle | PostgreSQL Licence, разрешительная |
Ни один из столбцов не является списком побед. Несколько пунктов MySQL - дешёвые соединения, отсутствие vacuum, который надо настраивать, - это эксплуатационные преимущества, которые на маленьком сервере важнее возможностей SQL.
Возможности SQL, которые вы заметите#
Возможности, которые меняют то, как вы пишете код приложения:
`RETURNING`. PostgreSQL возвращает вставленные или обновлённые строки из самого оператора: INSERT ... RETURNING id, created_at. MySQL даёт LAST_INSERT_ID() для автоинкрементного ключа и ничего для остального, так что получение значений по умолчанию или значений, заданных триггером, требует второго запроса. ORM это скрывают ценой лишнего обращения к серверу.
Транзакционный DDL. В PostgreSQL CREATE TABLE, ALTER TABLE и CREATE INDEX могут выполняться внутри транзакции и откатываться вместе с ней, так что миграция, упавшая на полпути, ничего после себя не оставляет. В MySQL каждый оператор DDL неявно коммитит. Начиная с 8.0 каждый отдельный оператор DDL атомарен - он либо выполняется, либо нет, - но миграция из пяти операторов, упавшая на четвёртом, оставляет три применёнными. Инструменты миграций на MySQL компенсируют это, записывая прогресс по каждому оператору; но когда что-то падает, убирать всё равно приходится вручную.
Частичные индексы. PostgreSQL может индексировать подмножество строк: CREATE INDEX ON orders (customer_id) WHERE status = 'open'. Такой индекс мал и быстр, когда большинство строк закрыты. В MySQL аналога нет; вы индексируете весь столбец или используете трюк с генерируемым столбцом.
Upsert. Он есть в обеих, но с разной семантикой. INSERT ... ON DUPLICATE KEY UPDATE в MySQL срабатывает на конфликт по любому уникальному ключу, что неожиданно на таблицах с несколькими уникальными ключами. ON CONFLICT (column) DO UPDATE в PostgreSQL называет ограничение, о котором идёт речь, а начиная с версии 15 доступен ещё и MERGE.
Соединения и операции над множествами. В PostgreSQL есть FULL OUTER JOIN; в MySQL его нет, и нужен UNION левого и правого соединения. Оконные функции и общие табличные выражения, включая рекурсивные, есть в обеих начиная с MySQL 8.0. INTERSECT и EXCEPT в MySQL добавили в 8.0.31.
Ограничения. MySQL проверяет ограничения CHECK начиная с 8.0.16 - до этого он их разбирал и игнорировал, и отсюда во многом растёт недоверие к нему. В PostgreSQL есть ещё ограничения исключения (никакие две брони одной и той же комнаты не могут пересекаться) и откладываемые ограничения, у которых нет аналогов в MySQL.
Материализованные представления, последовательности, `LISTEN`/`NOTIFY`, защита на уровне строк. В PostgreSQL есть все четыре. В MySQL нативно нет ни одного. Если вам нужно что-то из этого, это уже само по себе весомая причина.
Типы и строгость#
Система типов PostgreSQL больше: boolean, uuid, inet и cidr, массивы любого типа, диапазонные типы (tstzrange для периода брони), interval, перечисления, являющиеся настоящими типами, и пользовательские составные типы. MySQL покрывает самое необходимое и отображает некоторые типы на другие: BOOLEAN - это TINYINT(1), UUID - это BINARY(16) с UUID_TO_BIN() или CHAR(36), а массивы - столбец JSON.
Время - тип с подвохом. TIMESTAMP в MySQL хранит UTC, конвертирует в часовой пояс сессии и заканчивается на 2038-01-19 03:14:07 UTC - дате, которая уже попадает в срок действия ипотек, гарантий и долгих подписок. Для всего, что может выйти за эту дату, используйте DATETIME и сами храните в нём UTC. timestamptz в PostgreSQL доходит до 294276 года.
Репутация MySQL как базы, которая молча принимает плохие данные, тянется из старых версий и мягких режимов SQL. sql_mode по умолчанию в MySQL 8 включает STRICT_TRANS_TABLES, ONLY_FULL_GROUP_BY, NO_ZERO_IN_DATE, NO_ZERO_DATE и ERROR_FOR_DIVISION_BY_ZERO: вставка слишком длинной строки или даты 0000-00-00 - это ошибка, как и должно быть. Некоторые старые приложения при подключении задают более мягкий режим, чтобы продолжать работать; проверьте своё через SELECT @@SESSION.sql_mode.
Сравнение текста по умолчанию различается. Правило сравнения MySQL по умолчанию, utf8mb4_0900_ai_ci, сравнивает без учёта регистра и диакритики, так что WHERE email = 'Ana@Example.com' находит ana@example.com. PostgreSQL сравнивает точно, а для нечувствительного сопоставления используются lower(), ILIKE или расширение citext. Ни то ни другое не ошибка, но перенос приложения между ними меняет результаты запросов, которые о регистре даже не упоминают. Сторону MySQL объясняет статья utf8mb4 и правила сравнения.
JSON#
Обе хранят JSON и выполняют по нему запросы, и обе достаточно хороши, чтобы держать полуструктурированные поля рядом с реляционными данными. Разница - в индексировании.
jsonb в PostgreSQL может получить индекс GIN по всему документу, который затем обслуживает запросы на вхождение (WHERE attrs @> '{"colour": "red"}') по любому ключу. Тип JSON в MySQL индексируется извлечением нужных значений в генерируемые столбцы или многозначными индексами для массивов (8.0.17+):
-- MySQL: index one path through a generated columnALTER TABLE products ADD COLUMN colour VARCHAR(20) AS (attrs->>'$.colour') STORED, ADD INDEX idx_colour (colour);-- PostgreSQL: one index for any keyCREATE INDEX idx_attrs ON products USING gin (attrs jsonb_path_ops);Подход MySQL явный и быстрый для путей, которые вы запланировали; подход PostgreSQL гибок для запросов, которые вы не планировали. Если приложение в основном выполняет произвольную фильтрацию по JSON-документам, это склоняет чашу весов к PostgreSQL. Сторона MySQL подробно разобрана в статье JSON и генерируемые столбцы в MySQL.
Конкурентность, блокировки и обслуживание#
Обе используют многоверсионное управление конкурентным доступом: читатели не блокируют писателей, а писатели - читателей. Реализовано это по-разному, и разница проявляется в эксплуатации.
InnoDB в MySQL обновляет строки на месте и хранит старые версии в undo log для транзакций, которым они ещё нужны; фоновый поток purge удаляет их, когда они больше никому не нужны. Настраивать тут почти нечего. PostgreSQL при каждом обновлении пишет новую версию строки и оставляет старую в таблице, пока её не освободит VACUUM; autovacuum делает это автоматически, но на таблицах с интенсивной записью его нужно настраивать, а забытая долгая транзакция останавливает его полностью, и таблицы раздуваются. Полная история - в статье vacuum и раздувание в Postgres. Это реальная эксплуатационная цена PostgreSQL, о которой его список возможностей умалчивает.
Изоляция по умолчанию различается. MySQL по умолчанию использует REPEATABLE READ с блокировками промежутков и next-key на индексированных диапазонах, чтобы не допускать фантомов, - а это также означает больше ожиданий блокировок и периодические взаимоблокировки при конкурентных вставках в один и тот же диапазон. PostgreSQL по умолчанию использует READ COMMITTED, где каждый оператор видит последние подтверждённые данные. Код приложения, который читает, вычисляет и записывает обратно, ведёт себя при этих уровнях по-разному; поведение MySQL разбирает статья транзакции, блокировки и взаимоблокировки.
Кластерный первичный ключ InnoDB хранит строки в порядке ключа, поэтому сканирование диапазонов по первичному ключу очень быстрое, а случайные первичные ключи (UUIDv4) дорогие. PostgreSQL хранит строки в куче без определённого порядка, и каждый индекс, включая первичный ключ, указывает в неё.
Соединения и память#
MySQL обслуживает каждое соединение потоком. Простаивающее соединение стоит несколько сотен килобайт, и сервер спокойно держит сотни таких. PostgreSQL порождает процесс на каждое соединение, у каждого своя память; несколько сотен простаивающих соединений - это реальная цена, и обычный ответ - пулер вроде PgBouncer перед базой. На маленьком сервере это одно из самых практических различий: приложение с несколькими воркерами, у каждого из которых свой пул, упирается в комфортный предел PostgreSQL гораздо раньше. Обе всё равно наказывают за неограниченные пулы, и советы из статьи пулы соединений и лимиты применимы к каждой.
В настройке обе сводятся к размеру кэша и контролю над памятью на запрос. Главный рычаг MySQL - innodb_buffer_pool_size, обычно половина памяти или больше; у PostgreSQL - shared_buffers, около четверти, с опорой на кэш операционной системы для остального. Сравните статьи настройка InnoDB для небольших серверов и настройка PostgreSQL для небольших серверов, чтобы увидеть, насколько по-разному тратится одна и та же память.
Экосистема и переносимость#
Некоторое программное обеспечение решает за вас. WordPress требует MySQL (или совместимый сервер). Длинный хвост PHP-приложений, плагинов и тем пишет SQL, специфичный для MySQL, и тестируется только на нём. Плагины Minecraft и ресурсы FiveM, которые хранят данные в SQL-сервере, обычно рассчитаны на MySQL. С другой стороны, PostGIS делает PostgreSQL стандартом для географических данных, а инструменты вроде Supabase, Hasura и PostgREST построены вокруг него.
Фреймворки в основном нейтральны: Django, Rails, Laravel, Prisma, SQLAlchemy, Entity Framework Core и Hibernate поддерживают обе. Ненейтральны отдельные возможности: ArrayField в Django и его инструменты поиска django.contrib.postgres работают только с PostgreSQL, а любой «сырой» SQL в проекте привязывает его к той СУБД, под которую он написан.
Переехать с одной на другую позже можно, но не бесплатно. Данные переносятся инструментами вроде pgloader (из MySQL в PostgreSQL); запросы нужно проверить на чувствительность к регистру, синтаксис LIMIT внутри обновлений, функции дат, кавычки идентификаторов (обратные кавычки против двойных) и upsert. Приложение только на ORM переносится за дни; приложение с написанным вручную SQL, триггерами и процедурами - гораздо дольше.
О лицензиях: MySQL Community Server распространяется под GPLv2 и принадлежит Oracle, которая также продаёт редакцию Enterprise с дополнительными компонентами. PostgreSQL распространяется под разрешительной лицензией и развивается сообществом без единого владельца. Для приложения, подключающегося по сети, лицензия сервера в любом случае не накладывает обязательств на ваш код.
Как выбрать#
Выбирайте MySQL, когда:
- Разворачиваемое ПО требует или предпочитает его: WordPress, большинство CMS и форумов на PHP, плагины игровых серверов.
- Вы хотите, чтобы база данных требовала как можно меньше внимания, а ваш SQL - стандартный CRUD через ORM.
- Вы ожидаете много соединений от множества маленьких процессов и не хотите держать пулер.
Выбирайте PostgreSQL, когда:
- Вам нужен более богатый SQL -
RETURNING, транзакционные миграции, частичные индексы, массивы, диапазоны, ограничения исключения. - В ваших данных есть география (PostGIS) или активные произвольные запросы по JSON.
- Вы начинаете с нуля с фреймворком, который поддерживает обе, и у вас нет причин предпочесть MySQL.
Выбирайте ту, которую ваша команда уже знает, когда проект обычный, а дедлайн реальный. Для большинства приложений знание того, как отказывает одна СУБД, ценнее разницы в возможностях. Сравнение с документными и key-value хранилищами - в статьях какую базу данных выбрать и Postgres или MongoDB.
RE:NODE продаёт обе как отдельные линейки баз данных. Тарифы MySQL работают на MySQL 8.4 LTS, от 1 GB памяти и 10 GB NVMe за $4 в месяц, со сгенерированными паролем root, базой данных приложения и пользователем приложения. Тарифы PostgreSQL начинаются с 1 GB и 20 GB от $6, с учётными данными, сгенерированными для каждого сервера. К обеим подключаются по хосту и порту тарифа, и у обеих есть слоты бэкапов.
FAQ#
PostgreSQL лучше, чем MySQL?
У неё больше возможностей SQL и обширнее система типов, что делает её лучшим выбором по умолчанию для новых приложений, которые будут ими пользоваться. MySQL проще в эксплуатации, и его требует множество существующего ПО. «Лучше» зависит от того, что из этого важно для вашего проекта.
MySQL быстрее, чем PostgreSQL?
В общем случае нет. Обе быстры на простых индексированных чтениях и записях, из которых состоит большая часть веб-трафика. MySQL дешевле обслуживает большое число соединений; планировщик PostgreSQL часто лучше справляется со сложными запросами. Прежде чем решать по скорости, измерьте собственную нагрузку.
Может ли WordPress работать на PostgreSQL?
Никаким поддерживаемым способом. Ядро WordPress и большинство плагинов написаны под MySQL. Экспериментальные слои совместимости существуют, но плагины, которые пишут собственный SQL, на них ломаются. Запускайте WordPress на MySQL.
Насколько сложно потом перейти с MySQL на PostgreSQL?
Данные - простая часть. Работа - в запросах, которые полагаются на поведение MySQL: сравнения без учёта регистра, ON DUPLICATE KEY UPDATE, идентификаторы в обратных кавычках, функции дат MySQL и вольный GROUP BY в старом коде. Приложение, использующее только ORM, переносится быстро; приложению с «сырым» SQL нужны тщательная проверка и набор тестов.
Нужен ли с MySQL пулер соединений?
Обычно отдельный не нужен. Для большинства развёртываний хватает пулов на стороне приложения, потому что соединения MySQL - это потоки, а простаивающие дёшевы. Разумные размеры пулов всё равно нужны; сотни занятых соединений на двух ядрах CPU медленны в любой базе данных.




Комментарии
Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.