ИИ как опасный советчик

Почему нейросетям нельзя доверять настройку производительности PostgreSQL

Нейросеть видит паттерны, но не чувствует боль базы данных.

📄 Аннотация

Сравнивается эффективность оператора JOIN и коррелированного подзапроса в PostgreSQL в условиях высокой параллельной нагрузки. Экспериментальные данные опровергают универсальные рекомендации нейросетевых моделей и показывают, какие факторы на самом деле определяют производительность.

Инструментарий: статистический анализ, нагрузочное тестирование и построение отчётов выполнены открытым инструментом PG_EXPECTO (kznalp/PG_EXPECTO), доступным в репозиториях GitFlic и GitHub.

🎯 Постановка задачи

Вопрос простой: какой паттерн выбрать при высокой параллельной нагрузке на СУБД — JOIN или коррелированный подзапрос? Обе нейросети, которым его задали, ответили одинаково уверенно и одинаково однозначно.

Ask Postgres
Для нагрузочных тестов с растущей параллельностью всегда использовать версию с JOIN. Коррелированные подзапросы с агрегациями названы плохой практикой в сценариях с высокой конкуренцией.
DeepSeek
Для данного сценария производительность будет выше при использовании запроса с LEFT JOIN и GROUP BY.
Совет звучит как знание, но по своей природе это статистика формулировок из документации и статей — а не наблюдение за конкретным сервером под конкретной нагрузкой.

🧪 Детали эксперимента

Сравнивались два запроса, возвращающие один и тот же результат: количество заказов для каждого клиента.

Вариант 1 — JOIN

SELECT c.customer_id, COUNT(o.order_id) AS orders_count
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id;

План выполнения:

HashAggregate  (cost=35.85..37.25 rows=140 width=12) (actual time=0.622..0.629 rows=25 loops=1)
  Group Key: c.customer_id
  Batches: 1  Memory Usage: 40kB
  ->  Hash Right Join  (cost=13.15..30.85 rows=1000 width=8) (actual time=0.077..0.429 rows=1000 loops=1)
        Hash Cond: (o.customer_id = c.customer_id)
        ->  Seq Scan on orders o  (cost=0.00..15.00 rows=1000 width=8) (actual time=0.035..0.148 rows=1000 loops=1)
        ->  Hash  (cost=11.40..11.40 rows=140 width=4) (actual time=0.028..0.028 rows=25 loops=1)
              Buckets: 1024  Batches: 1  Memory Usage: 9kB
              ->  Seq Scan on customers c  (cost=0.00..11.40 rows=140 width=4) (actual time=0.018..0.021 rows=25 loops=1)

Planning Time: 0.221 ms
Execution Time: 0.787 ms

Вариант 2 — коррелированный подзапрос

SELECT c.customer_id,
       (SELECT COUNT(o.order_id)
          FROM orders o
         WHERE o.customer_id = c.customer_id) AS orders_count
FROM customers c;

План выполнения:

Seq Scan on customers c  (cost=0.00..1015.20 rows=140 width=12) (actual time=0.093..0.614 rows=25 loops=1)
  SubPlan 1
    ->  Aggregate  (cost=7.16..7.17 rows=1 width=8) (actual time=0.023..0.023 rows=1 loops=25)
          ->  Bitmap Heap Scan on orders o  (cost=1.56..7.06 rows=40 width=4) (actual time=0.007..0.017 rows=40 loops=25)
                Recheck Cond: (customer_id = c.customer_id)
                Heap Blocks: exact=125
                ->  Bitmap Index Scan on idx_orders_customer_id  (cost=0.00..1.55 rows=40 width=0) (actual time=0.004..0.004 rows=40 loops=25)
                      Index Cond: (customer_id = c.customer_id)

Planning Time: 0.145 ms
Execution Time: 0.718 ms
На одиночном запуске разница почти незаметна: 0.787 мс против 0.718 мс. Именно на таких «однопоточных» замерах и строятся общие рекомендации — а вся разница проявляется только под нагрузкой.

Результат нагрузочного тестирования

При параллельном выполнении измерялась операционная скорость СУБД. Среднее снижение операционной скорости на варианте с JOIN составило 288 % относительно коррелированного подзапроса — то есть подзапрос оказался почти вчетверо быстрее на всём протяжении теста.

8 000 000 7 000 000 6 000 000 5 000 000 4 000 000 3 000 000 2 000 000 Операционная скорость 1 21 41 61 81 101 Точка наблюдения SPEED-JOIN SPEED-SUBQUERY

График изменения операционной скорости в ходе нагрузочного тестирования.

Обе линии практически горизонтальны — вариант с подзапросом стабильно держится в районе 6,6–7,8 млн, вариант с JOIN — около 1,72–1,79 млн. Разрыв не сокращается с ростом числа точек наблюдения, а к концу теста даже увеличивается: подзапрос ускоряется примерно с 61-й точки, тогда как JOIN медленно проседает.


🔍 Почему рекомендации оказались неверны

1
Статические эвристики вместо анализа плана
«JOIN обычно эффективнее подзапросов», «избегайте N+1», «коррелированные подзапросы плохо масштабируются» — верные в среднем правила, применённые без разбора конкретного плана выполнения и без учёта конкуренции за ресурсы.
2
Игнорирование паттерна доступа к данным
JOIN идёт по Seq Scan on orders — полное сканирование таблицы. Подзапрос идёт по Bitmap Index Scan — точечный доступ по индексу. Умножьте это на число сессий: в первом случае получается N полных сканирований одной таблицы, во втором — равномерно распределённое чтение.
3
Неучёт блокировок и конфликтов ресурсов
При JOIN все сессии одновременно читают одни и те же страницы orders, создавая contention на буферный кэш и подсистему ввода-вывода. При подзапросе каждая сессия работает со своей частью индекса — конкуренция ниже, параллелизм лучше.
4
Разный профиль использования памяти
JOIN держит хэш-таблицу (Memory Usage: 40 kB на сессию), подзапрос расходует память точечно на каждого клиента. При множестве сессий JOIN заметно давит на shared_buffers.

⚙️ Что модель не видит в принципе

ФакторПочему он решает исход
Влияние на shared_buffersМножественные последовательные сканирования вытесняют из кэша полезные данные
Lock contentionСессии конкурируют за одни и те же ресурсы и блокировки
Распределение ввода-выводаИндексные чтения распределяются по диску равномернее, чем полные сканирования
Конфигурация сервераwork_mem, shared_buffers, random_page_cost и другие параметры меняют результат

Откуда берётся ошибка

Обучающие данные моделей — это преимущественно синтетические и упрощённые сценарии:

На таком материале и вырабатывается универсальное правило «JOIN всегда лучше», которое перестаёт работать в реальных условиях высокой конкуренции.


📋 Заключение

ВопросОтвет
Что советовали нейросети?Всегда использовать JOIN вместо коррелированного подзапроса
Что показал эксперимент?Под параллельной нагрузкой JOIN уступил подзапросу в среднем на 288 %
В чём причина?Полное сканирование таблицы всеми сессиями против распределённого доступа по индексу
Почему совет был дан?Общая эвристика вместо анализа плана и условий эксплуатации
Что делать вместо этого?Читать план выполнения и проводить нагрузочное тестирование на своих данных
Вывод: рекомендация нейросети — это гипотеза, а не результат. Проверяйте её через EXPLAIN (ANALYZE) и нагрузочный тест в своей среде: план выполнения и поведение под конкуренцией — единственный источник истины о производительности.