Edited at

MySQL - [PDO]PHP 経由でメモリリミットを回避し、CSV ファイルのファイルサイズ関係なくダウンロードをできるようにする方法


課題

CSV ダウンロード機能を実装してみたのはいいが、ある程度のファイルサイズになると

メモリリミットでエラーになったり、PHP からの応答待ちでタイムアウトしたりという課題がある

これを CSV のファイルサイズに関係なくダウンロードをできるように改善する方法をスクリプトで残した

参考まで簡単に手元でも確認できるソースを用意してみた

https://github.com/oz-urabe/qiita-for-csv-download


もともとの問題のある処理


  1. デフォルトのバッファモードで DB からデータを取得 [解決方法1]

  2. 取得したデータを配列に格納 [解決方法2]

  3. 格納した配列を echo などで出力


解決方法1

デフォルトのバッファモードで DB からデータを取得

$stmt = $pdo->prepare('SELECT * FROM dummy');

$stmt->execute();

ここで問題となるのが、PHP:バッファクエリと非バッファクエリ から抜粋した以下の記事

クエリは、デフォルトではバッファモードで実行されます。

つまり、クエリの結果がすぐに MySQL サーバーから PHP に転送され、
PHP プロセスのメモリ内に結果を保持し続けるということです。
これで、その後で行数を数えたり結果ポインタを
移動 (シーク) したりといった操作ができるようになります。

保持してくれることで、取得後の値を php 側であとから加工しやすくなるが、メモリリミットの問題を引き起こしやすくなるため、デフォルトの挙動が有用であるかはケースバイケース

以下のように追記した

$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);

$stmt = $pdo->prepare('SELECT * FROM dummy');
$stmt->execute();

純粋な PDO で記述しているが、他の DB 接続方法でも同様に対応できる。

また、ORM や フレームワークごとにも対応方法が定義されていると思うので、案件ごとにドキュメントを見てください。


解決方法2

取得したデータを配列に格納 という処理ですが、以下のように処理されています。

これは関数の中で配列を作り、それを返り値としています。

    $csv = [];

while ($row = $stmt->fetch()) {
$csv[] = sprintf(
'%d,%s,%s,%s,%s,%s MB',
$row['id'],
$row['name'],
$row['comment'],
$row['created'],
$row['updated'],
memory_get_peak_usage(true) / 1024 / 1024
);
}
return $csv;

単純に理解できると思いますが、配列が大きくなるとメモリ消費が大きくなります。

つまり、配列に保存しない方法を考えればよいわけです。

    while ($row = $stmt->fetch()) {

yield sprintf(
'%d,%s,%s,%s,%s,%s MB',
$row['id'],
$row['name'],
$row['comment'],
$row['created'],
$row['updated'],
memory_get_peak_usage(true) / 1024 / 1024
);
}

yield を使うと、その場で配列を生成せず、反復可能なオブジェクトを返します。こうすることでメモリに値を貯め込むことをせず、 foreach の処理内で csv を echo などで出力するタイミングで値を取得していくような動作となります。

yield が使えない古い php の場合においても、配列に溜め込まず、 関数内で echo するような処理にすればよいです。


補足

試しに github に配置したソースを使って、メモリリミットの現象が回避できるか検証していただいてもよいかと思います

https://github.com/oz-urabe/qiita-for-csv-download.git