過去の記事でも紹介されていますが、親子(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 BYDISTINCTで重複を削除する処理のコストが有るため、一概にどちらが速いと言い切れません。

実際に実装する環境でテストしてみるのが良いでしょう。

今回の実装環境ではhas()でもjoinSub()でも速度に大きな違いはありませんでした。
であればよりシンプルに記述できるhas()がベターと考えてそちらを採用することにしました。

By hikaru