目的
- バッチ処理などで大量のデータを処理する場合、データが増えてもメモリ使用量が一定になるようにしておきたい
- データの増加に伴ってメモリ使用量が増えると、そのうち処理できなくなるので
Tips
1. Doctrine2のIterating results
を使用する
- Batch Processing - Doctrine Object Relational Mapper (ORM)
- これによって、ハイドレーション(DBの結果をオブジェクトなどにマッピングする処理)時のメモリ使用量を抑えることができる
- 取得してきたすべての結果セット分のオブジェクト生成を行わず、1つずつの生成になる
- 定期的に
clear
(もしくはdetach
)メソッドを呼ぶのも忘れずに- こちらはDoctrineが管理しているオブジェクトの情報を解放する
2. DBからは一度に全件を取得せずに、ある程度の塊として小分けで取得する
-
1.
の対応だけでは、DBの結果セットはすべて呼び出し側のメモリ(※1)にのってしまう -
よってデータを小分けに取得する
-
※1 PHP: バッファクエリと非バッファクエリ - Manual こちらの設定にもよる
-
libmysqlclient
を使用している場合は、変数に入れるまで、phpから見たメモリ使用量に加算されない(memory_get_usage()
にも加算されないし、memory_limit
にも引っかからない)が、実際そのプロセスのメモリ使用量は増えるので注意すること
-
2-1. setMaxResults
, setFirstResult
を使う
- いわゆる
LIMIT
,OFFSET
を使う - しかし、大量のデータだと後半のデータに行けば行くほど取得するのに時間がかかる問題もある
2-2. id BETWEEN
を使う
-
WHERE id BETWEEN :start AND :end
で取得するようにすれば、インデックスをうまく使えるので、後半の取得になろうと一定のスピードで取得できる - しかし、idに歯抜けがあると、実際にはデータがない無駄なループが回ってしまう可能性がある
- どちらを使うかはケースバイケース?
3. Logger
に気をつける
- DoctrineBundle のデフォルトでは
kernel.debug = true
だと、SQL呼び出しの度にそのクエリを記録するのでメモリ使用量が線形に増加してしまう、 よってコマンドの場合--no-debug
で実行すること - ログの記録を常にやめたいのであれば http://stackoverflow.com/a/10913115 にある方法で
SQLLogger
にnull
を入れておく - その他、HTTP通信するライブラリなどで、通信の詳細をLoggerに記録する場合もあるので、そういうところも必要に応じて設定を変更する
[2018/10/29追記] ライブラリにしました
下記の2点を自動で行うジェネレータのライブラリを作りました。
https://packagist.org/packages/kalibora/chunk-generator
- Doctrine2の
Iterating results
を使用する -
id BETWEEN
を使って、DBからは一度に全件を取得せずに、ある程度の塊として小分けで取得する
use Kalibora\ChunkGenerator\ChunkGeneratorBuilder;
$fooRepository = $manager->getRepository(Foo::class);
$qb = $fooRepository->createQueryBuilder('f');
$gen = ChunkGeneratorBuilder::fromDoctrineQueryBuilder($qb)->setChunkSize(200)->build();
// Iterate all foo entity. Fetch 200 records at a time and clear it.
foreach ($gen() as $foo) {
echo $foo->getVar(), PHP_EOL;
}
こういう感じで使えます。