[パフォーマンス改善]LaravelのwhereIn()をgroupBy()で改善
こんにちは。レバテック開発部のきょうかです。
レバテックの社内業務用システムの保守開発を担当しています。
今回、担当しているシステムの中で、実行頻度が高く、処理時間が長い処理のパフォーマンスを改善しました。
その過程で学んだことを以下にまとめます。
修正内容
修正箇所の処理内容
data1とdata2を取得し、data1に紐づくdata2をリスト化して返却する処理を行っていました。
修正の概要
ループ内で繰り返し実行されていた whereIn() を削除し、事前に groupBy() を使ってデータをインデックス化することで、パフォーマンスを大幅に向上させました。
具体的なコードは以下の通りです。
修正前
/*
 * ※前提
 * data1とdata2を取得する処理は割愛しています
 * $data1_collection, $data2_collectionは、1万件ほどのレコードを持つCollection
 * $data1_collection = Collection (['data1_id' => 1, 'data2_ids' => 123],...)
 * $data2_collection = Collection (['id' => 1],...)
 */
return $data1_collection->map(function($data) use ($data2_collection) {
    // data2_idsに含まれるidのdata2を取得
    return $data2_collection->whereIn('id', $data->get('data2_ids'))->first();
});
- メモリ:18.3 MB
 - 実行時間:24秒
 
修正後
$grouped_data2 = $data2_collection->groupBy('id');
return $data1_collection->map(function($data) use ($grouped_data2) {
    // data2_idsに含まれるidのdata2を取得
    return array_map(function ($data2_id) use ($grouped_data2) {
        return $grouped_data2->get($data2_id);
    }, $data->get('data2_ids'));
});
- メモリ:22.06 MB
 - 実行時間:0.03秒!
 
詳細
修正内容の詳細を見ていきます。
修正前:whereInの場合
whereIn()は、LaravelのCollectionのメソッドです。
メソッドの中身を見てみましょう。
public function whereIn($key, $values, $strict = false)
{
    $values = $this->getArrayableItems($values);
    return $this->filter(fn ($item) => in_array(data_get($item, $key), $values, $strict));
}
コレクション全体をfilter()で走査していることがわかります。
$data1_collection->map()の中で毎回whereIn()を呼び出しているため、$data1_collectionのデータ数だけ$data2_collectionをフルスキャンしていることになります。
これによって実行時間が長くなっているようでした!
修正後:groupByの場合
事前にgroupBy()を使って、$data2_collectionのidをインデックス化しました。
これにより$data1_collection->map()内での処理速度が速くなっています。
ただし$grouped_data2というコレクションを新たに生成しているので、その分メモリ使用量が若干増加します。
学んだこと
ループ内でのwhereIn()には注意
毎回コレクション全体を走査する処理は非常に遅く、パフォーマンスのボトルネックになりやすいです。
メモリ使用量が低い=パフォーマンス最適とは限らない
メモリ使用量を最小限に抑えることは重要ですが、処理速度が最優先です。
メモリ消費を抑えながらもパフォーマンスを大幅に向上させる方法を都度選択できるようになりたいですね。
補足
インデックス化が目的であれば、keyBy()も有効です。
keyBy()の場合はメモリ:19.08 MB、実行時間:0.03秒となり、groupBy()よりもメモリを抑えられます。
今回はリファクタリングの都合上groupBy()を使いましたが、特に制約がなければkeyBy()の方が効率的な選択肢かもしれません!
感想
ループ系処理はデータ数や処理内容に合わせて都度最適なやり方を選びたいですね。
以下も調べてみたのでいつかまた記事にします。
- 配列操作系のメソッド(array_map()やarray_filter()など)とforeach()のメモリ使用量比較
 - Collectionと配列を比較したいとき、Collection化(collect())する場合と配列化(toArray())する場合の比較
 
Discussion