Edited at

FuelPHP1.8で大量のデータを処理する


問題

データ変換のためのバッチ処理などのように、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ではこういう書き方を普通にするけど、FuelPHPのコア内ではこういう記述は一ヶ所もない。

SQLサーバが物理的に異なるロケーションにある場合は遅くなるかもしれないけど、メモリ不足で落ちることはなくなる。今回はレスポンスを気にしない変換バッチなのでこれで問題ない。

$db = Database_Connection::instance()->connection();

$query = DB::select('*')->from('member_data')->compile();
$result = $db->query($query);
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の時点では存在しない。どうしたいのかまだよく分からないけど、今後に期待。

Laravelだったらchunkってのがある。