Рабочий процесс тюнинга
Систематический цикл: найти медленный запрос через pg_stat_statements, воспроизвести через EXPLAIN (ANALYZE, BUFFERS), диагностировать настоящую причину, применить наименьший фикс, проверить, повторить. Измеряй, не гадай.
Два инженера получают один тикет: «приложение тормозит». Первый открывает кодовую базу и начинает вешать индексы на каждую колонку, которую трогает WHERE — через три деплоя не быстрее, а записи теперь медленнее. Второй запускает один запрос к pg_stat_statements, находит, что 78% общего времени БД — это один отчётный запрос, делает EXPLAIN, видит промах оценки, запускает ANALYZE и закрывает тикет за двадцать минут. Потолок навыка тот же, исход противоположный. Разница — это цикл, а не трюк. Этот урок — цикл, связывающий весь раздел воедино.
Шаг 1 — найди запрос, который реально важен
Нельзя тюнить то, что не измерил, а интуиция про «медленный запрос» почти всегда неверна. Первый ход — pg_stat_statements — расширение, агрегирующее каждый выполненный запрос (нормализованный, со снятыми литералами) и отслеживающее его суммарную стоимость. Важное число — total_exec_time, а не mean_exec_time: запрос на 5 мс, выполненный 2 миллиона раз, стоит больше общего времени БД, чем 4-секундный отчёт, запущенный дважды, и срезание первого экономит больше.
-- Запросы, съедающие больше всего общего времени на сервере:
SELECT
substring(query, 1, 60) AS query,
calls,
round(total_exec_time) AS total_ms,
round(mean_exec_time, 2) AS mean_ms,
round(100 * total_exec_time / sum(total_exec_time) OVER (), 1) AS pct
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;Читай колонку pct: тюнинг это Парето. Если один запрос — 78% общего времени, это весь твой день; остальное — шум, пока не станет важным. Сортируй по total_exec_time, бери верхний, игнорируй всё остальное.
Шаги 2–4 — воспроизведи, диагностируй, почини наименьшее
Когда у тебя есть тот самый запрос, цикл механистичен:
- Воспроизведи через
EXPLAIN (ANALYZE, BUFFERS)(урок 01). Это истина в последней инстанции — никогда не тюнь по одному тексту запроса. - Диагностируй, сопоставив улики с одной корневой причиной (весь раздел, сжато):
- Большой зазор оценка-против-факта по строкам? → устаревшая или предполагающая независимость статистика →
ANALYZEилиCREATE STATISTICS(урок 03). - Seq Scan на селективном фильтре, высокие
read=буферы? → отсутствующий индекс → добавь (CONCURRENTLY), по главе индексов трека databases. - Живых строк мало, но буферы/размер огромны? → bloat → вакуум/
pg_repack(урок 04). - Оценки в порядке, скан верный, всё равно медленно? → данных правда слишком много → перепиши, пагинируй или предагрегируй.
- Большой зазор оценка-против-факта по строкам? → устаревшая или предполагающая независимость статистика →
- Примени наименьший фикс, адресующий эту причину. Одно изменение. Точечный
ANALYZEлучше нового индекса; новый индекс лучше переписывания; переписывание лучше изменения конфига — в этом порядке локальности, ведь наименьшее изменение имеет наименьший радиус поражения. - Проверь, перезапустив
EXPLAIN (ANALYZE, BUFFERS)и убедившись, что число сдвинулось. Затем пересмотриpg_stat_statementsпосле реального трафика — иногда фикс смещает узкое место на следующий запрос, и цикл повторяется.
Почему «измеряй, не гадай» — это вся дисциплина
Каждый анти-паттерн тюнинга — это догадка, пропустившая шаг измерения:
- Преждевременная индексация — добавление индексов «на всякий случай» до того, как хоть один запрос доказал, что нужен. Каждый индекс облагает налогом каждую запись (глава индексов трека databases это количественно оценивает) и может вводить планировщик в заблуждение; неиспользуемый индекс — чистая цена. Добавляй индекс только после того, как
EXPLAINпокажет Seq Scan, который он уберёт. - Карго-культ конфига — копирование значений
random_page_costилиwork_memиз блога без измерения своей нагрузки. - Слепое переписывание — джуниор из хука переписал SQL трижды, потому что текст казался медленным, когда реальной причиной была устаревшая оценка, которую переписывание не могло тронуть.
Сеньорский рефлекс обратен: каждому изменению предшествует измерение, его оправдывающее, и следует измерение, его подтверждающее. Это и заставляет цикл сходиться, а не молотить впустую. (Капстоун раздела 09 прогоняет этот самый цикл от начала до конца на реалистичном аналитическом API — этот урок — метод, который он применяет.)
▸Почему это работает
Почему сортировать по total_exec_time, а не mean_exec_time? Потому что ты оптимизируешь бюджет времени сервера, а общее время — это то, что пользователи коллективно ждут. Запрос со средним 2 мс, но вызываемый 50 миллионов раз в день, жжёт куда больше совокупного времени — и больше конкуренции за блокировки, больше оборота буферов, больше CPU — чем 30-секундный месячный отчёт. Починка запроса с 2 мс до 1 мс экономит больше реального времени по всем пользователям, чем вдвое срезанный отчёт. mean_exec_time важен для SLA латентности одного пользовательского запроса, но для «где база проводит свою жизнь?» доминирует total. Используй обе линзы: total — найти системную цену, mean — защитить конкретную интерактивную точку.
Расставь систематический цикл тюнинга для тикета «приложение тормозит»:
- 1 Запроси pg_stat_statements; сортируй по total_exec_time, чтобы найти самый дорогой запрос
- 2 Воспроизведи этот запрос через EXPLAIN (ANALYZE, BUFFERS)
- 3 Диагностируй единственную корневую причину: промах оценки, отсутствующий индекс, bloat или слишком много данных
- 4 Примени наименьший фикс, адресующий эту причину (например, ANALYZE до добавления индекса)
- 5 Проверь, перезапустив EXPLAIN и убедившись, что число реально сдвинулось
- 6 Пересмотри pg_stat_statements после трафика; вернись к новому верхнему запросу
В pg_stat_statements какая колонка говорит, где база тратит больше всего времени в целом?
EXPLAIN ANALYZE показывает большой зазор оценка-против-факта и nested loop. Какой наименьший верный первый фикс?
- 01Каков первый шаг цикла тюнинга и по какой колонке pg_stat_statements сортировать и почему?
- 02Имея EXPLAIN ANALYZE, как сопоставить улики с наименьшим фиксом?
- 03Почему «измеряй, не гадай» — ядро дисциплины и какие анти-паттерны это предотвращает?
Тюнинг — это цикл, и его прогон отличает сеньорскую работу от барахтанья. Найди запрос, который реально важен, через pg_stat_statements, сортируя по total_exec_time — тюнинг это Парето, и дешёвый запрос, выполненный миллионы раз, перевешивает редкий дорогой. Воспроизведи его через EXPLAIN (ANALYZE, BUFFERS); никогда не тюнь по тексту. Диагностируй единственную корневую причину, читая улики — большой зазор оценка-против-факта значит статистику (урок 03), Seq Scan на селективном фильтре значит отсутствующий индекс, мало живых строк в огромной таблице значит bloat (урок 04), а чистый план, всё равно медленный, значит данных правда слишком много. Примени наименьший фикс для этой причины, в порядке локальности — ANALYZE до индекса до переписывания до конфига — ведь наименьшее изменение имеет наименьший радиус поражения. Проверь, что число сдвинулось, затем вернись в начало, ведь фикс часто продвигает следующий запрос наверх. Связующая нить — измеряй, не гадай: каждому изменению предшествует измерение, его оправдывающее, и следует измерение, его подтверждающее. Теперь, когда приходит тикет «приложение тормозит», ты знаешь: первый запрос, который нужно запустить, — не в кодовой базе, а к pg_stat_statements.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.