Оптимизация PostgreSQL-запросов: EXPLAIN ANALYZE, индексы и планировщик

Медленные запросы в PostgreSQL тормозят бизнес-процессы и заставляют пользователей ждать ответа системы. Мы проводим глубокий анализ планов выполнения и настраиваем индексы, чтобы ускорить работу базы данных. Наша команда реализует оптимизацию под ключ — от диагностики до внедрения и последующей поддержки, обеспечивая стабильную производительность ваших сервисов.
Часто задаваемые вопросы

Оптимизация PostgreSQL-запросов (EXPLAIN ANALYZE, индексы, планировщик)

Медленные запросы в PostgreSQL — головная боль любого DevOps-инженера. Запрос, который работал полсекунды при 100k строк, начинает висеть по 8 секунд при 2 млн — и все смотрят на разработчика. При этом сам запрос правильный, схема нормальная, железо не изменилось. Дело в планировщике PostgreSQL, который выбирал один план при маленькой таблице и переключился на другой при росте данных. Или в том, что ANALYZE не запускался месяц, и статистика устарела. Или в том, что индекс есть, но планировщик его не использует из-за неправильного statistics target. Мы сталкивались с этими проблемами десятки раз и разработали системный подход к оптимизации. Наши инженеры с 10+ летним опытом гарантируют снижение времени ответа базы данных до 50% и уменьшение нагрузки на сервер. Оценим ваш проект: пишите, получите консультацию.

Как читать план EXPLAIN ANALYZE правильно?

EXPLAIN показывает план без выполнения запроса. EXPLAIN ANALYZE — реально выполняет и показывает фактические vs ожидаемые строки. Разница между ними — ключ к диагностике.

EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT u.email, COUNT(o.id)
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.created_at > CURRENT_DATE - INTERVAL '1 year'
GROUP BY u.email;

Флаг BUFFERS добавляет информацию о попаданиях в shared_buffers и обращениях к диску — критично для понимания, насколько запрос нагружает I/O. Читать план снизу вверх: нижние узлы выполняются первыми.

На что смотреть в первую очередь:

Rows estimation mismatch. Если планировщик ожидал 10 строк (rows=10), а получил 50000 (actual rows=50000) — статистика устарела или statistics_target слишком мал. Это вынуждает планировщик выбирать Nested Loop там, где нужен Hash Join.

-- посмотреть статистику и увеличить детализацию
SELECT n_distinct, correlation, most_common_vals
FROM pg_stats
WHERE tablename = 'orders' AND attname = 'status';

ALTER TABLE orders ALTER COLUMN status SET STATISTICS 500;
ANALYZE orders;

Sequential Scan на большой таблице. Не всегда плохо — если запрос возвращает >5-10% строк таблицы, Seq Scan дешевле Index Scan. Но если returning rows мало, а Seq Scan всё равно выбирается — скорее всего, индекса нет или он не покрывает условие.

Hash Join с большим Batches. Если Batches > 1 в Hash Join — хэш-таблица не влезла в work_mem и спилилась на диск. Решение: SET work_mem = '64MB' для тяжёлых аналитических запросов (не глобально — это съест всю память при конкурентных сессиях).

Почему планировщик выбирает неверный план?

PostgreSQL использует cost-based optimizer. Стоимость операций задаётся параметрами в postgresql.conf. Самый частый случай некорректного плана — устаревшие параметры стоимости для SSD:

# Для SSD снижаем random_page_cost (default=4.0)
random_page_cost = 1.1
effective_cache_size = 12GB # ~75% от RAM сервера

С random_page_cost = 4.0 планировщик считает случайный доступ к диску дорогим и предпочитает Seq Scan даже там, где Index Scan логичнее. На SSD это приводит к неправильным решениям. > PostgreSQL Documentation: random_page_cost должен быть снижен для SSD.

Если после сбора статистики и исправления параметров запрос всё равно берёт плохой план — можно зафиксировать его через pg_hint_plan (расширение) или через параметры сессии:

-- принудительно отключить Nested Loop для текущей сессии
SET enable_nestloop = off;
EXPLAIN ANALYZE SELECT ...;
SET enable_nestloop = on;

Это инструмент диагностики, не постоянное решение. Постоянное отключение оператору планировщика ломает другие запросы.

Индексы: какие, когда и зачем

Стандартный B-tree индекс работает для равенства и диапазонов на обычных типах. Для специфических задач нужны другие:

-- частичный индекс: индексируем только активные заказы
CREATE INDEX idx_orders_active ON orders(created_at) WHERE status = 'active';

