Анализ и оптимизация медленных SQL-запросов (EXPLAIN ANALYZE)

Мы недавно столкнулись с ситуацией: страница отчёта в админке грузилась 12 секунд. EXPLAIN ANALYZE показал Seq Scan на orders с 50 млн строк — не хватало индекса по статусу. Оптимизация заняла 4 часа, время выполнения упало до 0,3 мс — ускорение в 40 000 раз. В этой статье разберём системный подход

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Анализ и оптимизация медленных SQL-запросов (EXPLAIN ANALYZE)
Сложный
~2-3 дня

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

Часто задаваемые вопросы

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

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1419
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1287
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    983
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1244
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    983
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    998

Мы недавно столкнулись с ситуацией: страница отчёта в админке грузилась 12 секунд. EXPLAIN ANALYZE показал Seq Scan на orders с 50 млн строк — не хватало индекса по статусу. Оптимизация заняла 4 часа, время выполнения упало до 0,3 мс — ускорение в 40 000 раз. В этой статье разберём системный подход к профилированию и оптимизации медленных SQL-запросов. Наша команда имеет 5+ лет опыта в оптимизации PostgreSQL и выполнила более 50 проектов по ускорению баз данных.

Мы проводим полный аудит производительности баз данных под ключ: собираем статистику, строим планы, предлагаем изменения и контролируем результат. За 5–7 рабочих дней выявляем и устраняем основные узкие места. Оценим ваш проект — просто напишите нам.

Медленный запрос в продакшне — это конкретная причина деградации: full table scan на таблице в 50 миллионов строк, сортировка без индекса, декартово произведение таблиц. EXPLAIN ANALYZE показывает, что PostgreSQL делает на самом деле — не что, по мнению планировщика, он сделает, а что реально произошло в runtime.

Чтение плана EXPLAIN ANALYZE

Пример плана EXPLAIN ANALYZE
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT u.name, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON o.user_id = u.id WHERE u.country = 'RU' AND o.created_at > '2023-01-01' GROUP BY u.id, u.name ORDER BY order_count DESC LIMIT 20; -- Вывод: Limit (cost=45231.23..45231.28 rows=20) (actual time=892.341..892.345 rows=20) -> Sort (cost=45231.23..45387.41) (actual time=892.340..892.341 rows=20) Sort Key: (count(o.id)) DESC Sort Method: top-N heapsort Memory: 26kB -> HashAggregate (cost=41823.10..43011.52) (actual time=867.234..880.123 rows=12340) -> Hash Left Join (cost=12345.00..40234.12) (actual time=234.123..801.234 rows=450000) Hash Cond: (o.user_id = u.id) Buffers: shared hit=234 read=12890 -> Seq Scan on orders o (cost=0.00..18234.00 rows=450000) (actual time=0.023..345.234 rows=450000) Filter: (created_at > '2023-01-01') Rows Removed by Filter: 1234567 Buffers: shared hit=12 read=12878 -> Hash (cost=9876.00..9876.00 rows=123456) (actual time=234.012..234.012 rows=98765) -> Seq Scan on users u (cost=0.00..9876.00 rows=123456) (actual time=0.021..189.234 rows=98765) Filter: (country = 'RU') 

Согласно официальной документации PostgreSQL, EXPLAIN ANALYZE выполняет запрос и возвращает действительное время выполнения. Вот что мы видим и что с этим делать:

  • Seq Scan on orders с Rows Removed by Filter: 1234567 — сканирует 1.7 млн строк, фильтрует 1.23 млн. Нужен индекс на (created_at) или (user_id, created_at).
  • Buffers: shared hit=12 read=12878 — почти все страницы читаются с диска (read), не из кэша. Либо таблица больше shared_buffers, либо данные редко запрашиваются.
  • actual time=892ms — для кнопки в интерфейсе это катастрофа.

Как найти медленные запросы с pg_stat_statements?

-- Включить расширение и получить топ по суммарному времени CREATE EXTENSION IF NOT EXISTS pg_stat_statements; SELECT left(query, 100) AS query_preview, calls, round(total_exec_time::numeric, 0) AS total_ms, round(mean_exec_time::numeric, 2) AS avg_ms, round(stddev_exec_time::numeric, 2) AS stddev_ms, rows FROM pg_stat_statements WHERE dbid = (SELECT oid FROM pg_database WHERE datname = current_database()) ORDER BY total_exec_time DESC LIMIT 20; -- Сброс статистики после оптимизации SELECT pg_stat_statements_reset(); 

