SQL

БЛОК 10. Сравнение СУБД — 24. PostgreSQL vs MySQL — базовый синтаксис

📚 24 вопросовПройти тест →
Лекция

БЛОК 10. Сравнение СУБД — 24. PostgreSQL vs MySQL — базовый синтаксис

SQL

24. PostgreSQL vs MySQL — базовый синтаксис

🧭 Введение: зачем сравнивать синтаксис двух СУБД

На реальных проектах SQL редко живёт в «идеальном вакууме одной базы».
Команда может мигрировать сервис с MySQL на PostgreSQL, поддерживать оба диалекта или читать чужой код после смены стека.
В этой теме разбираем ключевые отличия именно по базовому прикладному синтаксису:
  • LIMIT / OFFSET;
  • UPSERT;
  • RETURNING;
  • FULL OUTER JOIN.
💡 Совет: Сравнивайте не «кто лучше», а «какой синтаксис даст тот же бизнес-результат в другой СУБД».
Вывод: Эта тема про переносимость запросов и снижение ошибок при смене диалекта.

⚠️ Проблема -> решение

Типичная проблема:
  1. запрос синтаксически валиден в PostgreSQL, но падает в MySQL;
  2. разработчик переносит SQL «как есть» и теряет функциональность (RETURNING, FULL OUTER JOIN);
  3. из-за различий UPSERT/пагинации появляется неожиданное поведение в API.
Решение:
  1. держать базовую карту различий по синтаксису;
  2. для каждого кейса знать «нативный вариант» и «fallback»;
  3. проверять критичные запросы на целевой СУБД до релиза.
🟢 Если совсем просто: Один и тот же смысл в PostgreSQL и MySQL часто пишется по-разному.
🎯 Как понять, что этап прошёл успешно: Вы можете показать эквивалентный SQL для двух СУБД по каждой из 4 тем.

🛠️ Чем помогает и как работает

Сравнение синтаксиса помогает не «переписывать всё с нуля», а быстро находить эквивалентные конструкции.
Это особенно важно в миграциях, multi-DB продуктах и собеседованиях.
🟢 Если совсем просто: У вас есть готовая шпаргалка «PostgreSQL -> MySQL».
🎯 Как понять, что этап прошёл успешно: Вы перестали ловить runtime-ошибки вида «syntax error near ...» при переносе запросов.
Чем помогает:
  • снижает риск поломок при миграции;
  • ускоряет code review по SQL;
  • делает поведение API предсказуемым между СУБД;
  • экономит время на отладке несовместимого синтаксиса.
Как это работает:
  • Шаг 1: фиксируем бизнес-задачу (пагинация, upsert, возврат вставленной строки, сверка данных);
  • Шаг 2: берём нативный синтаксис целевой СУБД;
  • Шаг 3: если функции нет (например, FULL OUTER JOIN в MySQL), используем рабочий обходной паттерн;
  • Шаг 4: проверяем итог на реальных данных.
Вывод: Грамотное сравнение диалектов — это инженерная необходимость, а не академическое упражнение.

📚 Ключевые термины (простыми словами)

Перед практикой синхронизируем словарь.
🟢 Если совсем просто: Важно различать «один смысл» и «разный синтаксис».
🎯 Как понять, что этап прошёл успешно: Вы уверенно объясняете, что такое UPSERT и почему FULL OUTER JOIN не везде доступен нативно.
  • Dialect (диалект SQL) — реализация SQL в конкретной СУБД с собственными расширениями.
  • UPSERT — «вставить или обновить, если ключ уже существует».
  • RETURNING — возврат вставленных/обновлённых строк прямо из DML.
  • FULL OUTER JOIN — соединение, возвращающее и совпадения, и несовпавшие строки с обеих сторон.
  • Portable SQL — SQL, который проще адаптировать между СУБД.
Вывод: Термины позволяют обсуждать перенос SQL без двусмысленности.

🧩 1. LIMIT / OFFSET: базовая пагинация

Обе СУБД поддерживают пагинацию, но есть различия формы записи и привычных паттернов.
Для переносимости лучше опираться на форму, которая читается одинаково и явно.
🟢 Если совсем просто: LIMIT и OFFSET есть в обеих, но записи отличаются.
🎯 Как понять, что этап прошёл успешно: Вы знаете эквивалент PostgreSQL-запроса в MySQL без потери смысла.
Назначение: Ограничивать размер выдачи и выбирать «страницу» результатов.
Простыми словами: LIMIT — сколько строк вернуть, OFFSET — сколько пропустить.
Для новичка: Всегда задавайте ORDER BY, иначе пагинация будет нестабильной.
Пример (PostgreSQL):
SELECT  o.id,  o.created_atFROM orders oORDER BY o.created_at DESC, o.id DESCLIMIT 20 OFFSET 40;
Пример (MySQL, переносимый стиль):
SELECT  o.id,  o.created_atFROM orders oORDER BY o.created_at DESC, o.id DESCLIMIT 20 OFFSET 40;
Пример (MySQL, альтернативная форма):
SELECT  o.id,  o.created_atFROM orders oORDER BY o.created_at DESC, o.id DESCLIMIT 40, 20;
🔎 Как это происходит на практике:
  • Контекст: API-список заказов с постраничной навигацией.
  • Действия: используем общий LIMIT ... OFFSET стиль для простоты миграции.
  • Результат: код легче переносить между PostgreSQL и MySQL.
