N+1問題とは
データベースから情報を取得するときのパフォーマンスの問題。
具体的には、SQLがN+1回発行されてしまうことでパフォーマンスが低下する問題。
N+1問題の具体例
データベースの構造
N+1問題は、子テーブルの中に親テーブルに紐づく情報が複数存在するときに現れる。
例えば、学校テーブル(親)と生徒テーブル(子)があるとする。学校には複数の生徒が所属しているので、学校テーブルと生徒テーブルは1対多の関係にある。
田中ひめ、鈴木ひなは西高校に所属しており、それ以外の3人は中央高校に所属していることがわかる。
典型的なコード例でのN+1問題の発生
冒頭に示した通り、N+1問題はSQLがN+1回発行されてしまうことでパフォーマンスが低下する問題である。
ここではSQLがN+1回発行されるコードを見ていく。
$schools = $db->query("SELECT * FROM schools");
$result = [];
foreach ($schools as $school) {
$student = $db->query("SELECT * FROM students WHERE students.id = {$school['id']}");
$result[] = [
"id" => $school["id"],
"school_name" => $school["id"],
"students" => $student,
]
}
学校テーブルから情報を取得するのに1回のSQL発行を行なっている。
また、foreachループの中で、学校テーブルのレコードの数だけ(N回)SQL発行を行なっている。
つまり、N+1回のSQL発行を行なっている。
今回の例では合計3回のSQL発行で済んでいるが、親テーブルに何万レコードも存在することもある。
そんなにSQLを発行していたら、パフォーマンスは低下する。
N+1問題の解決策
- JOIN句の活用
- Eager Loadingの導入
JOIN句の活用
JOIN句を活用することで、学校レコードを取得するときに生徒レコードも一緒に取得できる。
SQLの発行は1回になる。
$schools = $db->query(
"SELECT * FROM schools LEFT JOIN students ON schools.id = students.id"
);
Eager Loadingの導入
学校レコードの取得とは別に、生徒レコードをまとめて取得する。
SQL発行回数は2回になる。
$schools = $db->query("SELECT * FROM schools");
$students = $db->query("SELECT * FROM students WHERE school_id IN (1, 2)");
ORM(Object-Relational Mapping)とN+1問題
ORMの概要
ORM(Object-Relational Mapping)は、データベースとオブジェクト指向プログラミング言語の間でデータを効果的にやり取りするための仕組みや手法のこと。
言い換えれば、データベースのテーブルとオブジェクト指向プログラム内のオブジェクトの間で、簡単にデータを変換したり、操作したりするためのツールやライブラリ。
ORMがN+1問題をどのように扱うか
cakephp4だと、クエリビルダーの機能によってできるだけ最小のクエリで取り出せるように最適化してくれる。この機能を使うにはテーブル間の関連を記述する必要がある(テーブルファイルをbakeで生成する場合、自動的に書いてくれる事が多い)。
クエリオブジェクトを作成するときは、containを忘れずに書こう。
$query = $articles->find('all');
$query->contain(['Authors', 'Comments']);
まとめ
N+1問題はデータベースから情報を取得するときのパフォーマンスの問題。SQLの発行をループ処理の中で書こうとしているときは、一度立ち止まって考えてみてほしい。
本記事で誤っている点があったら、ご指摘いただけるとありがたいです。
参考