前回書いた記事では、Eloquent ORMにおける参照系メソッドについて紹介しました。
その中で、ChunkとCursorという項目について、
こちらについては、僕自信の理解が曖昧なため、もう少し調べる必要があると思います。 詳しく理解されている方がいらしたら、コメントで参考サイトなど教えていただきたいです。
と書いたところ
chunk, cursorは発行されるSQLを確認した方がいいと思います
というフィードバックをいただいたので、もう一度調べてみました。
※ 本エントリで利用しているコードはすべて
にあります。
目次
(もう一度)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のほうが有用なのではないかと考えられると思います。