Eloquent ORMのChunkとCursorについて調べた

前回書いた記事では、Eloquent ORMにおける参照系メソッドについて紹介しました。

blog.zuckey17.org

その中で、ChunkとCursorという項目について、

こちらについては、僕自信の理解が曖昧なため、もう少し調べる必要があると思います。 詳しく理解されている方がいらしたら、コメントで参考サイトなど教えていただきたいです。

と書いたところ

chunk, cursorは発行されるSQLを確認した方がいいと思います

というフィードバックをいただいたので、もう一度調べてみました。

※ 本エントリで利用しているコードはすべて

github.com

にあります。

目次

(もう一度)Chunk と Cursor

公式ドキュメント(和訳)によると以下のように書かれています。

ChunkとCursor

数1000行ものEloquentのレコードを処理する場合、chunkを利用します。chunkメソッドはEloquentのモデルを"塊"で取得し、それを引数として受け取ったクロージャに渡して処理をします。chunkメソッドを使うことによって、巨大なクエリ結果を扱うような処理において、メモリ使用量を防ぐことができます。 chunkメソッドの第1引数は取得する"塊"ごとのレコードの数です。第2引数として渡されるクロージャは、データベースから取得した塊ごとに呼ばれます。そのため、クロージャに"塊"を渡すたびに、毎度クエリが発行されます。

Cursorを使う

cursorメソッドはカーソルを利用して、ただ1回のクエリによってデータベースから取りだしたリストに対し1行ずつ処理を繰り返すことができます。大量のデータを処理する場合、cursorメソッドによってメモリ使用量をかなり削減することができます。

つまり、どちらのメソッドについても、大量のデータを取得する際にメモリ使用量を抑えるためのもののようです。

Eloquentで実際に発行されるSQL文を確認する

Eloquentではいくつか発行されるSQL文を調べる方法があります。

toSqlメソッド

最もシンプルな方法はtoSqlメソッドです。
getメソッドを呼ぶ代わりに、クエリビルダーのインスタンスに対してtoSqlメソッドを呼ぶことでSQL文を取得することができます。
例) php-sandbox/toSql.php at master · zuckeyM-17/php-sandbox · GitHub

<?php
$sql = Book::query()
    ->where('author', '=', '川原 礫')
    ->toSql();

echo 'Query: ' . $sql . PHP_EOL;

// Query: select * from "books" where "author" = ? and "books"."deleted_at" is null

結果から分かる通り、where句でauthorカラムが「川原 礫」さんで絞っていますが、出力されるSQL文の中では指定が?に変わっているのがわかると思います。 *1 これを回避できるのがもうひとつの方法です。

getQueryLogメソッド

もうひとつの方法は、getQueryLogメソッドです。1プロセスでこのメソッドが呼ばれるまでに実際に発行されたSQL文を出力することができます。
例)php-sandbox/getQueryLog.php at master · zuckeyM-17/php-sandbox · GitHub

<?php

$capsule->getConnection()->enableQueryLog();

$books = Book::query()
    ->where('author', '=', '川原 礫')
    ->get();

var_dump($capsule->getConnection()->getQueryLog());

/**
array(1) {
  [0]=>
  array(3) {
    ["query"]=>
    string(73) "select * from "books" where "author" = ? and "books"."deleted_at" is null"
    ["bindings"]=>
    array(1) {
      [0]=>
      string(12) "川原 礫"
    }
    ["time"]=>
    float(8.81)
  }
}
 */

getQueryLogメソッドは、事前にIlluminate\Database\Connectionクラスのインスタンスに対して、enableQueryLogメソッドを呼んでおく必要があります。
このIlluminate\Database\Connectionクラスは、Laravelフレームワーク内で利用している場合はDBファサード経由でアクセスができ、

DB::enableQueryLog()

と書くことができます。
本エントリではEloquentを単体で利用しているため、事前に用意したCapsuleインスタンスに対して、getConnectionメソッドを呼び、Illuminate\Database\Connectionインスタンスを取得しています。

※ またenableQueryLogをしてそのまま放置すると、そのプロセス中で呼ばれたクエリをすべて保持してしまうので、開発中無駄なSQL文の出力を避けるために、処理の最後にdisableQueryLogメソッドを呼ぶというように使うようです。

ChunkとCursorのSQLを比較する

Chunk

<?php
$capsule->getConnection()->enableQueryLog();

Book::chunk(2, function ($books) {
    foreach ($books as $book) {
        echo $book->name , "\n";
    }
});

$queryLog = $capsule->getConnection()->getQueryLog();

foreach ($queryLog as $i => $query) {
    echo 'Query' . ($i + 1) . ': ' . $query['query'] . PHP_EOL;
}

// Query1: select * from "books" where "books"."deleted_at" is null order by "books"."id" asc limit 2 offset 0
// Query2: select * from "books" where "books"."deleted_at" is null order by "books"."id" asc limit 2 offset 2
// Query3: select * from "books" where "books"."deleted_at" is null order by "books"."id" asc limit 2 offset 4

Cursor

<?php
$capsule->getConnection()->enableQueryLog();

foreach (Book::cursor() as $book) {
    echo $book->name , "\n";
}

$queryLog = $capsule->getConnection()->getQueryLog();

foreach ($queryLog as $i => $query) {
    echo 'Query' . ($i + 1) . ': ' . $query['query'] . PHP_EOL;
}

// Query1: select * from "books" where "books"."deleted_at" is null

ドキュメントの通り、chunkは第1引数に渡される数ごとにクエリを発行しており、cursorは1回のクエリ発行のみとなっているということがわかります。
また、chunkの場合は、クエリ発行を複数に分けるため、必ずORDER BY "PRIMARY_KEY" ASCを指定しています。

上記からわかることとしては、
1回のクエリでアクセスしている分cursorのほうが実行速度は速いものの、メモリの使用量を抑えるという意味では、あまりにも膨大な量のレコードを処理する場合にはcursorよりもchunkのほうが有用なのではないかと考えられると思います。

まとめ

  • Eloquentのchunkとcursorについてもう少し深く調べてみた
    • toSqlメソッドは知っていたが、getQueryLogメソッドによるSQL文の取得はより開発中に役立つと思った
    • ORMのSQL文生成の流れをもう少し深掘りしてみたくなった
    • メモリ使用量について、実際に計測してみようと思った*2

*1:Bookモデルはソフトデリートの設定をしているためdeleted_atカラムがnullのもののみを取り出すようなSQLとなっていることもわかります

*2:次回やります