バックエンド 2026.10.20

PHPで大容量ファイルを扱うときメモリ不足を起こさない実装パターン

約13分で読めます

PHPで大容量ファイルを処理する際のメモリ不足問題を解決する実装パターンを解説。ストリーム処理・ジェネレータ・チャンク分割など現場で使える手法を紹介します。

「メモリ不足でスクリプトが死ぬ」——あなたも経験ありませんか?

PHPでCSVや画像、ログファイルを扱うコードを書いていると、ある日突然こんなエラーに直面することがあります。

Fatal error: Allowed memory size of 134217728 bytes exhausted

ローカル環境では問題なく動いていたのに、本番の大容量ファイルを処理した途端にスクリプトがクラッシュ。焦ってメモリ上限を memory_limit = 512M に引き上げて一時凌ぎをした経験がある方も多いのではないでしょうか。

実はこれ、「とりあえず動いている」だけで、根本的な問題は解決されていません。ファイルが大きくなればいずれまた同じエラーが出ますし、サーバー全体のパフォーマンスを圧迫するリスクも残ります。

この記事では、ファイルサイズに依存しないメモリ効率の良いPHP実装パターンを、実際の案件経験をもとに解説します。


なぜPHPはメモリ不足になるのか——問題の根本を理解する

まず原因を整理しましょう。PHPの典型的なファイル処理コードがなぜメモリを食うのか、以下の例を見てください。

// NG例: ファイル全体をメモリに読み込んでしまう
$lines = file('/path/to/large_file.csv'); // 全行を配列として保持
foreach ($lines as $line) {
    // 処理...
}

file() 関数は、ファイル全体を一度に配列として読み込みます。100MBのCSVなら100MB以上のメモリを使用します。 同様に file_get_contents() も全データをメモリ上に展開するため、大容量ファイルには向きません。

PHPのデフォルト memory_limit は多くの環境で128MBに設定されています。50万行のCSVや高解像度画像を扱うシステムでは、この上限をあっさり超えてしまうわけです。

グラフを見れば一目瞭然です。ストリーム処理を使うと、ファイルサイズが増えてもメモリ使用量はほぼ一定に保てます。


バックエンド開発でお困りですか?

API設計・DB最適化・システム構築など、ご相談ください

無料で相談する

現場で使える実装パターン3選

パターン① ストリーム処理でCSVを1行ずつ読む

最もシンプルかつ効果的な方法です。fopen() + fgets() または fgetcsv() を使い、1行読んでは処理し、メモリから解放するサイクルを繰り返します。

// 良い例: 1行ずつストリーム処理
$handle = fopen('/path/to/large_file.csv', 'r');
if ($handle === false) {
    throw new RuntimeException('ファイルを開けませんでした');
}

try {
    while (($row = fgetcsv($handle, 4096)) !== false) {
        // 1行分のデータだけメモリ上に存在する
        processRow($row);
    }
} finally {
    fclose($handle); // 必ずクローズする
}

ポイントは try/finally ブロックで必ずファイルハンドルを閉じることです。例外が発生しても確実に fclose() が実行されます。

パターン② PHPジェネレータで再利用可能なイテレータを作る

同じCSV処理をいろんな箇所から呼び出す場合、ジェネレータを使うとロジックを綺麗に分離できます。

// ジェネレータ関数: メモリ効率よく行を返す
function readCsvRows(string $filePath): Generator
{
    $handle = fopen($filePath, 'r');
    if ($handle === false) {
        throw new RuntimeException("ファイルが開けません: {$filePath}");
    }

    try {
        // ヘッダー行をスキップ
        fgetcsv($handle);

        while (($row = fgetcsv($handle, 4096)) !== false) {
            yield $row; // 1行だけ返して一時停止
        }
    } finally {
        fclose($handle);
    }
}

// 呼び出し側: 通常のforeachで使える
foreach (readCsvRows('/path/to/data.csv') as $row) {
    // $rowは1行分のみ。全体はメモリに乗っていない
    importToDatabase($row);
}

yield キーワードにより、関数は1行処理するたびに「一時停止 → 呼び出し元へ返す → 再開」を繰り返します。10万行のファイルを処理しても、常に数行分のデータしかメモリを使いません。

パターン③ 大容量ファイルのチャンク分割書き込み

ファイルへの書き込み処理も同様です。大量データをDBから取得してCSVに書き出す場合、一括取得は禁物です。

// NG: 全件取得してから書き込み
$allData = $pdo->query('SELECT * FROM orders')->fetchAll(); // メモリ爆発の危険
file_put_contents('output.csv', formatAsCsv($allData));