Характеристики:
  • семантика пагинации совпадает;
  • форма записи в MySQL может быть двух видов;
  • большие OFFSET тяжелы в обеих СУБД.
Когда использовать: Для базовой пагинации и интерфейсных листингов.
Вывод: По LIMIT/OFFSET различия небольшие, но единый стиль снижает риск ошибок.

🧪 2. UPSERT: PostgreSQL ON CONFLICT vs MySQL ON DUPLICATE KEY

UPSERT синтаксически различается заметно сильнее, чем пагинация.
Именно здесь чаще всего ломается перенос «один-в-один».
🟢 Если совсем просто: В PostgreSQL и MySQL одна идея UPSERT, но разные ключевые слова.
🎯 Как понять, что этап прошёл успешно: Вы пишете корректный upsert для каждой СУБД без подсказки.
Назначение: Идемпотентно записывать данные без дублей по уникальному ключу.
Простыми словами: Если ключа нет — вставить, если есть — обновить.
Для новичка: UPSERT опирается на уникальный индекс/constraint.
Пример (PostgreSQL):
INSERT INTO users (email, full_name, updated_at)VALUES ('a@a.com', 'Alice', NOW())ON CONFLICT (email) DO UPDATESET full_name = EXCLUDED.full_name,    updated_at = EXCLUDED.updated_at;
Пример (MySQL):
INSERT INTO users (email, full_name, updated_at)VALUES ('a@a.com', 'Alice', NOW()) AS newON DUPLICATE KEY UPDATE  full_name = new.full_name,  updated_at = new.updated_at;
🔎 Как это происходит на практике:
  • Контекст: синхронизация пользователей из внешнего сервиса.
  • Действия: под каждую СУБД используем нативный UPSERT.
  • Результат: загрузка становится идемпотентной и устойчивой к дублям.
Характеристики:
  • логика одинаковая, синтаксис разный;
  • в PostgreSQL явная цель конфликта через ON CONFLICT (...);
  • в MySQL ключ определяется через сработавший duplicate key.
Когда использовать: В ETL, репликации событий и API с повторными запросами.
Вывод: UPSERT — главный «синтаксический разлом» между PostgreSQL и MySQL.

🧱 3. RETURNING: возврат изменённых строк

RETURNING часто нужен в CRUD: после INSERT/UPDATE/DELETE сразу получить id, timestamps и другие поля.
В PostgreSQL это нативный и очень удобный механизм.
🟢 Если совсем просто: PostgreSQL умеет сразу вернуть изменённую строку, MySQL — обычно через отдельные шаги.
🎯 Как понять, что этап прошёл успешно: Вы знаете, как сделать эквивалентный поток «insert -> получить id/данные» в обеих СУБД.
Назначение: Сократить количество round-trip и получить консистентный результат DML.
Простыми словами: RETURNING экономит лишний SELECT.
Для новичка: В MySQL для такого сценария обычно используют LAST_INSERT_ID() и затем SELECT.
Пример (PostgreSQL):
INSERT INTO users (email, full_name)VALUES ('bob@example.com', 'Bob')RETURNING id, email, created_at;
Пример (MySQL, частый рабочий паттерн):
INSERT INTO users (email, full_name)VALUES ('bob@example.com', 'Bob'); SELECT LAST_INSERT_ID() AS id;
SELECT  u.id,  u.email,  u.created_atFROM users uWHERE u.id = LAST_INSERT_ID();
🔎 Как это происходит на практике:
  • Контекст: endpoint «создать пользователя и вернуть DTO».
  • Действия: в PostgreSQL — один запрос, в MySQL — обычно два шага.
  • Результат: одинаковый бизнес-результат при разном синтаксисе.
Характеристики:
  • PostgreSQL RETURNING сильно упрощает DML-флоу;
  • в MySQL для эквивалентного флоу обычно используют INSERT + LAST_INSERT_ID() + дополнительный SELECT;
  • при переносе важно заранее продумать контракт ответа API.
