Как мы ускорили SQL-запросы: реальные кейсы оптимизации PostgreSQL

в 17:06, , рубрики: join, postgresql, sql, оптимизация SQL, оптимизация запросов, оптимизация запросов postgresql, производительность базы данных

Достаточно большое количество проблем производительности в backend-приложениях на самом деле находятся не в коде. За последние пару лет мне несколько раз приходилось разбирать системы, где:

  • API отвечало слишком долго

  • CPU базы был загружен почти на 100%

При этом всем, инфраструктура мощная: достаточное количество RAM, NVMe-диски ну и конечно же CPU последних поколений. Но проблема почти всегда оказывалась в SQL-запросах.
Я хочу поделиться реальным опытом, как мы оптимизировали PostgreSQL в десятки раз

Кейс 1. Индекс, который помог ускорить запрос

Один из самых типичных запросов в системе:

SELECT *
FROM orders
WHERE user_id = 54821
ORDER BY created_at DESC
LIMIT 20;

Таблица "orders" у нас содержит где-то 1 миллион строк, 25 колонок , ну и конечно же активную запись. Запрос выполнялся примерно 3.2 секунды.
План выполнения

EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE user_id = 54821
ORDER BY created_at DESC
LIMIT 20;

Фрагмент плана:

Seq Scan on orders
Filter: (user_id = 54821)
Rows Removed by Filter: 17984521

Многие могут понять, что это значит. PostgreSQL прочитал почти всю таблицу.
Есть решение. который ускорит запрос.

CREATE INDEX idx_orders_user_created
ON orders(user_id, created_at DESC);

После такого уже новый план:

Index Scan using idx_orders_user_created

Примерное время выполнения 25 мс! Это почти в 100 раз ускорение.

Кейс 2. SELECT *, который вечно долго думал

Один из сервисов отдавал список заказов:

SELECT *
FROM orders
WHERE status = 'completed'
ORDER BY created_at DESC
LIMIT 1000;

Тут была проблема не в SQL-времени, а в объёме данных. Таблица содержала JSON-колонку "metadata", текстовые поля и некоторое количество больших колонок. Примерный размер строки 6кб, а 1000 строк примерно 6мб. Запрос выполнялся за 400мс. 

Решением такой проблемы послужило следующее решение:

SELECT id, user_id, total_price, created_at
FROM orders
WHERE status = 'completed'
ORDER BY created_at DESC
LIMIT 1000;

После такого изменения новый размер ответа составил где-то 140кб и время ответа составило 40мс. Ускорилось в 10 раз.

Кейс 3. JOIN, который создавал бомбежку всех строк

Один из аналитических запросов выглядел так:

SELECT *
FROM users
JOIN orders ON users.id = orders.user_id
JOIN payments ON orders.id = payments.order_id
JOIN reviews ON users.id = reviews.user_id;

Проблема заключается в кардинальности.
У пользователя есть 20 заказов, 20 платежей и 10 отзывов. Рузельтат 20 × 20 × 10 = 4000 строк. Это называется join explosion - объём результатов запроса с операцией JOIN значительно больше, чем ожидалось.

Решение следующее. Агрегируем данные до JOIN:

WITH payments_agg AS (
  SELECT order_id, SUM(amount) total
  FROM payments
  GROUP BY order_id
)

SELECT *
FROM orders
JOIN payments_agg
ON orders.id = payments_agg.order_id;

Инструменты, которыми я пользуюсь и которые могу помочь

Мой совет, если вы работайте с PostgreSQL, то обязательно используйте:

  • EXPLAIN ANALYZE которые показывает реальные строки. Вез него оптимизация SQL будет мучением.

  • pg_stat_statements который показывает медленные запросы, частые запросы и время их выполнения.

Мои личные правила для оптимизации SQL

  1. Индекс должен соответствовать запросу

  2. Не использовать OFFSET на огромных таблицах

  3. Избегать " SELECT *", лучше выбирать только нужные поля

  4. Всегда использовать " EXPLAIN ANALYZE" для лучше оптимизации

Автор: Razor00913

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js