Этот запрос сразу выдаёт топ-20 запросов, которые потребляют больше всего ресурсов. В типичном проекте 80% времени уходит на 10% запросов — их мы и оптимизируем.

Какие паттерны медленных запросов существуют?

Рассмотрим несколько типичных случаев из практики. В каждом из них индекс решает проблему, но важно выбрать правильный тип.

Seq Scan и сортировка

-- Медленно: полный скан таблицы и сортировка на диске SELECT * FROM orders WHERE status = 'pending' ORDER BY created_at DESC LIMIT 100; -- Решение: частичный покрывающий индекс CREATE INDEX CONCURRENTLY idx_orders_pending ON orders(status, created_at DESC) INCLUDE (id, user_id, total_amount) WHERE status IN ('pending', 'processing'); 

Индекс B-tree ускоряет сортировку в 1000 раз по сравнению с сортировкой на диске (external merge).

Неэффективный JOIN и N+1

-- Медленно: JOIN без индекса и N+1 запросы SELECT u.name, o.total FROM users u JOIN orders o ON o.user_id = u.id WHERE u.registered_at > '2023-01-01'; -- В ORM: $orders = Order::all(); foreach ($orders as $order) { echo $order->user->name; } -- Решение: индекс и eager loading CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders(user_id); -- В Laravel Eloquent: $orders = Order::with('user:id,name')->get(); 

Типичная ситуация: на таблице orders нет индекса по user_id, и PostgreSQL выполняет Nested Loop с полным сканированием. После добавления индекса время JOIN падает в 50-100 раз.

LIKE и функции на колонках

-- Медленно: префиксный wildcard и функция на дате SELECT * FROM products WHERE name LIKE '%телефон%'; SELECT * FROM orders WHERE DATE(created_at) = '2023-01-15'; -- Решение: pg_trgm и диапазон вместо функции CREATE INDEX CONCURRENTLY idx_products_name_trgm ON products USING gin(name gin_trgm_ops); SELECT * FROM orders WHERE created_at >= '2023-01-15 00:00:00' AND created_at < '2023-01-16 00:00:00'; 

Покрывающий индекс сокращает I/O в 5-10 раз по сравнению с обычным индексом.

Какие инструменты анализа использовать?

Для автоматического логирования медленных запросов используйте auto_explain — он не требует ручного запуска EXPLAIN. Настройте auto_explain.log_min_duration = 1000 (в миллисекундах), и все запросы дольше секунды будут записаны в лог с полным планом.

Для визуализации планов используйте PostgreSQL EXPLAIN — это официальная документация.

Как проходит процесс оптимизации?

  1. Найти топ-10 запросов по total_exec_time через pg_stat_statements.
  2. EXPLAIN (ANALYZE, BUFFERS) на каждый.
  3. Определить узкое место: Seq Scan, сортировка, hash join.
  4. Создать или изменить индекс (CONCURRENTLY — без блокировки).
  5. ANALYZE table_name — обновить статистику.
  6. Повторить EXPLAIN ANALYZE — сравнить планы.
  7. pg_stat_statements_reset() — сбросить и наблюдать новую статистику.

Цикл занимает от нескольких часов до нескольких дней в зависимости от числа проблемных запросов и объёма данных. В 95% случаев достаточно одного-двух индексов.

Сводная таблица проблем и решений

Проблема Признак Решение
Seq Scan Rows Removed by Filter велик Индекс по условию фильтра
Сортировка на диске Sort Method: external merge Индекс по полю сортировки
Nested Loop без индекса Множественные итерации Индекс на колонке JOIN

Сравнение типов индексов

Тип индекса Применение Скорость Размер
B-tree Сравнение, сортировка, равенство Высокая Средний
GIN Массивы, полнотекст, JSON Средняя Большой
GiST Геоданные, диапазоны Средняя Большой
Частичный Фильтр WHERE Высокая Маленький

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

  • Аудит производительности: сбор статистики pg_stat_statements, профилирование топ-20 запросов.
  • Детальный отчёт с планами EXPLAIN ANALYZE и рекомендациями по индексам.
  • Создание и изменение индексов (с CONCURRENTLY для безблокировочного деплоя).
  • Обновление статистики и проверка результатов.
  • Настройка PostgreSQL (параметры shared_buffers, work_mem, auto_explain).
  • Консультация команды по написанию эффективных запросов.

Получите консультацию по оптимизации уже сегодня. Свяжитесь с нами для оценки вашего проекта — мы подготовим план работ и примерные сроки. Если вы хотите ускорить SELECT запросы в 100 раз, начните с аудита.