Как делать11 октября 2026 г.7 мин чтения
Базы данныхШаг 12 из 12

PostgreSQL или MySQL: отличия на практике

Коротко

PostgreSQL и MySQL — зрелые реляционные СУБД с транзакциями и репликацией, и для типичного веб-приложения подойдёт любая. Различия проявляются в деталях: по умолчанию PostgreSQL работает в Read Committed, а InnoDB в MySQL — в Repeatable Read; MySQL сравнивает строки без учёта регистра и не откатывает CREATE TABLE; у PostgreSQL богаче типы, jsonb с индексами GIN и механизм расширений. Выбирайте PostgreSQL для сложной модели данных, аналитики и расширений вроде PostGIS или pgvector, MySQL — когда его требует стек (например, WordPress) или он уже есть у команды и хостинга.

СравнениеУровень: СреднийMySQL

После материала вы сможете

  • назвать практические различия PostgreSQL и MySQL: сравнение строк, типы, DDL в транзакции, RETURNING, JSON и индексы, расширения
  • объяснить, чем Read Committed в PostgreSQL отличается от Repeatable Read в MySQL, и показать разницу на двух сессиях
  • предсказать, какой SQL поведёт себя в двух СУБД по-разному, и переписать его переносимо
  • выбрать СУБД под проект по таблице «когда что» с учётом хостинга и экосистемы

Проверено на

PostgreSQL 17.11MySQL 8.4.11 LTS

Сверено с документацией: 11 октября 2026 г.

Содержание
  1. 01Общая база и версии
  2. 02Один скрипт — два результата
  3. 03Транзакции: Read Committed против Repeatable Read
  4. 04JSON, типы и расширения
  5. 05Репликация, хостинг и экосистема
  6. 06Когда что: таблица выбора

Общая база и версии

Обе СУБД реляционные: таблицы, SQL, внешние ключи, транзакции, репликация, управляемые сервисы у крупных облаков. Поэтому вопрос «какая лучше» без контекста не имеет ответа. Полезнее знать, где они ведут себя по-разному: именно эти места ломают перенос приложения и удивляют разработчика, привыкшего к другой базе.

Версии на дату проверки. У PostgreSQL каждая мажорная версия поддерживается пять лет: актуальная — 18, а 17, на которой проверены примеры, поддерживается до ноября 2029 года. У MySQL две линии выпусков: LTS (8.4) получает только исправления, без удаления функций, и следует политике Oracle Lifetime Support — пять лет основной и три года расширенной поддержки; Innovation-выпуски приносят новые возможности и поддерживаются до следующего выпуска. Документация MySQL уже описывает следующую LTS-линию 9.7 и переход между соседними LTS без пропуска.

Изоляция по умолчанию

PostgreSQL 17
Read Committed
MySQL 8.4 (InnoDB)
Repeatable Read

Сравнение строк по умолчанию

PostgreSQL 17
С учётом регистра
MySQL 8.4 (InnoDB)
Без учёта регистра и диакритики (utf8mb4_0900_ai_ci)

CREATE TABLE внутри транзакции

PostgreSQL 17
Откатывается вместе с транзакцией
MySQL 8.4 (InnoDB)
Неявный COMMIT, откатить нельзя

Вернуть вставленную строку

PostgreSQL 17
INSERT … RETURNING
MySQL 8.4 (InnoDB)
Нет RETURNING; для автоинкремента — LAST_INSERT_ID()

7 / 2

PostgreSQL 17
3 (целочисленное деление)
MySQL 8.4 (InnoDB)
3.5000

||

PostgreSQL 17
Склейка строк
MySQL 8.4 (InnoDB)
Логическое OR (устаревший синоним)

boolean

PostgreSQL 17
Отдельный тип: t / f
MySQL 8.4 (InnoDB)
Синоним TINYINT(1): 1 / 0

JSON

PostgreSQL 17
jsonb + индекс GIN на весь документ
MySQL 8.4 (InnoDB)
JSON + индекс на сгенерированный столбец или многозначный индекс

