🔥

【PHP】10 万件のデータも怖くない!Iterator パターンによる Lazy Loading 実践入門

に公開

皆様お疲れ様です。Ayumu3746221と申します。

最近になって、まだ実務未経験だった時に勉強したデザインパターンを勉強しなおしてるのですが、その際に iterator pattern が遅延読み込みに使われていることを知りました。

将来、数万人規模のサービス開発に携わりたい、私にとっては遅延読み込みはいずれ必要になる知識ですが、なかなか機会がないので、今回は遅延読み込みでよく扱われる、ライブラリ等の一部を自作してみようと思います。

1.はじめに

PHP で大量のデータベースレコードを扱う際に、一度に全てのレコードを呼び出すとメモリを大幅に使ってしまいます。個人開発の規模であれば問題ない範囲ですが、現場では数十万のレコードがある場合もあるため、開発者は遅延読み込みで情報を取得するわけですね。

普段はライブラリ等でお手軽で簡単に実装できるわけですが、今回は fetchAll で一度に呼び出す際のメモリ消費量と、自作で作成した遅延読み込みを比較してみようと思います!!

実装したコードは下記のリポジトリに入っているので、ご自由にお使いください。

https://github.com/Ayumu3746221/measuring_lazy_loading

2.検証したアプローチ

fetchAll によるアプローチ

以下のソースコードで、fetchAll で実行されるスクリプトを実装しました。

<?php
namespace App;

use PDO;

class BadLoading
{
    // -------<<一部 割愛>>------

    public function run(): void
    {
        echo "Running BadLoading (fetchAll)..." . PHP_EOL;
        $initialMemory = memory_get_usage();
        echo "Initial memory usage: " . $this->formatBytes($initialMemory) . PHP_EOL;

        // Fetch all records at once
        $stmt = $this->pdo->query("SELECT * FROM items");
        $items = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $memoryAfterFetch = memory_get_usage();
        echo "Memory after fetchAll: " . $this->formatBytes($memoryAfterFetch) . PHP_EOL;
        echo "Memory used by fetchAll: " . $this->formatBytes($memoryAfterFetch - $initialMemory) . PHP_EOL;

        // Simulate processing
        $processedCount = 0;
        foreach ($items as $item) {
            // Simulate some light processing
            $processedCount++;
        }

        echo "Processed " . $processedCount . " items." . PHP_EOL;
        $peakMemory = memory_get_peak_usage();
        echo "Peak memory usage: " . $this->formatBytes($peakMemory) . PHP_EOL;
    }

    // ------<<一部 割愛>>------
}

お分かりの通り、この方法だと一度に全てのレコードがメモリに流れてきて、メモリを大量に使用することになってしまいます。

実際に最大メモリ使用量は 60.75 MB となります。

遅延読み込みの実装

実際にここに遅延読み込みの実装を載せたいのですが、あまりにも冗長すぎるため、リポジトリのsrc/配下にある、Item.php,ItemStore.php,ItemStoreIterator.phpをご確認いただければどのように実装されたかわかると思います。

結果

今回の結果は

読み込み方式 最大メモリ使用量
一括読み込み(fetchAll) 60.75 MB
遅延読み込み(Iterator) ~0.52 MB

となりました。こういうの見ると最初に思いついた偉人は凄いですね、明日からはライブラリを使って巨人の肩に立ちましょう!!

なぜこれほど差がつくのか?

fetchAllは、結果のセット、つまりデータベースから取得したレコードを PHP の配列としてメモリ上に展開してしまいます。

一方、遅延読み込み(Iterator)を使った方法では、ループの各イテレーションで DB から取得した一行分のデータしかメモリに保持せず。処理が終わったデータはガベージコレクションによって解放されるため、ピーク時でもメモリを小さく抑えられるわけです。

まとめ

遅延読み込みは最近はライブラリで一行で実装できるようになりましたが、内部のロジックはそれなり重たいので、テーブルの規模感や使用できるメモリや、その際に使われるコストなどを考えて実装しましょう!!

個人的にライブラリで一行で行われているロジックの中に、データベースの問い合わせ等があることに恐ろしさを感じました。

データベースの問い合わせ量もメモリの使用量も大クラウドインフラ時代の昨今では、かなり重要な数値ではありますから、今回学習できてよかったです。

上記のようなトレードオフから判断できるエンジニアになれると良きですね!!

ではでは〜!!

Discussion