Почему нейросетям нельзя доверять настройку производительности PostgreSQL
Сравнивается эффективность оператора JOIN и коррелированного подзапроса в PostgreSQL в условиях высокой параллельной нагрузки. Экспериментальные данные опровергают универсальные рекомендации нейросетевых моделей и показывают, какие факторы на самом деле определяют производительность.
PG_EXPECTO (kznalp/PG_EXPECTO), доступным в репозиториях GitFlic и GitHub.Вопрос простой: какой паттерн выбрать при высокой параллельной нагрузке на СУБД — JOIN или коррелированный подзапрос? Обе нейросети, которым его задали, ответили одинаково уверенно и одинаково однозначно.
JOIN. Коррелированные подзапросы с агрегациями названы плохой практикой в сценариях с высокой конкуренцией.LEFT JOIN и GROUP BY.Сравнивались два запроса, возвращающие один и тот же результат: количество заказов для каждого клиента.
JOINSELECT 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
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
При параллельном выполнении измерялась операционная скорость СУБД. Среднее снижение операционной скорости на варианте с JOIN составило 288 % относительно коррелированного подзапроса — то есть подзапрос оказался почти вчетверо быстрее на всём протяжении теста.
График изменения операционной скорости в ходе нагрузочного тестирования.
Обе линии практически горизонтальны — вариант с подзапросом стабильно держится в районе 6,6–7,8 млн, вариант с JOIN — около 1,72–1,79 млн. Разрыв не сокращается с ростом числа точек наблюдения, а к концу теста даже увеличивается: подзапрос ускоряется примерно с 61-й точки, тогда как JOIN медленно проседает.
Seq Scan on orders — полное сканирование таблицы. Подзапрос идёт по Bitmap Index Scan — точечный доступ по индексу. Умножьте это на число сессий: в первом случае получается N полных сканирований одной таблицы, во втором — равномерно распределённое чтение.orders, создавая contention на буферный кэш и подсистему ввода-вывода. При подзапросе каждая сессия работает со своей частью индекса — конкуренция ниже, параллелизм лучше.Memory Usage: 40 kB на сессию), подзапрос расходует память точечно на каждого клиента. При множестве сессий JOIN заметно давит на shared_buffers.| Фактор | Почему он решает исход |
|---|---|
Влияние на shared_buffers | Множественные последовательные сканирования вытесняют из кэша полезные данные |
| Lock contention | Сессии конкурируют за одни и те же ресурсы и блокировки |
| Распределение ввода-вывода | Индексные чтения распределяются по диску равномернее, чем полные сканирования |
| Конфигурация сервера | work_mem, shared_buffers, random_page_cost и другие параметры меняют результат |
Обучающие данные моделей — это преимущественно синтетические и упрощённые сценарии:
На таком материале и вырабатывается универсальное правило «JOIN всегда лучше», которое перестаёт работать в реальных условиях высокой конкуренции.
| Вопрос | Ответ |
|---|---|
| Что советовали нейросети? | Всегда использовать JOIN вместо коррелированного подзапроса |
| Что показал эксперимент? | Под параллельной нагрузкой JOIN уступил подзапросу в среднем на 288 % |
| В чём причина? | Полное сканирование таблицы всеми сессиями против распределённого доступа по индексу |
| Почему совет был дан? | Общая эвристика вместо анализа плана и условий эксплуатации |
| Что делать вместо этого? | Читать план выполнения и проводить нагрузочное тестирование на своих данных |
EXPLAIN (ANALYZE) и нагрузочный тест в своей среде: план выполнения и поведение под конкуренцией — единственный источник истины о производительности.