Как лучше использовать join для выборки связанных данных в SQL запросах
Что проверяют
Умеете ли вы получать связанные данные одним запросом без размножения строк и без проблемы N+1, которая в Python-проектах почти всегда приходит из ORM. Следом часто дают связь «один ко многим» и просят посчитать сумму так, чтобы она не удвоилась.
Как отвечать
Коротко по типам, с акцентом на выбор:
INNER JOIN: только строки, у которых есть пара в обеих таблицах;LEFT JOIN: все строки слева, справаNULL, если пары нет. Основной инструмент для задач «покажи всех, даже без заказов»;FULL JOIN: все строки обеих сторон, без пары дополненыNULL. Нужен редко, в основном для сверки данных;EXISTSвместо JOIN: если из второй таблицы нужен только факт наличия, полусоединение не размножает строки и не требуетDISTINCT.
Главное про «один ко многим»: JOIN умножает строки. Если к заказам присоединить сразу позиции и оплаты, сумма оплат посчитается столько раз, сколько позиций в заказе. Решение: сначала агрегировать каждую сторону в подзапросе или CTE, потом соединять.
Производительность проверяйте через EXPLAIN ANALYZE: видно, какой алгоритм выбран (Nested Loop, Hash Join, Merge Join) и где оценка строк расходится с фактом. В PostgreSQL внешний ключ не создаёт индекс на ссылающейся колонке, его добавляют сами.
Обязательно свяжите с ORM. В Django select_related делает JOIN и подходит для ForeignKey, а prefetch_related делает второй запрос с IN и нужен для связей «ко многим». В SQLAlchemy аналоги joinedload и selectinload. Без них цикл по объектам даёт отдельный запрос на каждую строку.
Где спотыкаются
Ставят условие на правую таблицу в WHERE после LEFT JOIN. Строки с NULL отсекаются, и соединение молча становится внутренним. Такое условие переносят в ON.
Лечат дубли через DISTINCT, не разобравшись, откуда они. Это прячет ошибку в логике и добавляет сортировку всего результата.
Берут joinedload для коллекций на тысячи элементов. Строка родителя повторяется для каждого потомка, и отдельный запрос через selectinload выходит дешевле.