過去の記事でも紹介されていますが、親子(or 1対多)関係にあるModelにおいて、「1つ以上子を持つ親」などの条件で絞り込む際にhas()は便利です。
しかし、has()を使わずともjoinSub()でサブクエリを指定して同じ条件で絞り込む事もできそうです。
どちらを使うのがベターなのか?気になったので調べてみました。
has()とjoinSub()の違い
例えば、店(Shop)と商品(Product)というModelが有った場合、「1つ以上商品を持つ店」はそれぞれ以下で絞り込むことができます。
// has()で絞り込み
$shops = Shop::has('product')->get();
// joinSub()で絞り込み
$sub = Product::distinct()->select('shop_id');
$shops = Shop::joinSub($sub, 'sub', function ($join) {
$join->on('sub.shop_id', '=', 'shop.shop_id');
})->get();
見ての通り、joinSub()の方が冗長ですね。
発行されたsqlはそれぞれ以下になります。
# has()のsql select * from `shop` where exists (select * from `product` where `shop`.`shop_id` = `product`.`shop_id`); # joinSub()のsql select * from `shop` inner join (select distinct `shop_id` from `product`) as `sub` on `sub`.`shop_id` = `shop`.`shop_id`;
has()ではEXISTS句で相関サブクエリを使用して絞り込んでいます。
相関サブクエリとは
mysqlの公式ドキュメントからの引用です。
相関サブクエリは、外部クエリにも現れるテーブルへの参照を含むサブクエリです。
前述のhas()のsqlでは、外部クエリのFROMに指定しているshopをサブクエリ内で参照している為、相関サブクエリと言えます。
相関サブクエリの注意点
レコード数が多いテーブルに紐づいたModelにてhas()使う際は注意が必要です。
相関サブクエリでは外部クエリの結果行数分、比較処理が行われる可能性があり、結果行数が多ければ遅くなる為です。
例えば前述のhas()のsqlでは、shopのレコード数 x productのレコード数分の比較処理が走ることになります。
(EXISTS句では条件にヒットする行が見つかればその時点でtrueを返し、次のレコードの比較処理に移るため、あくまでも全件ヒットしなかった場合)
とはいえ、それをjoinSub()に置き換えた場合もGROUP BYやDISTINCTで重複を削除する処理のコストが有るため、一概にどちらが速いと言い切れません。
実際に実装する環境でテストしてみるのが良いでしょう。
今回の実装環境ではhas()でもjoinSub()でも速度に大きな違いはありませんでした。
であればよりシンプルに記述できるhas()がベターと考えてそちらを採用することにしました。