// 良い例: チャンクで分割して処理
$handle = fopen('output.csv', 'w');
$offset = 0;
$chunkSize = 1000; // 一度に処理する件数

try {
    $stmt = $pdo->prepare(
        'SELECT * FROM orders LIMIT :limit OFFSET :offset'
    );

    do {
        $stmt->bindValue(':limit', $chunkSize, PDO::PARAM_INT);
        $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
        $stmt->execute();
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        foreach ($rows as $row) {
            fputcsv($handle, $row);
        }

        $offset += $chunkSize;
    } while (count($rows) === $chunkSize);
} finally {
    fclose($handle);
}

Laravelを使っているなら chunk() メソッドがこの処理を内包しています。

// Laravelのchunk()を使う場合
Order::chunk(1000, function ($orders) use ($handle) {
    foreach ($orders as $order) {
        fputcsv($handle, $order->toArray());
    }
});

よくある失敗パターンと対処法

php.iniで `memory_limit = 1G` にして解決したつもりになるパターン。ファイルが大きくなれば再発します。しかも高負荷時に複数リクエストが同時に大容量ファイルを処理すると、サーバー全体のメモリが枯渇してサービスダウンにつながります。**上限を上げるのではなく、上限に依存しない設計にすることが根本解決です。**
1行ずつ読んでいるつもりが、処理の中でデータをどんどん配列に追加してしまっているケースです。例えばバリデーションエラーを蓄積するために `$errors[] = $row` を繰り返していると、結局全件分のデータがメモリに残ります。エラー件数だけカウントするか、エラーをその場でファイルやDBに書き出す設計にしましょう。
メモリ問題を解決しても、今度は `max_execution_time` の上限に引っかかるケースがあります。大容量ファイル処理はCLIから実行するか、バックグラウンドジョブ(LaravelならQueue)として非同期処理するのがベストプラクティスです。WebリクエストのタイムアウトはUX上も問題になります。
例外処理を書かずに処理中断が起きると、ファイルハンドルが開きっぱなしになります。特にWindowsサーバー環境ではファイルロックが残り、後続の処理でエラーが出ることがあります。必ず `try/finally` でクローズを保証してください。

あるクライアントでの実例——30万行CSV処理の改善

神奈川の卸売業者様から「商品マスタCSVのインポートが途中でエラーになる」というご相談をいただいたことがあります。30万行・約80MBのCSVを file() で読み込んでいたため、memory_limit 128M の壁に毎回ぶつかっていました。

対応策として取ったのは次の3点です。

  • fgetcsv() によるストリーム処理への書き換え
  • バリデーションエラーの別ファイル書き出し(配列蓄積をやめる)
  • LaravelのQueueジョブ化による非同期処理

結果として、メモリ使用量は最大時でも約8MBに収まり、処理も安定して完走するようになりました。インポート完了後に管理者へメール通知する仕組みも合わせて実装し、担当者の作業負担も大幅に軽減されました。

「コードを少し書き直すだけでこんなに変わるんですね」と驚いていただけたことが印象に残っています。


バックエンド開発でお困りですか?

API設計・DB最適化・システム構築など、ご相談ください

無料で相談する

開発・運用でお困りなら

システム開発

設計から運用まで、堅牢なシステムを構築します

200件以上の制作実績 顧客満足度97% 初回相談無料

※ 通常1営業日以内にご返信します

まとめと次のステップ

PHPのメモリ不足問題は、「ファイルを全部読んでから処理する」設計そのものが原因です。ストリーム処理・ジェネレータ・チャンク分割の3パターンを使い分けることで、ファイルサイズに依存しない堅牢なシステムを構築できます。

まず既存コードを見直してみてください。file() や file_get_contents() でCSVや大容量テキストを読んでいる箇所があれば、今日から改善のスタートラインに立てます。


Fivenine Designからひとこと

「動いてはいるが、実は時限爆弾を抱えている」コードは珍しくありません。特にデータ量が増えてきた段階で一気に問題が顕在化するケースが多いです。

既存システムのパフォーマンス改善・コードレビュー・Laravel/WordPressを使った安定した処理基盤の構築など、バックエンド開発でお困りのことがあれば、神奈川拠点の私たちFivenine Designにお気軽にご相談ください。20年以上の実績で、貴社のシステムを安心・安定して動かすお手伝いをします。

この記事をシェア

システム開発のご相談、受付中です

設計・開発・テスト・運用まで、ビジネスに合ったシステムを構築します。 初回相談は無料です。

※ 1営業日以内にご返信いたします

この技術でお困りなら

無料でプロに相談できます

相談する
AIに無料相談