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

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

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

Ea059a886741b21e8d1dd992129634f7?s=128

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クラウド つざき

  2. Copyright© M&Aクラウド 2 自己紹介 つざき M&Aクラウド所属 Laravel(PHP) / Nuxt.js (TypeScript)

    キャンプ/筋トレ/サウナ 820zacky
  3. Copyright© M&Aクラウド N+1問題? 3

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

  5. Copyright© M&Aクラウド DBアクセスは時間がかかるので データが何件でもDBアクセスはできるだけ少ない方が早 い N件ごとにN回DBアクセスがあると 扱うデータが増えると処理時間が線形に伸びてしまう 5

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

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

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

  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; }
  10. Copyright© M&Aクラウド 11 EloquentのリレーションによるN+1問題 ここで別の問題が……

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

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

  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のつけ忘れに気づくことができます。
  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}]."); });
  15. Copyright© M&Aクラウド Eloquent外でのN+1問題 16

  16. Copyright© M&Aクラウド 17 Eloquent外でのN+1問題 - そもそもEloquent使ってないケース - 直接PHPでSQL書いてる - クエリビルダーを使っている

    - など - ドメイン層など別のレイヤーのループ処理でDBアクセスを行っているケース - など
  17. Copyright© M&Aクラウド 18 Eloquent外でのN+1問題 どうしたらよいか?

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

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

    });
  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(); } }
  21. Copyright© M&Aクラウド まとめ preventLazyLoading と テスト時のSQL実行数カウント を使って N+1問題をサクッと倒しチャオ! 22

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