問題
データ変換のためのバッチ処理などのように、FuelPHPで大量のデータを総当たりで処理しようとするとメモリが足りずエラーになることがある。ギガを超える量のメモリを割り当てても動作しないので調べてみた。
$query = DB::select('*')->from('member_data')->as_object()->execute();
データの件数は12万件くらい。ORMはオーバーヘッドが大きいのでクエリビルダーで書いたけど、それでも全然軽くならない。
Fatal Error!
ErrorException [ Fatal Error ]:
Allowed memory size of 134217728 bytes exhausted (tried to allocate 4194312 bytes)
COREPATH/classes/database/pdo/connection.php @ line 300
エラーの内容は上記のとおり。メモリーを使い切ってしまっている。
// Convert the result into an array, as PDOStatement::rowCount is not reliable
if ($as_object === false)
{
$result = $result->fetchAll(\PDO::FETCH_ASSOC);
}
elseif (is_string($as_object))
{
$result = $result->fetchAll(\PDO::FETCH_CLASS, $as_object);
}
else
{
$result = $result->fetchAll(\PDO::FETCH_CLASS, 'stdClass');
}
https://github.com/fuel/core/blob/1.8.0.4/classes/database/pdo/connection.php#L286-L298
FuelPHPのデータアクセスの仕組みはこんなふうになっている。fetchAllで全てのデータを一気に取り込んで配列化する。メモリを贅沢に使ってパワーで回す感じ?12万件のデータを処理するバッチだったら、12万件のデータで構成された巨大な配列を生成する。
メモリさえあればFuelPHPは軽快に動作するみたいだけど、数十万件を超える規模のデータをバッチで回すようなケースだと、いくらメモリを足しても限度がある。
解決方法
下記のように書き換えてみた。
$db = Database_Connection::instance()->connection();
$result = $db->query('select * from member_data');
while($row = $result->fetch(PDO::FETCH_OBJ))
{
echo sprintf('ID:%s 名前:%s<br />',$row->id,$row->name);
}
結果セットだけ取り寄せてカーソルを進めながら1件ずつ処理する。これならメモリを食わない。PDOとか生SQLをゴリゴリ書く人ならよく使う書き方。
一度ですませていたキャッチボールがループ回数ぶん増えるため、DBサーバとWebサーバが物理的に異なるロケーションにある場合は遅くなりそうだけど、メモリ不足で落ちることはなくなる。今回はレスポンスを気にしない変換バッチなので問題ない。
$db = Database_Connection::instance()->connection();
$result = $db->query(
DB::select('*')->from('member_data')->compile()
);
while($row = $result->fetch(PDO::FETCH_OBJ))
{
echo sprintf('ID:%s 名前:%s<br />',$row->id,$row->name);
}
クエリビルダーでFuelっぽく書きたいなら、上記のようにcompile()を使う。
https://github.com/fuel/core/blob/1.9/develop/classes/database/pdo/result.php
開発中のv1.9ではこんな処理が加わっている。このファイル自体がv1.8の時点では存在しない。どうしたいのかまだよく分からないけど、もしかすると将来はこれが使えそう。