Как декартово произведение влияет на производительность запросов к базе данных?
Что проверяют
Понимаете ли вы, откуда в запросе берётся взрыв строк и почему его не лечат индексы. Для Java-разработчика вопрос почти всегда уходит в ORM: следом спрашивают про JOIN FETCH нескольких коллекций в Hibernate.
Как отвечать
Декартово произведение соединяет каждую строку одной таблицы с каждой строкой другой: 1000 клиентов на 2000 заказов дают 2 000 000 строк. Стоимость растёт от объёма результата: база перебирает n × m пар, держит их в памяти или сбрасывает во временные файлы, гонит по сети, а драйвер JDBC и маппер создают объект на каждую строку.
Откуда оно берётся на практике:
- забытое условие соединения: старый синтаксис
FROM a, bбезWHEREили явныйCROSS JOIN; - соединение по неуникальному ключу: условие есть, но значения повторяются с обеих сторон, и строки размножаются;
- несколько связей один-ко-многим в одном запросе: заказ с 10 позициями и 5 платежами превращается в 50 строк, а
SUMпо платежам тихо умножается на 10; - Hibernate:
JOIN FETCHдвух коллекций типаListпадает сMultipleBagFetchException. Замена наSetубирает исключение, но само произведение остаётся.
Как исправлять: загружать коллекции отдельными запросами, использовать @BatchSize или FetchMode.SUBSELECT, агрегировать в подзапросе до соединения. Проверять через EXPLAIN ANALYZE, сравнивая ожидаемое и фактическое число строк на узлах плана.
Осознанное произведение бывает полезно: календарь дат на список магазинов даёт строки и для дней без продаж. Там обе таблицы маленькие, и результат нужен целиком.
Где спотыкаются
Говорят «добавим индекс». Индекс ускоряет поиск по условию, но без условия результат всё равно n × m строк.
Прячут дубли за DISTINCT. На экране данные выглядят правильно, но база по-прежнему строит произведение, а агрегаты уже посчитаны неверно.
Сводят тему к CROSS JOIN и не узнают ту же проблему в запросе, где все соединения формально написаны правильно.