Оптимізація 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