【Laravel】メモリ消費を抑える!cursorメソッドとは

Laravel

こんにちは!

今回はLaravelでのデータ取得時のメモリ消費を抑える、「cursorメソッド」のご紹介をしていきます。

Laravelで何かデータ一覧を取得したいとき、どのように実装しているでしょうか?getメソッド?allメソッド?

実はよく使われるこれらのメソッドは、メモリ消費量が多いんです

それは一体なぜなのか?また、cursorメソッドを使うことでどれくらいメモリ消費を抑えることができるのか、について実測結果を元に紹介していきます。

読者
読者

「Laravelのcursorメソッドについて知りたい」

「Laravelでメモリ消費量を抑える方法を知りたい」

このようにお考えの方は参考になるのではないかと思います。

それでは、早速見ていきましょう!

cursorメソッドの使い方

cursorメソッドとは、Laravelに用意されているメソッドで、データ一覧を取得するときに使います。

使い方はgetメソッドやallメソッドと同じです。

$users = User::cursor();

これでユーザー一覧が取得できます。

このメソッドは、主にデータ量が多くメモリ消費が激しい処理で、メモリ消費を抑える目的で使われることが多いです。

ただし、cursorメソッドにはデメリットもあります。ここで、メリットとデメリットを整理してみます。

メリット

  • メモリ消費量を抑えられる

デメリット

  • 速度が遅い
  • Eagerローディングを併用できない

Eagerローディングとは、モデルのデータを効率的に取得するためのLaravelの機能です。主に、N+1問題を解消するために導入されています。

N+1問題に関してわからない方は、以下の記事が参考になります。

【Laravel】N+1問題を解消する方法

通常はN+1問題を解消するには、withメソッドを使うのですが、cursorメソッドを使用する場合は併用できません。

そこで、cursorメソッドを使用しているところでN+1問題が発生している場合は、joinやleftJoinを使うとよいでしょう。

getメソッドとcursorメソッドの違い

では、なぜgetメソッドなどとの違いが生まれるのか、について詳しくみていきます。

結論から言うと、getメソッドは全件データを一括でstdClassオブジェクトからEloquent Modelオブジェクトに変換する(ハイドレートする)のに対して、cursorメソッドは一件ずつ変換しています。

そうすると、当然一括で全データを変換する方がメモリ消費が激しくなるわけですね。ただし、cursorメソッドの場合は1件ずつ変換しているので、代わりに時間がかかってしまうわけです。

少し解説を加えると、Laravelではデータを取得した時点ではstdClassオブジェクトとして取り出されます。

stdClassオブジェクトというのは、プロパティもメソッドも何も持っていないオブジェクトです。通常はこれをEloquentオブジェクトに変換することで、モデル操作ができるようにしています。この変換することを「ハイドレート」と言います。

図にすると、以下のようになります。

SELECT実行

PHPメモリにレコードデータを保存

PDOStatement::fetchバッファからfetchする

stdClassオブジェクト

ハイドレート

Eloquentオブジェクト

このような仕組みから、getメソッドやallメソッドとcursorメソッドの挙動に違いが生まれています。

実測結果

それでは、cursorメソッドを使った場合に、実際にどれくらいメモリ消費を抑えることができるのかや、どれくらい速度が遅くなるのかについてみていきます。

まず、事前準備として、ユーザーデータを10,000件用意しました。

Laravelでコマンドを作成して、以下のようなコードで試していきます。

public function handle()
{
    $startTime = microtime(true);
    $initialMemory = memory_get_usage();

    $users = User::all();
    dump('ユーザー件数: ' . $users->count());

    $runningTime =  microtime(true) - $startTime;
    $usedMemory = (memory_get_peak_usage() - $initialMemory) / (1024 * 1024);

    dump('running time: ' . $runningTime . ' [s]'); // or var_dump()
    dump('used memory: ' . $usedMemory . ' [MB]'); // or var_dump()

    return Command::SUCCESS;
}

次に、取得部分をcursorメソッドに切り替えてみます。

$users = User::cursor();

実行結果は、以下のようになりました。

取得件数取得方法速度(s)メモリ消費量(MB)
10,000件allメソッド0.106223.4918
10,000件cursorメソッド0.16093.480
allメソッドとcursorメソッド比較

速度は1.6倍程度になってしまっていますが、メモリ消費量はかなり抑えられていますね!

結論として、速度を多少犠牲にしてもメモリ消費を抑えたい時に使うのがベストかなというところですね。皆さんも意識して使ってみましょう。

コメント

タイトルとURLをコピーしました