Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHPerKaigi 2022 【Laravel】 サクッとN + 1問題を見つけて倒しチャオ!

y-tsuzaki
April 11, 2022

PHPerKaigi 2022 【Laravel】 サクッとN + 1問題を見つけて倒しチャオ!

N + 1問題とは、ループ処理の中で都度SQLを実行してしまうことで必要以上にSQLが発行されてしまい処理が遅くなってしまう問題です。
N + 1問題が存在していても、ユーザーに影響のない程度の処理時間なら改修する必要はありません。
しかしながら、データ量が増えたりページへのアクセスが増えることで後から問題になってしまうこともあります。
今回は、そんなN + 1問題をサクッと検出して、問題が発生する前にN + 1問題を撲滅する方法をご紹介します。

https://fortee.jp/phperkaigi-2022/proposal/4307d420-7ef0-4b69-b3f7-005b72bf87b0

y-tsuzaki

April 11, 2022
Tweet

More Decks by y-tsuzaki

Other Decks in Programming

Transcript

  1. Copyright© M&Aクラウド
    【Laravel】
    サクッとN + 1問題を見つけて倒しチャオ!
    PHPerKaigi 2022
    M&Aクラウド つざき

    View Slide

  2. Copyright© M&Aクラウド 2
    自己紹介
    つざき
    M&Aクラウド所属
    Laravel(PHP) / Nuxt.js (TypeScript)
    キャンプ/筋トレ/サウナ
    820zacky

    View Slide

  3. Copyright© M&Aクラウド
    N+1問題?
    3

    View Slide

  4. Copyright© M&Aクラウド
    DBアクセスが合計 N+1 回実行される問題です。
    4

    View Slide

  5. Copyright© M&Aクラウド
    DBアクセスは時間がかかるので
    データが何件でもDBアクセスはできるだけ少ない方が早

    N件ごとにN回DBアクセスがあると
    扱うデータが増えると処理時間が線形に伸びてしまう
    5

    View Slide

  6. Copyright© M&Aクラウド
    本来はデータがN件でも1回のクエリで済むところ
    データに付随する別のデータ取得がN回実行されて
    1 + N回のクエリになってしまった!!
    というのが「N+1問題」です!
    7

    View Slide

  7. Copyright© M&Aクラウド
    どんな時にN+1問題が起こる?
    - EloquentのリレーションによるN+1問題
    - Eloquent以外のN+1問題
    8

    View Slide

  8. Copyright© M&Aクラウド
    EloquentのリレーションによるN+1問題
    9

    View Slide

  9. Copyright© M&Aクラウド 10
    EloquentのリレーションによるN+1問題
    Eager(積極的)ロードでN+1問題を直します
    すべて「本」に紐づく「著者」をとってきたい場合
    $books = Book::all();
    foreach ($books as $book) {
    echo $book->author->name ;
    }
    何も工夫せず書くと すべての本取得に1クエリ実行
    1つの本に紐づく著者の取得に1クエリ実行。本が25個ある場合、25+1回クエリが実行される。
    with句を使えば2クエリで済むようになる。
    $books = Book::with('author')->get();
    foreach ($books as $book) {
    echo $book->author->name;
    }

    View Slide

  10. Copyright© M&Aクラウド 11
    EloquentのリレーションによるN+1問題
    ここで別の問題が……

    View Slide

  11. Copyright© M&Aクラウド 12
    EloquentのリレーションによるN+1問題
    with忘れがち🥺

    View Slide

  12. Copyright© M&Aクラウド 13
    EloquentのリレーションによるN+1問題
    そんなとき便利なのが
    preventLazyLoading

    View Slide

  13. Copyright© M&Aクラウド 14
    EloquentのリレーションによるN+1問題
    preventLazyLoadingはLaravel 8から追加された機能です。
    AppServiceProviderのboot()で呼び出しを追加します。
    use Illuminate¥Database¥Eloquent¥Model;
    public function boot()
    {
    Model::preventLazyLoading(! $this->app->isProduction());
    }
    このように設定することで実行時に例外を投げるようになるので、開発中にwithのつけ忘れに気づくことができます。

    View Slide

  14. Copyright© M&Aクラウド 15
    EloquentのリレーションによるN+1問題
    アプリケーションに途中から追加する場合、いちいち例外で停止されると面倒です。
    そんなときはwith漏れをログ出力させて後で一気に直しましょう!
    Model::handleLazyLoadingViolationUsing(function ($model, $relation) {
    $class = get_class($model);
    info("Attempted to lazy load [{$relation}] on model [{$class}].");
    });

    View Slide

  15. Copyright© M&Aクラウド
    Eloquent外でのN+1問題
    16

    View Slide

  16. Copyright© M&Aクラウド 17
    Eloquent外でのN+1問題
    - そもそもEloquent使ってないケース
    - 直接PHPでSQL書いてる
    - クエリビルダーを使っている
    - など
    - ドメイン層など別のレイヤーのループ処理でDBアクセスを行っているケース
    - など

    View Slide

  17. Copyright© M&Aクラウド 18
    Eloquent外でのN+1問題
    どうしたらよいか?

    View Slide

  18. Copyright© M&Aクラウド 19
    Eloquent外でのN+1問題
    たくさんSQLを実行している
    Featureテストを見つけチャオ!
    たくさんクエリ実行があるテストはN+1問題が存在する可能性があります。
    (テストで扱うデータ数が少ないと検知できないかもしれない)

    View Slide

  19. Copyright© M&Aクラウド 20
    Eloquent外でのN+1問題
    DB::listen()でクエリの実行をハンドルできるので、実行回数をカウントします。
    Featureテストの親クラスを作ってテスト前後に処理をはさむことで、カウント+ログ出力ができます。
    DB::listen(function () {
    $this->queryCount++;
    });

    View Slide

  20. Copyright© M&Aクラウド 21
    Eloquent外でのN+1問題
    サンプルコード
    abstract class MyTestCase extends TestCase
    {
    protected int $queryCount = 0;
    protected function setUp(): void {
    parent::setUp();
    DB::listen(function () {
    $this->queryCount++;
    });
    }
    protected function tearDown(): void {
    if ($this->queryCount > 50) {
    Log::warning("[WARNING] TOO MANY QUERY! query count: " . $this->queryCount . " " . get_class($this) . "#" . $this-
    >getName());
    }
    parent::tearDown();
    }
    }

    View Slide

  21. Copyright© M&Aクラウド
    まとめ
    preventLazyLoading

    テスト時のSQL実行数カウント
    を使って
    N+1問題をサクッと倒しチャオ!
    22

    View Slide

  22. Copyright© M&Aクラウド
    おわり
    23

    View Slide