Когда использовать: В CRUD-операциях, где нужно немедленно вернуть изменённые данные.
Вывод: По RETURNING PostgreSQL обычно даёт более прямой синтаксис, MySQL чаще требует workaround.

🚦 4. FULL OUTER JOIN: нативно в PostgreSQL, обход в MySQL

FULL OUTER JOIN нужен для сверки двух наборов: «совпало», «только слева», «только справа».
В PostgreSQL это стандартный оператор. В MySQL в синтаксисе JOIN нет нативного FULL OUTER JOIN, поэтому используют эмуляцию.
🟢 Если совсем просто: PostgreSQL: FULL OUTER JOIN есть. MySQL: в JOIN-синтаксисе его нет, поэтому делаем LEFT + RIGHT через UNION ALL.
🎯 Как понять, что этап прошёл успешно: Вы умеете написать эквивалентный reconciliation-запрос для обеих СУБД.
Назначение: Сравнить два источника и найти расхождения.
Простыми словами: Нужно увидеть и совпадения, и несовпавшие строки с обеих сторон.
Для новичка: Для отчётов сверки сразу добавляйте метку типа расхождения (only_left, only_right, matched).
Пример (PostgreSQL):
SELECT  COALESCE(c.id, b.external_user_id) AS key_id,  c.email AS crm_email,  b.email AS billing_emailFROM crm_users cFULL OUTER JOIN billing_users b ON b.external_user_id = c.id;
Пример (MySQL, эмуляция):
SELECT  c.id AS key_id,  c.email AS crm_email,  b.email AS billing_emailFROM crm_users cLEFT JOIN billing_users b ON b.external_user_id = c.id
UNION ALL SELECT  b.external_user_id AS key_id,  c.email AS crm_email,  b.email AS billing_emailFROM billing_users bLEFT JOIN crm_users c ON c.id = b.external_user_idWHERE c.id IS NULL;
🔎 Как это происходит на практике:
  • Контекст: ночная сверка CRM и Billing.
  • Действия: PostgreSQL использует FULL OUTER JOIN, MySQL — LEFT + UNION ALL.
  • Результат: одинаковая бизнес-сводка расхождений.
Характеристики:
  • PostgreSQL даёт компактный и очевидный синтаксис;
  • MySQL требует явной эмуляции;
  • при эмуляции важно не забывать фильтр WHERE left_id IS NULL во второй части.
Когда использовать: Для reconciliation-отчётов и поиска «висячих» данных.
Вывод: FULL OUTER JOIN — ключевое функциональное отличие, которое нужно помнить при миграции.

🧪 5. Мини-шпаргалка различий

КейсPostgreSQLMySQL
ПагинацияLIMIT ... OFFSET ...LIMIT ... OFFSET ... и LIMIT offset, count
UPSERTON CONFLICT (...) DO UPDATEON DUPLICATE KEY UPDATE
RETURNINGНативно для DMLЧасто нужен LAST_INSERT_ID() + SELECT
FULL OUTER JOINНативноОбычно эмуляция через LEFT JOIN + UNION ALL
Вывод: Главные различия — в UPSERT, RETURNING и FULL OUTER JOIN; пагинация ближе по смыслу.

🎓 Junior Roadmap: минимум и опционально

Чтобы тема была junior-friendly, разделяем материал на два слоя.
🟢 Обязательный минимум (junior):
  • LIMIT / OFFSET для базовой пагинации;
  • UPSERT: ON CONFLICT vs ON DUPLICATE KEY UPDATE;
  • RETURNING в PostgreSQL и fallback-поток в MySQL (LAST_INSERT_ID() + SELECT);
  • FULL OUTER JOIN в PostgreSQL и эмуляция в MySQL.
🟡 Опционально позже (через 1-2 месяца практики):
  • тонкости составных/функциональных индексов под переносимые запросы;
  • углублённый анализ EXPLAIN ANALYZE;
  • продвинутые нюансы производительности и оптимизации multi-DB SQL.
Вывод: Сначала закрепляем эквивалентность базового синтаксиса, потом добавляем прод-нюансы.

🧠 Must-Know (запомнить)

  • PostgreSQL и MySQL делят общую SQL-базу, но различаются в важных деталях синтаксиса.
  • LIMIT/OFFSET близки, но в MySQL есть альтернативная форма LIMIT offset, count.
  • UPSERT: PostgreSQL ON CONFLICT, MySQL ON DUPLICATE KEY.
  • Для UPSERT нужен уникальный ключ/индекс.
  • PostgreSQL RETURNING обычно делает CRUD проще.
  • В MySQL в прикладных CRUD-флоу обычно используют INSERT + LAST_INSERT_ID() и отдельный SELECT.
  • FULL OUTER JOIN нативен в PostgreSQL.
  • В MySQL в JOIN-синтаксисе нет FULL OUTER JOIN, поэтому используют LEFT JOIN + UNION ALL.
  • При переносе SQL проверяйте не только синтаксис, но и эквивалентность результата.
