はじめに
Webアプリケーションを開発していると
「動作は正しいのにデータ量が増えると急に遅くなる…」
という状況に遭遇することがあります。
その原因としてよく挙げられるのが 「N+1問題」 です。
これはPHP/Java/Ruby/Python/Go/Node.jsなど言語を問わずあらゆるアプリケーションで発生しうる問題です。
N+1問題とは?
たとえば、データベースに保存しているユーザー一覧と、そのユーザーに紐づく住所情報を一緒に扱いたいとします。
このとき、次のようなコードを書いてしまうと
ユーザー1件ごとに追加でSQLが発行されてしまうため、ユーザー数に比例してクエリ回数が増えていきます。
ユーザーが 10件なら 11回、1,000件なら 1,001回……というように
最終的にはパフォーマンスの低下につながります。
これが、いわゆる 「N+1問題」 と呼ばれるものです。
use App\Http\Controllers\Controller;
use App\Models\User;
class UserController extends Controller
{
public function index()
{
// すべてのユーザーを取得
$users = User::all();
$usersData = [];
foreach ($users as $user) {
$usersData[] = [
'id' => $user->id,
'name' => $user->name,
// Model側に address のリレーションを定義している想定
'address' => $user->address?->full_address,
];
}
return $usersData;
}
}
上記のコードは、バックエンド側で起こり得るケースとしてPHP/laravelでシンプルに書いたものです。
動作としては正しいのですが、パフォーマンスの面ではあまり評価できません。
じゃあ、どうすべき?
とはいえ、関連テーブルのデータも一緒に扱いたい場面は多々あります。
その場合は、laravelであれば関連データを事前にまとめて取得する(Eager Loading) が定石です。
use App\Http\Controllers\Controller;
use App\Models\User;
class UserController extends Controller
{
public function index()
{
// addressを事前にまとめて取得(Eager Loading)
$users = User::with('address')->get();
$usersData = [];
foreach ($users as $user) {
$usersData[] = [
'id' => $user->id,
'name' => $user->name,
// Eagerロード済みなのでここで追加クエリは発生しない
'address' => $user->address?->full_address,
];
}
return $usersData;
}
}
これならユーザー数が何件に増えても、クエリ回数は「ほぼ2回」のままで済みます。
上記のコードは、「必要なカラムのみに絞る」などの工夫で
もう少しパフォーマンス改善できますが
N+1問題の改善方法としての大枠はこれで充分伝わると思います。
つまり、必要な関連データは事前取得しておき
可能な限りループの中で追加クエリを発生させないこと。
言語やフレームワークにより方法や書き方は違えど、根本的な考え方は変わりません。
開発をする際にこの点を意識して進めるだけでも、N+1問題による事故を防げるのではないでしょうか。