Обидва фільтрують за пов'язаною таблицею, але роблять це по-різному.
whereHas() будує підзапит EXISTS:
$posts = Post::whereHas('comments', function ($query) {
$query->where('is_approved', true);
})->get();
Модель повертається одна й без дублікатів, зв'язок не витягується. Для простої перевірки «чи є хоч один» є коротший has('comments'), а для кількості - has('comments', '>=', 3).
join() зшиває таблиці в одному запиті:
$posts = Post::join('comments', 'comments.post_id', '=', 'posts.id')
->where('comments.is_approved', true)
->select('posts.*')
->distinct()
->get();
Різниця, яка вирішує:
joinмножить рядки: пост із десятьма коментарями повернеться десять разів, тому потрібенdistinct()абоgroupBy- і разом з ними падає перевага в швидкості.joinдає доступ до колонок другої таблиці - сортувати чи вибирати за ними можна тільки так.whereHasчитається ближче до наміру й не ламає зв'язки моделі.
Про продуктивність. Поширене «join завжди швидший» невірне: сучасні планувальники виконують EXISTS ефективно й часто зупиняються на першому збігу, тоді як join матеріалізує всі пари. Різниця залежить від індексів і селективності - міряйте EXPLAIN, а не вгадуйте.
Практичне правило: фільтруєте за наявністю - whereHas; потрібні колонки пов'язаної таблиці для вибірки чи сортування - join.