Вывод: Сравнение диалектов — это про корректный эквивалент, а не про буквальный copy-paste.

❌ Частые мифы

Миф: PostgreSQL и MySQL отличаются только «производительностью». ✅ Как правильно: Отличия синтаксиса напрямую влияют на прикладной код и миграции. 📎 Почему это важно: Ошибки часто возникают именно на уровне SQL-операторов.
Миф: UPSERT одинаковый во всех СУБД. ✅ Как правильно: Логика похожа, но синтаксис и детали конфликта отличаются. 📎 Почему это важно: Неправильный перенос ломает идемпотентность.
Миф: RETURNING «везде одинаково работает». ✅ Как правильно: В PostgreSQL это базовый инструмент, в MySQL часто применяют обходной путь. 📎 Почему это важно: Иначе ломаются контракты API после вставки/обновления.
Миф: FULL OUTER JOIN можно использовать в MySQL без изменений. ✅ Как правильно: Обычно нужен шаблон эмуляции через UNION ALL. 📎 Почему это важно: Иначе reconciliation-отчёты не запускаются.

🎤 Часто спрашивают на собеседованиях

Вопрос: Чем отличается UPSERT в PostgreSQL и MySQL? ✅ Ответ: PostgreSQL использует ON CONFLICT (...) DO UPDATE, MySQL — ON DUPLICATE KEY UPDATE.
Вопрос: Как написать базовую пагинацию переносимо между PostgreSQL и MySQL? ✅ Ответ: Использовать форму LIMIT ... OFFSET ... и явный ORDER BY.
Вопрос: Зачем нужен RETURNING в PostgreSQL? ✅ Ответ: Чтобы сразу вернуть изменённые строки из DML без отдельного SELECT.
Вопрос: Как получить аналог RETURNING в MySQL? ✅ Ответ: Часто через LAST_INSERT_ID() и дополнительный SELECT по этому id.
Вопрос: Есть ли FULL OUTER JOIN в MySQL? ✅ Ответ: Обычно нет нативного оператора, используют эмуляцию через LEFT JOIN + UNION ALL.
Вопрос: Что чаще всего ломается при миграции SQL между PostgreSQL и MySQL? ✅ Ответ: UPSERT, возврат данных после DML и запросы со FULL OUTER JOIN.
Вопрос: Достаточно ли «синтаксически запустить» запрос после переноса? ✅ Ответ: Нет, нужно проверить эквивалентность результата на реальных данных.
Вопрос: Какой практический подход к multi-DB SQL? ✅ Ответ: Держать диалектные адаптеры/ветки запросов для несовместимых конструкций.

🚨 Типичные ошибки

  • Переносить PostgreSQL SQL в MySQL без проверки диалектных различий.
  • Забывать уникальный ключ для UPSERT.
  • Ожидать RETURNING в MySQL как в PostgreSQL без fallback-логики.
  • Пытаться писать FULL OUTER JOIN в MySQL нативно.
  • Использовать пагинацию без ORDER BY.
  • Проверять перенос только на «маленьких» тестовых данных.
Вывод: Большинство ошибок в теме — это ошибки переносимости и неверных ожиданий от диалекта.

✅ Best Practices

  • Для каждого критичного запроса храните эквиваленты PostgreSQL/MySQL.
  • Используйте единый стиль LIMIT ... OFFSET ... для более простого переноса.
  • В UPSERT явно документируйте ключ конфликта.
  • Для MySQL заранее проектируйте fallback вместо RETURNING.
  • Для FULL OUTER JOIN в MySQL держите готовый шаблон LEFT + UNION ALL.
  • Добавляйте интеграционные тесты на обе СУБД, если продукт multi-DB.
  • Проверяйте не только запуск SQL, но и бизнес-эквивалент результата.
Вывод: Production-ready подход — это управляемая диалектная совместимость, а не надежда на «универсальный SQL».

🏁 Заключение

PostgreSQL и MySQL решают одинаковые прикладные задачи, но путь в синтаксисе часто разный.
Если вы уверенно сравниваете LIMIT/OFFSET, UPSERT, RETURNING и FULL OUTER JOIN, вы сможете безопасно переносить SQL и поддерживать multi-DB код без хаотичных правок.
Вывод: Ключ к стабильной миграции — знать диалектные различия заранее и держать рабочие эквиваленты.
🎯

Проверьте знания

Закрепите материал — пройдите тест по теме «БЛОК 10. Сравнение СУБД — 24. PostgreSQL vs MySQL — базовый синтаксис»

Пройти тест →