MERGE в PostgreSQL 15 beta: тестируем на реальных ETL-процессах
PostgreSQL 15 Beta 3 добавила команду MERGE. Проверяем, как она работает на клиентских ETL-нагрузках и что даёт против классической upsert-связки.
PostgreSQL 15 Beta 3 - добавлена команда MERGE, улучшена производительность сортировки и логической репликации
PostgreSQL 15 вышел в третью бету в конце июля. В списке изменений сразу бросается в глаза MERGE - команда, которую другие СУБД поддерживают лет двадцать, а в PostgreSQL её не было никогда. Мы ведём DWH-инфраструктуру нескольких клиентов, и upsert-паттерны там повсюду - самое время посмотреть, что реально даёт эта команда в условиях, приближенных к боевым.
Что такое MERGE и зачем он нужен
Классическая задача ETL: пришла запись из источника, нужно вставить её в целевую таблицу если такой строки нет, или обновить если есть. Иногда добавляется третья ветка - удалить если источник прислал маркер удаления.
До PG15 стандартный ответ - INSERT ... ON CONFLICT DO UPDATE. Выглядит компактно, но у этого подхода есть ограничения: одно условие конфликта, одно действие на конфликт. Если нужно что-то сложнее - несколько условий, разная логика для разных типов строк - начинается огородный забор из CTE, подзапросов и нескольких операторов внутри транзакции.
Структура MERGE прямолинейна:
MERGE INTO target_table AS t
USING source_table AS s
ON t.id = s.id
WHEN MATCHED AND s.deleted_at IS NOT NULL THEN
DELETE
WHEN MATCHED THEN
UPDATE SET value = s.value, updated_at = s.updated_at
WHEN NOT MATCHED THEN
INSERT (id, value, updated_at) VALUES (s.id, s.value, s.updated_at);
Несколько веток WHEN, каждая с собственным условием. Читается - и это не преувеличение - значительно лучше, чем эквивалентный код через CTE.
Что тестировали и как
У нас есть staging-таблица с данными из нескольких внешних источников - порядка нескольких сотен тысяч строк за батч. ETL-процесс прилетает каждые несколько минут, логика уже несколько месяцев крутится на PG14 в формате нескольких CTE внутри одной транзакции: сначала UPDATE по совпадающим ключам, потом INSERT по несовпадающим. Удаление отдельной задачей по ночам.
Подняли тестовый стенд на PG15 Beta 3, залили копию данных, переписали ETL-функцию с двух CTE на один MERGE с тремя ветками. Несколько дней гоняли параллельно с продакшном, сравнивали результаты и время выполнения.
Результат по данным - идентичный. Это было первое, что проверили: строки в целевой таблице совпадают побайтно с тем, что производит старый код. На нескольких десятках батчей расхождений не нашли.
По производительности - нейтрально, плюс-минус. Честно говоря, ожидали либо заметного ускорения либо какого-то регресса. Получили примерно одинаковое время выполнения - с небольшим преимуществом у MERGE на больших батчах. Вероятно, потому что один проход по целевой таблице вместо двух последовательных операций с отдельными scan-ами. Но разница в пределах шума - не стоит ждать magic bullet по throughput.
Код стал значительно чище. Это субъективно, но важно. Старая реализация занимала 40+ строк SQL с двумя CTE и комментариями объясняющими порядок операций. MERGE-версия - 15 строк, которые сами себя объясняют. Логика трёх веток (обновить активную запись, удалить помеченную на удаление, вставить новую) видна сразу.
Нюансы, на которые натолкнулись
MERGE не поддерживает RETURNING. Для нас это была бы полезная фича - получать список вставленных/обновлённых строк для дальнейшей обработки. В текущей бете RETURNING в MERGE не поддерживается. Пришлось оставить отдельный SELECT после MERGE для получения списка изменённых записей.
Условия в WHEN MATCHED проверяются на данных source. Это интуитивно понятно, но стоит проверить заранее: условие AND s.deleted_at IS NOT NULL в WHEN MATCHED работает именно так, как написано - на данных из источника, не из целевой таблицы. Если нужно смотреть на данные target в условии - нужно добавлять алиас явно. Один раз запутались, потратили полчаса на дебаг.
Concurrent writes. Тестовый стенд у нас однопоточный по ETL. В продакшне на PG14 у нас бывают параллельные записи в staging с разных источников - как с этим справляется MERGE под нагрузкой нужно проверять отдельно. Beta есть beta, в серьёзную нагрузку не лезли.
Про улучшения сортировки и логической репликации
Beta 3 помимо MERGE несёт улучшения производительности сортировки (incremental sort в ряде случаев стал быстрее) и логической репликации (фильтрация строк на уровне публикации через WHERE). Последнее для нас актуально - у одного клиента репликация выбранных таблиц используется для аналитического стримера. До этой части ещё не добрались: это бета, и логическая репликация - не то место где хочется экспериментировать на нагрузке без длинного тестового окна.
Где сейчас
На GA переводить ETL на MERGE смысл есть - прежде всего ради читаемости кода и сокращения числа отдельных операторов в транзакциях. По производительности не рассчитывайте на большой выигрыш на простых upsert-сценариях - там INSERT ... ON CONFLICT работает отлично. MERGE даёт ценность там, где логика многоветочная: несколько типов строк, разные действия по разным условиям.
PG15 GA по расписанию осенью - будем смотреть на changelog до финального релиза и планировать миграцию тестового кластера.