Расширения

PostgreSQL 17
CREATE EXTENSION: модули contrib, PostGIS, pgvector
MySQL 8.4 (InnoDB)
Возможности встроены в сервер и зависят от версии

Один скрипт — два результата

Запустим один и тот же файл в обеих СУБД. Оба сервера — временные контейнеры с флагом --rm: после остановки от них ничего не остаётся.

bash
docker run -d --rm --name kb-pg-pgvsmy -e POSTGRES_PASSWORD=test postgres:17-alpine
docker run -d --rm --name kb-mysql-pgvsmy -e MYSQL_ROOT_PASSWORD=test -e MYSQL_DATABASE=kb mysql:8.4
docker cp compare.sql kb-pg-pgvsmy:/tmp/
Два временных сервера; файл для PostgreSQL копируется в контейнер, MySQL читает его со стандартного ввода
compare.sql
-- compare.sql — one script for PostgreSQL 17 and MySQL 8.4
CREATE TABLE customers (
  id     integer PRIMARY KEY,
  email  varchar(50) NOT NULL UNIQUE,
  name   varchar(10) NOT NULL,
  active boolean NOT NULL DEFAULT true
);

INSERT INTO customers (id, email, name) VALUES (1, 'aigerim@example.com', 'Aigerim');

-- 1. The same email in a different case
INSERT INTO customers (id, email, name) VALUES (2, 'Aigerim@Example.com', 'Aigerim');
SELECT 'Almaty' = 'ALMATY' AS same_text;

-- 2. A value longer than the column
INSERT INTO customers (id, email, name) VALUES (3, 'kon@example.com', 'Konstantin Ivanov');

-- 3. Integer division and the || operator
SELECT 7 / 2 AS division;
SELECT 'Inter' || 'LAB' AS joined;

-- 4. What a boolean looks like
SELECT id, active FROM customers ORDER BY id;

-- 5. Can you roll back CREATE TABLE?
BEGIN;
CREATE TABLE draft (id integer);
ROLLBACK;
SELECT count(*) AS draft_exists FROM information_schema.tables WHERE table_name = 'draft';

-- 6. Get the new row back in the same statement
INSERT INTO customers (id, email, name) VALUES (4, 'timur@example.com', 'Timur') RETURNING id, active;
Шесть проверок: регистр, длина, деление, ||, boolean, DDL в транзакции и RETURNING
terminal
$ docker exec kb-pg-pgvsmy psql -U postgres -f /tmp/compare.sql
CREATE TABLE
INSERT 0 1
INSERT 0 1
 same_text 
-----------
 f
(1 row)

psql:/tmp/compare.sql:16: ERROR:  value too long for type character varying(10)
 division 
----------
        3
(1 row)

  joined  
----------
 InterLAB
(1 row)

 id | active 
----+--------
  1 | t
  2 | t
(2 rows)

BEGIN
CREATE TABLE
ROLLBACK
 draft_exists 
--------------
            0
(1 row)

 id | active 
----+--------
  4 | t
(1 row)

INSERT 0 1
Реальный вывод PostgreSQL 17.11: psql продолжает выполнение после ошибки
terminal
$ docker exec -i -e MYSQL_PWD=test kb-mysql-pgvsmy mysql -uroot --force --unbuffered --show-warnings -t kb < compare.sql
ERROR 1062 (23000) at line 12: Duplicate entry 'Aigerim@Example.com' for key 'customers.email'
+-----------+
| same_text |
+-----------+
|         1 |
+-----------+
ERROR 1406 (22001) at line 16: Data too long for column 'name' at row 1
+----------+
| division |
+----------+
|   3.5000 |
+----------+
+--------+
| joined |
+--------+
|      0 |
+--------+
Warning (Code 1287): '|| as a synonym for OR' is deprecated and will be removed in a future release. Please use OR instead
Warning (Code 1292): Truncated incorrect DOUBLE value: 'Inter'
Warning (Code 1292): Truncated incorrect DOUBLE value: 'LAB'
+----+--------+
| id | active |
+----+--------+
|  1 |      1 |
+----+--------+
+--------------+
| draft_exists |
+--------------+
|            1 |
+--------------+
ERROR 1064 (42000) at line 32: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'RETURNING id, active' at line 1
Реальный вывод MySQL 8.4.11: --force продолжает после ошибок, --show-warnings печатает предупреждения