-- составной индекс: порядок колонок важен
-- работает для WHERE user_id = ? AND status = ?
-- и для WHERE user_id = ? (первая колонка)
-- НЕ работает для WHERE status = ? (только вторая колонка)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

-- covering index: включаем нужные колонки, избегая heap fetch
CREATE INDEX idx_orders_covering ON orders(user_id) INCLUDE (total_amount, created_at);

-- GIN для JSONB
CREATE INDEX idx_events_payload ON events USING GIN(payload jsonb_path_ops);

Covering index (с INCLUDE) особенно полезен для часто выполняемых запросов, которые читают 2-3 конкретных поля. Планировщик делает Index Only Scan — данные берутся прямо из индекса, без обращения к heap.

Распухшие индексы — отдельная проблема. После массовых UPDATE/DELETE индексы накапливают мёртвые страницы. pgstatindex покажет bloat:

SELECT * FROM pgstatindex('idx_orders_user_status'); -- leaf_fragmentation > 30% → пора REINDEX CONCURRENTLY 

REINDEX CONCURRENTLY не блокирует чтение и запись — в отличие от обычного REINDEX.

Пример анализа плана с расхождением rows

Допустим, у нас запрос:

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE status = 'pending'; 

План показывает: Seq Scan on orders (cost=0.00..4567.89 rows=100 actual rows=25000). Расхождение в 250 раз — статистика не обновлена. После ANALYZE orders план меняется на Index Scan с correct rows.

Кейс из нашей практики: оптимизация Laravel-приложения

Наш клиент — проект на Laravel с PostgreSQL. Запрос на выборку постраничного списка с фильтрами по статусу и дате:

SELECT * FROM products WHERE category_id = 5 AND is_active = true ORDER BY created_at DESC LIMIT 20 OFFSET 10000; 

С OFFSET 10000 планировщик прогоняет 10020 строк, отбрасывает 10000. При индексе на (category_id, is_active, created_at DESC) Index Scan работает, но offset всё равно дорогой. Решение — keyset pagination: WHERE (created_at, id) < ($last_created_at, $last_id) с соответствующим составным индексом. После внедрения время выполнения упало с 2.3 с до 0.05 с.

Другой случай — запросы с LIKE '%pattern%' без pg_trgm. Full scan на каждый запрос. После CREATE INDEX ... USING GIN(name gin_trgm_ops) те же запросы отрабатывают по индексу.

Процесс работы

  1. Сбор медленных запросов из pg_stat_statements (топ по total_exec_time) и pg_stat_activity (долгие активные запросы)
  2. EXPLAIN (ANALYZE, BUFFERS) по каждому кандидату
  3. Анализ mismatched row estimates — обновление статистики, увеличение statistics_target где нужно
  4. Создание/перестройка индексов через CREATE INDEX CONCURRENTLY (без блокировок)
  5. Проверка параметров планировщика под конкретное железо
  6. Тест производительности до/после на репрезентативных данных

Что входит в работу

  • Детальный отчёт по каждому медленному запросу (план, рекомендации)
  • Скрипты для создания/перестройки индексов
  • Настройка параметров postgresql.conf под ваше оборудование
  • Интеграция pg_stat_statements и настройка мониторинга
  • Обучение вашей команды: как читать EXPLAIN и диагностировать проблемы
  • Гарантия на результат: снижение времени выполнения топ-10 запросов минимум на 30% или доработка бесплатно

Сроки и стоимость

Тип работы Типичный срок
Анализ топ-5 медленных запросов 4-8 часов
Полный аудит + индексы + планировщик 2-3 дня
Оптимизация под высокую конкурентность (OLTP) 3-5 дней

Стоимость рассчитывается индивидуально в зависимости от объёма. Оценим ваш проект за один день: свяжитесь с нами.

Тип индекса Когда использовать Особенности
B-tree Равенство, диапазоны, сортировка Стандартный, универсальный
GIN JSONB, массивы, полнотекстовый поиск Требует jsonb_path_ops для оптимальной работы
GiST Геоданные, полнотекстовый поиск Поддерживает типы данных, не подходящие под B-tree
BRIN Большие таблицы с естественной упорядоченностью Экономит место, но менее эффективен для точечных запросов

Наши инженеры имеют 10+ лет опыта в DevOps и более 50 успешных проектов по оптимизации баз данных. Доверьте настройку PostgreSQL профессионалам — получите консультацию уже сегодня.

Наши компетенции:

Последние работы

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1542
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1393
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1368
  • Разработка логотипа компании B2B Advance
    Разработка логотипа компании B2B Advance
    815
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1139