Планировщик всегда прав. Даже когда запрос тормозит
Как несправедливые обвинения в адрес детерминированного алгоритма мешают увидеть истинную причину проблем производительности PostgreSQL.
Материал подготовлен нейросетью.
Предисловие
В профессиональном обиходе администраторов баз данных существует устойчивое и почти ласковое ругательство: «планировщик ошибся». Когда запрос выполняется минуту вместо миллисекунды, рука так и тянется обвинить во всём алгоритм, перебирающий планы выполнения. Это успокаивает, ведь проще свалить вину на абстрактный «искусственный интеллект» СУБД, чем раскапывать пласты статистики и конфигурационных файлов. Однако, если на мгновение отключить эмоции и взглянуть на архитектуру PostgreSQL с холодной инженерной точки зрения, мы обнаружим, что планировщик — это образец математической честности. Цель этого эссе — доказать, что планировщик не ошибается никогда; он лишь безупречно отражает искажённую картину мира, которую мы, администраторы и разработчики, ему предоставили.
О непогрешимости калькулятора: Почему фраза «Планировщик ошибся» абсурдна с точки зрения архитектуры СУБД
В среде администраторов и разработчиков баз данных можно часто услышать сетование: «Планировщик снова ошибся и выбрал идиотский план запроса». Обычно за этим следует ручное манипулирование через pg_hint_plan или отключение определенных типов сканирования на уровне сессии. Это утверждение настолько укоренилось в профессиональном жаргоне, что стало восприниматься как аксиома.
Однако если рассмотреть это утверждение сквозь призму архитектурной логики PostgreSQL, оно оказывается не просто неточным, а фундаментально абсурдным. Оно приписывает математической машине качества человеческого мышления — склонность к просчетам, небрежность или недомыслие. На самом деле, планировщик PostgreSQL — это образец детерминированного алгоритма, и его «неправильный» выбор — это всегда кристально честное отражение несовершенства мира данных, который мы ему предоставили.
Часть 1. Планировщик как детерминированная функция
В основе работы планировщика лежит модель оценки стоимости (Cost Model). Это чистая математика. Получив на вход:
Дерево синтаксического разбора SQL.
Системные каталоги (pg_class, pg_statistic).
Конфигурационные константы (random_page_cost, effective_cache_size).
Алгоритм перебирает конечное число способов соединения таблиц и методов доступа к данным, вычисляя для каждого гипотетического плана безразмерную величину — стоимость. Эта стоимость выражается не в секундах или миллисекундах, а в условных единицах, где seq_page_cost = 1.0 означает «прочитать 8 КБ с диска».
Выбор плана происходит по жесткому правилу: выбирается вариант с минимальной расчетной стоимостью.
Детерминированность алгоритма подразумевает, что при абсолютно идентичных входных данных (та же статистика, та же конфигурация) планировщик всегда выдаст один и тот же план. Он не устает, у него не бывает плохого настроения, он не боится сложных подзапросов. Если в результате вычислений Total Cost плана «А» оказался 101.2, а плана «Б» — 102.0, планировщик не выберет план «Б» из спортивного интереса или из-за временного помутнения. Он выберет «А».
В этом контексте утверждение, что алгоритм ошибся в выборе, равносильно утверждению, что калькулятор ошибся при перемножении чисел. Калькулятор не ошибается, ошибается рука, нажавшая не на ту кнопку.
Часть 2. Эпистемологический разрыв: «Стоимость» против «Времени»
Если алгоритм безупречен, почему мы видим медленные запросы? Корень противоречия лежит в разнице между планируемой стоимостью (Cost) и наблюдаемой продолжительностью (Duration).
Планировщик живет в упрощенной, статической модели мира, которую создают для него команды ANALYZE и файл postgresql.conf. Пользователь живет в реальном мире с кэшированием файловой системы, механическими задержками дисков и стохастическим распределением данных.
Когда планировщик выдает план, который кажется абсурдным, он не «ошибается» — он добросовестно заблуждается, основываясь на неверных или неполных исходных данных.
Рассмотрим классические примеры этого заблуждения.
1. Ложь статистики (Stale Statistics)
Планировщик оценивает количество строк, которое вернет фильтр WHERE city = 'London', в 100 записей, потому что pg_statistic говорит, что таблица маленькая. На самом деле ночью была залита партиция с 50 миллионами записей, а autovacuum не успел обновить reltuples. Планировщик, веря в число 100, выбирает хрупкий Nested Loop. Алгоритм принял идеальное решение для таблицы в 100 строк. Проблема не в алгоритме. Проблема в том, что мы скормили ему ложную информацию.
2. Неучтенная корреляция (Hidden Dependencies)
В запросе WHERE country='USA' AND state='California'. Планировщик перемножает вероятности, считая, что это независимые события. Он получает долю 0.001% строк. На деле — 15% строк. Оценка кардинальности ошибается в 1000 раз. Планировщик математически корректно перемножил две дроби. Он не обязан знать семантику географических данных, если мы не создали объект CREATE STATISTICS.
3. Искажение модели cost (Misconfiguration)
Параметр random_page_cost установлен в 4.0 (классическое значение для вращающегося HDD). База данных физически расположена на быстром NVMe SSD. Планировщик, верный модели, считает, что прыгать по индексу в 4 раза дороже, чем читать все подряд. Поэтому он честно, неукоснительно следуя заданным параметрам, выбирает медленное Seq Scan. Виноват ли планировщик, что мы не соизволили сообщить ему о смене железа? Нет.
Часть 3. Логический абсурд персонификации
Фраза «планировщик ошибся» — это пример когнитивного искажения, известного как персонификация техники. Мы наделяем сложную программу человеческими чертами, чтобы нам было проще справляться с разочарованием от ее поведения. Гораздо проще сказать «он тупит», чем признать собственное упущение в администрировании (VACUUM не настроен) или недостаток проектирования схемы (отсутствие расширенной статистики).
С точки зрения кибернетики, планировщик — это идеальный исполнитель. Он подобен камертону. Если камертон выдает ноту «до», а вы хотите услышать «ми», камертон не сломан. Вы просто не настроили струну.
Заключение: Перекладывание ответственности
Итак, утверждение о том, что планировщик PostgreSQL «ошибся» в выборе плана, является логически несостоятельным. Оно противоречит детерминированной природе алгоритма. То, что мы воспринимаем как ошибку, является симптомом.
Симптомом того, что данные, переданные в планировщик, не соответствуют действительности. Планировщик не может ошибиться, потому что у него нет свободы воли, чтобы ошибаться. Он лишь вычисляет производную от предоставленной статистики и конфигурации.
Поэтому, сталкиваясь с «плохим» планом, корректнее говорить не «Планировщик выбрал неверный план», а «У планировщика сложилась неверная картина мира, и он действовал оптимально в рамках этой искаженной картины». Перефразируя классика: планировщик PostgreSQL не решает, как выполнить ваш запрос быстро; он решает, как выполнить ваш запрос наиболее эффективным способом в той вымышленной вселенной, которую вы построили для него статистикой и конфигурационными файлами.
Послесловие
В следующий раз, глядя на результат команды EXPLAIN ANALYZE и чувствуя подступающее раздражение от «глупого» Seq Scan, остановитесь и задайте себе два вопроса: «Когда в последний раз собиралась статистика?» и «Соответствует ли random_page_cost моему железу?». Планировщик PostgreSQL — это не предсказатель будущего и не хитрый противник, с которым нужно бороться хинтами. Это честное зеркало вашей базы данных. И если отражение вам не нравится, проблема не в зеркале.