Email в другом регистре

PostgreSQL
Вставка прошла: две «разные» записи
MySQL
Duplicate entry: для колляции это один email
Что делать
В PostgreSQL — уникальный индекс по lower(email); в MySQL помнить, что é = e

Строка длиннее колонки

PostgreSQL
Ошибка
MySQL
Ошибка — благодаря строгому режиму STRICT_TRANS_TABLES в sql_mode по умолчанию
Что делать
Не отключать строгий режим: без него MySQL подрежет значение и выдаст лишь предупреждение

7 / 2

PostgreSQL
3: деление целых отбрасывает дробную часть
MySQL
3.5000: для деления целых в MySQL есть DIV
Что делать
Приводить тип явно: CAST(7 AS decimal(10, 2)) / 2

'Inter' || 'LAB'

PostgreSQL
InterLAB
MySQL
0 и три предупреждения
Что делать
CONCAT() работает в обеих

boolean

PostgreSQL
t
MySQL
1
Что делать
Драйверы приводят оба варианта к булеву типу языка, но в сырых выгрузках разница видна

CREATE TABLE + ROLLBACK

PostgreSQL
Таблицы нет
MySQL
Таблица осталась
Что делать
В MySQL неудачная миграция может примениться частично — планируйте откат отдельно

RETURNING

PostgreSQL
Вернул id и active
MySQL
Синтаксическая ошибка
Что делать
В MySQL — LAST_INSERT_ID() после вставки

Транзакции: Read Committed против Repeatable Read

Обе СУБД принимают все четыре уровня изоляции из стандарта SQL (в PostgreSQL Read Uncommitted работает как Read Committed), но по умолчанию выбирают разные.

terminal
$ docker exec kb-pg-pgvsmy psql -U postgres -c 'SHOW transaction_isolation'
 transaction_isolation 
-----------------------
 read committed
(1 row)

$ docker exec -e MYSQL_PWD=test kb-mysql-pgvsmy mysql -uroot -t -e 'SELECT @@transaction_isolation'
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+
Реальный вывод: уровень изоляции по умолчанию

В Read Committed каждый запрос видит данные, зафиксированные до его начала, поэтому два одинаковых SELECT в одной транзакции могут вернуть разное. В Repeatable Read InnoDB все обычные чтения транзакции видят снимок, созданный первым чтением. Проверим на двух сессиях: A читает, B вставляет строку и фиксирует, A читает снова.

isolation.sh
#!/usr/bin/env bash
# isolation.sh — session A reads twice inside one transaction,
# session B inserts and commits a row between those reads.
# Usage: ./isolation.sh pg | ./isolation.sh mysql
set -eu

if [ "$1" = "pg" ]; then
  client() { docker exec -i kb-pg-pgvsmy psql -U postgres -qtA; }
else
  client() { docker exec -i -e MYSQL_PWD=test kb-mysql-pgvsmy mysql -uroot -N --unbuffered kb; }
fi

{
  echo "BEGIN;"
  echo "SELECT 'A, first read:', count(*) FROM customers WHERE id >= 5;"
  sleep 2   # session B commits during this pause
  echo "SELECT 'A, second read:', count(*) FROM customers WHERE id >= 5;"
  echo "COMMIT;"
} | client &

