24. PostgreSQL vs MySQL — базовый синтаксис
🧭 Введение: зачем сравнивать синтаксис двух СУБД
На реальных проектах SQL редко живёт в «идеальном вакууме одной базы».
Команда может мигрировать сервис с MySQL на PostgreSQL, поддерживать оба диалекта или читать чужой код после смены стека.
Команда может мигрировать сервис с MySQL на PostgreSQL, поддерживать оба диалекта или читать чужой код после смены стека.
В этой теме разбираем ключевые отличия именно по базовому прикладному синтаксису:
LIMIT / OFFSET;UPSERT;RETURNING;FULL OUTER JOIN.
💡 Совет:
Сравнивайте не «кто лучше», а «какой синтаксис даст тот же бизнес-результат в другой СУБД».
✅ Вывод:
Эта тема про переносимость запросов и снижение ошибок при смене диалекта.
⚠️ Проблема -> решение
Типичная проблема:
- запрос синтаксически валиден в PostgreSQL, но падает в MySQL;
- разработчик переносит SQL «как есть» и теряет функциональность (
RETURNING,FULL OUTER JOIN); - из-за различий UPSERT/пагинации появляется неожиданное поведение в API.
Решение:
- держать базовую карту различий по синтаксису;
- для каждого кейса знать «нативный вариант» и «fallback»;
- проверять критичные запросы на целевой СУБД до релиза.
🟢 Если совсем просто:
Один и тот же смысл в PostgreSQL и MySQL часто пишется по-разному.
🎯 Как понять, что этап прошёл успешно:
Вы можете показать эквивалентный SQL для двух СУБД по каждой из 4 тем.
🛠️ Чем помогает и как работает
Сравнение синтаксиса помогает не «переписывать всё с нуля», а быстро находить эквивалентные конструкции.
Это особенно важно в миграциях, multi-DB продуктах и собеседованиях.
Это особенно важно в миграциях, 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.idUNION 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. Мини-шпаргалка различий
| Кейс | PostgreSQL | MySQL |
|---|---|---|
| Пагинация | LIMIT ... OFFSET ... | LIMIT ... OFFSET ... и LIMIT offset, count |
| UPSERT | ON CONFLICT (...) DO UPDATE | ON 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 CONFLICTvsON 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, MySQLON 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 код без хаотичных правок.✅ Вывод:
Ключ к стабильной миграции — знать диалектные различия заранее и держать рабочие эквиваленты.