Корельований підзапит посилається на колонки зовнішнього запиту, тож логічно виконується для кожного рядка зовнішнього запиту:
SELECT u.id, u.name,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS orders_count,
(SELECT MAX(created_at) FROM orders o WHERE o.user_id = u.id) AS last_order_at
FROM users u;
Звичайний (некорельований) підзапит не залежить від зовнішнього і виконується один раз: WHERE id IN (SELECT user_id FROM vip_list).
Чим корельовані підзапити можуть бути погані: на 100 000 користувачів - 200 000 виконань підзапитів. З індексом на orders.user_id кожне швидке, але сумарно це багато роботи. Без індексу - катастрофа.
Як переписують:
-- JOIN з агрегованою похідною таблицею: orders проходиться один раз
SELECT u.id, u.name,
COALESCE(s.orders_count, 0) AS orders_count,
s.last_order_at
FROM users u
LEFT JOIN (
SELECT user_id, COUNT(*) AS orders_count, MAX(created_at) AS last_order_at
FROM orders
GROUP BY user_id
) s ON s.user_id = u.id;
Але не завжди варто переписувати:
- Оптимізатор MySQL сам перетворює багато підзапитів з
IN/EXISTSна semi-join і виконує їх як з'єднання. - Для сторінки з 20 рядків корельований підзапит виконається 20 разів - це дешевше, ніж агрегувати всю таблицю замовлень.
- Корельований
EXISTSз індексом зупиняється на першому знайденому рядку - часто найефективніший спосіб перевірки.
Правило: дивитися EXPLAIN і час на реальних даних. Підзапит у SELECT для кожного рядка великої вибірки - підозрілий; EXISTS у WHERE з індексом - зазвичай у порядку.
У Laravel withCount('orders') генерує саме корельований підзапит - для пагінованих списків це нормально.