sleep 1
echo "INSERT INTO customers (id, email, name) VALUES (5, 'dana@example.com', 'Dana');" | client
echo "B: inserted id 5 and committed"
wait
Две сессии: A держит транзакцию, B вставляет строку в паузе между чтениями
terminal
$ bash isolation.sh pg
A, first read:|0
B: inserted id 5 and committed
A, second read:|1

$ bash isolation.sh mysql
A, first read:	0
B: inserted id 5 and committed
A, second read:	0
Реальный вывод: PostgreSQL видит новую строку во втором чтении, MySQL — нет
  • PostgreSQL в Read Committed: отчёт из нескольких запросов в одной транзакции может собрать цифры из разных моментов времени. Если нужен согласованный снимок — BEGIN ISOLATION LEVEL REPEATABLE READ.
  • В Repeatable Read PostgreSQL при попытке изменить строку, которую уже изменила и зафиксировала другая транзакция, вернёт ошибку could not serialize access due to concurrent update; документация требует повторить транзакцию целиком. Код с уровнем выше Read Committed должен уметь повторять транзакции.
  • MySQL в Repeatable Read: снимок действует для обычных SELECT, но не для UPDATE и DELETE — они могут изменить строки, которые другая транзакция только что зафиксировала, хотя SELECT той же транзакции их не видит. Блокирующие чтения (SELECT … FOR UPDATE) и изменения ставят блокировки, в том числе на промежутки (gap locks). Документация не рекомендует смешивать в одной такой транзакции блокирующие и обычные чтения.

JSON, типы и расширения

Обе СУБД хранят JSON в двоичном формате и проверяют его при записи. Различие — в поиске и индексах. В PostgreSQL индекс GIN строится по всему документу jsonb и ускоряет оператор вхождения @>.

json_pg.sql
-- json_pg.sql — PostgreSQL: jsonb, the containment operator and a GIN index
CREATE TABLE products (
  id    integer PRIMARY KEY,
  attrs jsonb NOT NULL
);

INSERT INTO products VALUES
  (1, '{"name": "Latte", "tags": ["coffee", "milk"], "volume_ml": 350}'),
  (2, '{"name": "Espresso", "tags": ["coffee"], "volume_ml": 40}'),
  (3, '{"name": "Matcha", "tags": ["tea", "milk"]}');

CREATE INDEX products_attrs_gin ON products USING GIN (attrs jsonb_path_ops);

SELECT id, attrs ->> 'name' AS name
FROM products
WHERE attrs @> '{"tags": ["milk"]}'
ORDER BY id;

SET enable_seqscan = off;  -- three rows: force the planner to show the index
EXPLAIN (COSTS OFF) SELECT id FROM products WHERE attrs @> '{"tags": ["milk"]}';
jsonb, оператор вхождения и индекс GIN
terminal
$ docker exec kb-pg-pgvsmy psql -U postgres -f /tmp/json_pg.sql
CREATE TABLE
INSERT 0 3
CREATE INDEX
 id |  name  
----+--------
  1 | Latte
  3 | Matcha
(2 rows)

SET
                         QUERY PLAN                         
------------------------------------------------------------
 Bitmap Heap Scan on products
   Recheck Cond: (attrs @> '{"tags": ["milk"]}'::jsonb)
   ->  Bitmap Index Scan on products_attrs_gin
         Index Cond: (attrs @> '{"tags": ["milk"]}'::jsonb)
(4 rows)
Реальный вывод PostgreSQL: на трёх строках полный перебор отключён, чтобы показать использование индекса

В MySQL столбец JSON напрямую не индексируется: индекс строят на сгенерированном столбце, который извлекает значение по пути, или многозначный индекс по массиву через CAST(… AS … ARRAY). Оптимизатор использует такой индекс для MEMBER OF(), JSON_CONTAINS() и JSON_OVERLAPS().

json_mysql.sql
-- json_mysql.sql — MySQL: JSON, MEMBER OF and a multi-valued index
CREATE TABLE products (
  id    integer PRIMARY KEY,
  attrs json NOT NULL
);

