RSS Telegram YouTube Apple Яндекс Spotify Google Amazon Почта

34. Выстрел в ногу с PostgreSQL

14.01.2024

Скачать

К списку выпусков

Ссылки выпуска:

В этом выпуске мы обсудим статью Фила Бута под названием:

9 способов выстрелить себе в ногу с PostgreSQL

Попробуем аргументированно согласится или не согласится с автором.

1. Оставить значение work_mem по умолчанию.

work_mem - максимальный объем используемой памяти для выполнения запроса. Если мы делаем JOINы (hash/merge join), CTE или сортировки, то значение work_mem важно для эффективного выполнения запроса. Посмотреть текущее значение можно: SHOW work_mem;. Для текущей сессии можно задать значение таким образом: SET [ SESSION | LOCAL ] work_mem TO '16MB';. SET SESSION устанавливает значение параметра для текущей сессии, LOCAL для транзакции.

Но как понять какое значение устнавливать для work_mem? Для этого нужно проверять вывод EXPLAIN (ANALYSE, BUFFERS): если будет hash join, то вы увидите размер hash-таблицы для мержа (нужно искать "Memory usage"). Так, опытным путем станет понятно достаточно ли дефолтных 4Мб или это значение нужно увеличить.

Дефолтные 4Мб - это вполне достаточно для старта. Т.е. можно вполне запускаться в продакшн с этим дефолтом. Важно не упускать мониторинг за медленными запросами, в чем может помочь, например, pgBadger.

2. Перенести логику приложения в функции и хранимые процедуры.

На самом деле это вполне себе неплохая идея. Я даже работал с биллинг системой в телекоме, где на хранимых процедурах было построено чуть больше чем все. Также хранимые процедуры можно писать не только на динамических ЯП вроде Python 3, TCL, Perl, но и на, например, Rust.

Преимущества: нет сетевых издержек. Недостаток: меньшая гибкость (в коде внести правки проще), проблемы с версионированием (если надо, чтобы работала как старая логика, так и новая).

В этом вопросе я расхожусь с мнением автора. Хранимые процедуры - отличные инструмент, который может прийтись в пору на орпеделенном этапе развития проекта.

3. Очень много триггеров.

Начинаем с простых ограничений (constraints): CHECK, NOT NULL, UNIQUE, PK, FK, EXCLUDE. Если необходимо вставить калькулированные данные в колонку, то лучше рассмотреть возможность использования сгенерированных колонок (GENERATED COLUMNS) или, если нужно формировать больше данных, на основе существующих, то обратиться к материализованным представлениям (MATERIALIZED VIEWS). Кстати, обновлять данные материализованных представлений можно с помощью REFRESH и делать это в самом PostgreSQL с использованием расширения pg_cron. Если эти средства исчерпаны, а цель не достигнута, то можно рассмотреть триггеры. Например, если надо убедиться в целостности данных, когда речь идет о других таблицах.

Триггеры не стоит рассматривать как модель событийно-ориентированной разработки. Например, не стоит логгировать в таблицу из триггеров, когда данные были добавлены в какую-то таблицу. Лучше предпочесть принцип "явное лучше неявного" и писать явно лог-таблицу в соседнем запросе.

4. Активное использование NOTIFY.

Если нужно немедленно сообщить об успешном завершении транзакции, то можно использовать NOTIFY. Это решение хорошо тем, что уведомления транзакционные. Все слушающие стороны принимают просто строку. Строку можно оформить в виде JSON-объекта, например, используя row_to_json функцию.

Проблема с этим решением в том, что все равно нужно где-то хранить payload уведомления на случай, если по какой-либо причине не было ни одного активного слушателя (LISTEN). Я поступаю так: делаю очередь прямо тут, в PostgreSQL (если это позволяют требования) и в добавок, как элемент усложнения, добавляю NOTIFY.

Для организации очереди создаем простую табличку, в которой по крону выбираем задачи. Не забываем, что выборка может случится конкурентная, поэтому полезно использовать в SELECT-запросе FOR UPDATE SKIP LOCKED.

5. Не использовать EXPLAIN ANALYZE.

Полностью согласен, что инструмент крайне полезный. Чем больше и сложнее проект, тем чаще нужно анализировать ваши запросы с помощью EXPLAIN ANALYZE. Там можно увидеть какие индексы используются, сколько потребляется памяти, как быстро выполняется запрос и т.д. Особенно полезно прибегать к EXPLAIN для анализа запросов к большим таблицам или при слиянии таблиц (JOIN).

6. Предпочитать CTE, а не подзапросы.

Чем хороши CTE? Читаемость, переиспользуемость (и рекурсии; с ними у меня очень мало опыта)! Особенно это удобно, когда у вас есть серия запросов, где один зависит от результатов предыдущего.

Итак, до 12 версии PostgreSQL, все CTE были MATERIALIZED. Это значит, что результаты работы выражения сохранялись в оперативной памяти вроде как таблица, у которой естественно нет индексов и любые операции с этими данными означала Seq Scan в EXPLAIN. С 12 версии все запросы стали NOT MATERIALIZED. Т.е. если планировщик посчитает, что можно превратить CTE в подзапрос, то он это сделает.

work_mem и тут о себе напоминает! Промежуточные результаты работы CTE хранятся как раз в этой памяти.

Итого, можно вполне спокойно использовать CTE. Не забываем про п. 5 и проверяем эффективность выполнения запросов.

7. Использование рекурсивных CTE.

Все, что требует рекурсии в СУБД скорее всего может быть денормализовано так, чтобы рекурсия не была нужна. В случае древовидных структур есть nested set или полная денормализация графа. Да, во втором случае долгая запись, но зато очень быстрое чтение!

8. Не добавлять индексы к FK.

В MySQL индексы создаются автоматически для колонки, которая является FK. В PostgreSQL это не так. Без индексов каждый JOIN по FK становится ударом по ресурсам. Не забываем, что даже если мы не делаем JOIN по FK, но у нас есть каскадное удаление или обновление FK (ON DELETE, ON UPDATE) то индексы тоже нужны! Если не пренебрегать EXPLAIN, то вы точно не пропустите отсутствие индекса.

Заключение

От себя я еще добавлю, что имеет смысл периодически обновлять PostgreSQL до более свежих версий. Как и со всеми обновлениями торопиться не стоит. В новых версиях как правило улучшается производительность, улучшается планировщик и т.д.

К списку выпусков