O(N)やN+1問題とは?Webアプリを爆速化する計算量の基礎知識

システム開発

こんにちは、編集長Kです。

開発したアプリの動作がモッサリしている。 データが増えた途端に画面がフリーズした。 そんな経験、ありませんか?

その原因は、多くの場合「計算量」にあります。 もしくは「N+1問題」のせいかもしれません。

「言葉は聞いたことあるけどフワッとしている」 そんな方も多いのではないでしょうか。

この記事では、それらの仕組みを紐解きます。 そして、コードを爆速化する手法を学びましょう。 現場レベルで使える知識をお伝えしますよ。

この記事で解説する手順の全体像です。

  • 手順①:計算量(ビッグオー記法)の基本を理解する
  • 手順②:データベースの罠「N+1問題」を撃退する
  • 手順③:O(N^2)の二重ループをO(N)に改善する

前提として、以下の知識があるとスムーズです。

  • 基礎的なプログラミングの経験
  • データベースからデータを取得した経験
  • LaravelなどのWebフレームワークの基本知識
スポンサーリンク
OANDA証券 MT4

手順①:計算量の基本を理解する

まずはアルゴリズムのスピード指標です。 これを「ビッグオー(O)記法」と呼びます。 データが増えた時に、どれくらい遅くなるか。 それを示すための共通言語です。

【O(1)】は最強です。 データが10件でも100万件でもスピードは同じ。 処理時間は常に一定です。

【O(N)】はデータ量に比例します。 1万件のデータなら、1万回の処理が必要です。 ここは簡単ですね。

【O(N^2)】は最悪のパターンです。 データが10倍になると、処理時間は100倍です。 システムが止まる原因の多くはこれです。

では、O(N)のコードを見てみましょう。 まずは、下記を実行してください。

// O(N)の例:全員を1人ずつチェックする
$targetName = '田中';
$found = false;

foreach ($users as $user) {
    if ($user->name === $targetName) {
        $found = true;
        break;
    }
}

これでOKです。 順番に探すので、人数分だけ時間がかかります。

次は、これをO(1)に近づける工夫です。 連想配列(ハッシュマップ)を使います。 続いて、下記です。

// O(1)に近い例:キーを指定して一発で探す
// 準備としてIDをキーにした配列を作っておく
$userMap = [
    '田中' => ['age' => 30, 'role' => 'admin'],
    '佐藤' => ['age' => 25, 'role' => 'user'],
];

// 探す処理は1回で終わる
if (isset($userMap['田中'])) {
    $found = true;
}

はい、これで一瞬で検索できるようになりました。
ループを使わないのが爆速化のコツです。

手順②:データベースの罠「N+1問題」を撃退する

次は、フレームワーク特有の罠です。 「N+1問題」と呼ばれる有名な現象ですね。

これはDBへ無駄な問い合わせを繰り返す問題です。 1回の質問で済むのに、人数分質問してしまいます。

例えば、ユーザー一覧とプロフィールを表示します。 ダメな例を見てみましょう。 まずは、下記を実行してください。

// ダメな例(N+1問題が発生)
$users = User::all(); // ここで1回クエリ発行

foreach ($users as $user) {
    // ユーザー数(N回)だけクエリが追加発行される!
    echo $user->profile->bio; 
}

これで確認できましたね。 ログを見ると、恐ろしい数のSQLが発行されます。 ユーザーが100人なら、101回の通信が発生します。

これを防ぐには「Eager Loading」を使います。 事前に関連データをまとめて取得する手法です。 続いて、下記です。

// 改善例(Eager Loadingで解決)
// with()を使ってプロフィールも一緒に持ってくる
$users = User::with('profile')->get(); // クエリは合計2回だけ

foreach ($users as $user) {
    // ここではクエリは発行されない
    echo $user->profile->bio; 
}

これでバッチリです。
クエリが2回に減り、通信のムダがなくなりました。
体感速度が劇的に変わるはずです。

手順③:O(N^2)の二重ループをO(N)に改善する

現場でよく見る「遅いコード」の代表格です。 配列と配列を突き合わせる処理ですね。

無意識に「ループの中でループ」を書いていませんか? これはO(N^2)となり、非常に危険です。 まずは、下記を実行してください。

// ダメな例:O(N^2)の二重ループ
$trades = Trade::all(); // 1000件
$accounts = Account::all(); // 100件

foreach ($trades as $trade) {
    foreach ($accounts as $account) {
        if ($trade->account_id === $account->id) {
            $trade->account_name = $account->name;
        }
    }
}

これで動くには動きます。
しかし、1000件×100件で10万回のループが発生します。
これも連想配列を使ってO(N)に改善しましょう。
続いて、下記です。

// 改善例:O(N)に落とし込む
$trades = Trade::all();
$accounts = Account::all()->keyBy('id'); // IDをキーにする

foreach ($trades as $trade) {
    // ループは1000回だけで済む
    if (isset($accounts[$trade->account_id])) {
        $trade->account_name = $accounts[$trade->account_id]->name;
    }
}

これで安心です!
二重ループがなくなり、一気に処理が軽くなりました。

よくあるエラーと解決策

知識があっても、現場ではトラブルが起きます。
私の経験則から、よくある罠を共有しますね。

N+1対策をしたのにまだ遅い

「with()を使ったのに遅い」という相談をよく受けます。
この場合、そもそもDBのインデックスが不足しています。
外部キーのカラムにインデックスを張りましょう。

メモリ枯渇エラー(Out of Memory)が発生する

クエリの回数を減らそうと、一度に全データを取得していませんか? 「O(1)」の通信回数になっても、メモリがパンクします。 数万件のデータは chunk() で分割取得しましょう。

まとめ:計算量を意識してプロのエンジニアへ

お疲れ様でした! 計算量やN+1問題の改善手法、いかがでしたか?

「ループを減らす」「DBへの通信回数を減らす」 これらを意識するだけで、システムは驚くほど安定します。 現場で愛されるエンジニアへの第一歩ですね。

Trade Agencyでは、こうした現場のリアルな開発ノウハウをどんどん発信しています。 トレードシステムの裏側に興味がある方は、ぜひブックマークをお願いします!

それでは、また次回の記事でお会いしましょう。編集長Kでした!

コメント

タイトルとURLをコピーしました