INSERT INTO products VALUES
  (1, '{"name": "Latte", "tags": ["coffee", "milk"], "volume_ml": 350}'),
  (2, '{"name": "Espresso", "tags": ["coffee"], "volume_ml": 40}'),
  (3, '{"name": "Matcha", "tags": ["tea", "milk"]}');

-- A JSON column cannot be indexed directly...
CREATE INDEX products_attrs_idx ON products (attrs);

-- ...so index the array values instead (a multi-valued index)
CREATE INDEX products_tags_idx ON products ((CAST(attrs -> '$.tags' AS CHAR(20) ARRAY)));

SELECT id, attrs ->> '$.name' AS name
FROM products
WHERE 'milk' MEMBER OF (attrs -> '$.tags')
ORDER BY id;

EXPLAIN FORMAT=TREE SELECT id FROM products WHERE 'milk' MEMBER OF (attrs -> '$.tags');
JSON, MEMBER OF и многозначный индекс
terminal
$ docker exec -i -e MYSQL_PWD=test kb-mysql-pgvsmy mysql -uroot --force --unbuffered -r kb < json_mysql.sql
ERROR 3152 (42000) at line 13: JSON column 'attrs' supports indexing only via generated columns on a specified JSON path.
id	name
1	Latte
3	Matcha
EXPLAIN
-> Filter: json'"milk"' member of (cast(json_extract(attrs,_latin1'$.tags') as char(20) array))  (cost=0.7 rows=2)
    -> Index lookup on products using products_tags_idx (cast(json_extract(attrs,_latin1'$.tags') as char(20) array)=json'"milk"')  (cost=0.7 rows=2)
Реальный вывод MySQL 8.4: прямой индекс отклонён, многозначный используется

Встроенных типов у PostgreSQL больше: uuid, сетевые inet и cidr, интервалы, массивы, диапазоны, tsvector для полнотекстового поиска, а собственные типы добавляются через CREATE TYPE. В MySQL BOOLEAN — синоним TINYINT(1), зато в сервер встроены пространственные типы по модели OpenGIS с индексами SPATIAL в InnoDB и полнотекстовые индексы.

Главное отличие PostgreSQL — расширения. Модули из поставки (contrib) подключаются командой CREATE EXTENSION в нужной базе; сторонние проекты, например PostGIS для геоданных и pgvector для векторного поиска, устанавливаются на сервер отдельно.

ext_pg.sql
-- ext_pg.sql — extensions in the official postgres:17-alpine image
SELECT count(*) AS available FROM pg_available_extensions;

CREATE EXTENSION pg_trgm;                        -- trigram similarity, ships with PostgreSQL
SELECT similarity('Almaty', 'Almata') AS score;

CREATE EXTENSION vector;                         -- pgvector is a separate project
Расширения в официальном образе postgres:17-alpine
terminal
$ docker exec kb-pg-pgvsmy psql -U postgres -f /tmp/ext_pg.sql
 available 
-----------
        59
(1 row)

CREATE EXTENSION
   score   
-----------
 0.5555556
(1 row)

psql:/tmp/ext_pg.sql:7: ERROR:  extension "vector" is not available
DETAIL:  Could not open extension control file "/usr/local/share/postgresql/extension/vector.control": No such file or directory.
HINT:  The extension must first be installed on the system where PostgreSQL is running.
Реальный вывод: pg_trgm подключён, pgvector в образе отсутствует

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

Репликация, хостинг и экосистема

Физическая репликация

PostgreSQL
Потоковая передача WAL на резервные серверы; hot standby принимает запросы только на чтение
MySQL
Репликация из двоичного журнала (binlog) с источника на реплики, по умолчанию асинхронная

Логическая и дополнительные режимы

PostgreSQL
Логическая репликация отдельных таблиц по идентичности строк (обычно первичному ключу); синхронная репликация как опция
MySQL
GTID вместо позиций в журнале, полусинхронная репликация, Group Replication и InnoDB Cluster

Управляемые сервисы

PostgreSQL
Amazon RDS, Google Cloud SQL и другие
MySQL
Amazon RDS, Google Cloud SQL и другие

Экосистема

PostgreSQL
Расширения (PostGIS, pgvector), сложные запросы и аналитика внутри базы
MySQL
Традиционный стек PHP; WordPress требует MySQL 8.0+ или MariaDB 10.11+

По хостингу различий почти нет: Amazon RDS и Google Cloud SQL предлагают обе СУБД. Разница начинается в деталях — какие версии и расширения доступны, как настраиваются реплики, — поэтому их проверяют в документации конкретного провайдера, а не по общему названию СУБД.

Когда что: таблица выбора

Новый продукт без жёстких ограничений по стеку

Выбор
PostgreSQL
Почему
Строгие типы, транзакционные миграции, jsonb и расширения дают запас на рост

Геоданные, полнотекстовый или векторный поиск в основной базе

Выбор
PostgreSQL
Почему
PostGIS, tsvector, pgvector — без отдельного сервиса

Сложные отчёты и аналитика внутри базы

Выбор
PostgreSQL
Почему
Богатые типы и функции, Read Committed с явным переходом на Repeatable Read для отчётов

WordPress и другие системы, рассчитанные на MySQL

Выбор
MySQL (или MariaDB)
Почему
Это требование самой системы; другая СУБД — путь без официальной поддержки

У команды и хостинга уже есть MySQL, опыт и мониторинг

Выбор
MySQL
Почему
Знакомая эксплуатация важнее различий, которые проекту не нужны

Перенос существующего приложения

Выбор
Та же СУБД, что сейчас
Почему
Смена базы затрагивает коллации, изоляцию, DDL и синтаксис — это отдельный проект
  • Какую СУБД требует платформа или CMS проекта?
  • Нужны ли расширения (PostGIS, pgvector) и есть ли они у выбранного хостинга?
  • Какой уровень изоляции ожидает код и умеет ли он повторять транзакции?
  • Должны ли уникальные текстовые поля учитывать регистр?
  • Как откатывается неудачная миграция?
Как проверено6 · пройденосверено 11 октября 2026 г.
  • запущено, работает

    compare.sql в PostgreSQL и MySQL, вывод обеих СУБД

    docker run --rm postgres:17-alpine (17.11) и mysql:8.4 (8.4.11, sql_mode по умолчанию, collation_server utf8mb4_0900_ai_ci); psql -f и mysql --force --unbuffered --show-warnings -t со стандартного ввода — ровно те команды, что в статье; вывод вставлен скриптом build.mjs из out/

  • запущено, работает

    SHOW transaction_isolation / SELECT @@transaction_isolation

    read committed / REPEATABLE-READ

  • запущено, работает

    isolation.sh (две сессии) и isolation_swap.sh (упражнение)

    Git Bash на Windows 11 + docker exec: PG RC 0→1, MySQL RR 0→0; после смены уровней PG RR 0→0, MySQL RC 0→1; запускались несколько раз с одинаковым результатом. На Linux/macOS не запускались, но используют только bash и docker

  • запущено, работает

    json_pg.sql и json_mysql.sql

    обе возвращают Latte и Matcha; PG — Bitmap Index Scan on products_attrs_gin (enable_seqscan=off); MySQL — ERROR 3152 на прямой индекс и Index lookup using products_tags_idx в EXPLAIN FORMAT=TREE

  • запущено, работает

    ext_pg.sql

    59 доступных расширений в postgres:17-alpine, CREATE EXTENSION pg_trgm, similarity = 0.5555556, CREATE EXTENSION vector → extension "vector" is not available

  • запущено, работает

    решения упражнений: exercise_pg.sql, portable.sql

    уникальный индекс по lower(email) отклоняет вторую вставку; portable.sql в обеих СУБД: InterLAB, 3.5 (3.5000000000000000 / 3.500000), true (t / 1)

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

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