はじめに
今回は私がN+1問題について調べたことをもとに,
- N+1問題とはなにか
- N+1問題を解決するためには
をアウトプットして言いたいと思います.まだまだひよっこエンジニアのため,修正点等があればコメントで教えてください!
N+1問題とは
データベースの操作においてよく見られるパフォーマンスの問題です.特にオブジェクト関係マッピング(ORM)を使用しているときに発生しやすい問題です.1つのデータを取得するために1回のクエリを実行すると,関連するデータを取得するために追加のN回のクエリは必要になり,結果として「N+1」回のクエリが必要になるケースを指します.
たとえば,あなたが友達10人にパーティーのお土産を渡すとします.友達一人一人に「何が欲しいの?」と聞いて,それを取りに行くのは非常に手間がかかりますよね.それよりも,最初に何が欲しいのかを全員に聞いてから,1度にすべてのお土産を取りに行く方が速いです.このように,N+1問題が発生したときに,それを解決する手段として有効なのが「一度にすべてを処理する」ことです.
N+1問題について考えるとき,Nが大きいときは処理に非常に時間がかかるので,打開策が必要になります.
この解決策として,
- JOINを利用して表の結合
- Eager Loading
の2種類があります.
これらの解決策についてそれぞれ説明したいと思います.
まずは例として以下のようなケースがあったとします.
ケース
以下のような「アニメ」のテーブルとアニメの「筆者」テーブルが存在するとします.
各アニメに対して,そのアニメの筆者の名前と年齢を知りたいとすると,まず以下のようなSQLを発行します.
SELECT * FROM アニメ;
次に,アニメテーブルの各レコードから筆者IDを用いて筆者テーブルから検索するSQLを打つ.
SELECT * FROM 筆者 WHERE 筆者ID=3;
SELECT * FROM 筆者 WHERE 筆者ID=2;
SELECT * FROM 筆者 WHERE 筆者ID=4;
SELECT * FROM 筆者 WHERE 筆者ID=1;
上記のケースでは,最初にアニメテーブルを取得してきて(1件のクエリ),次に筆者テーブルを取得(4件のクエリ)しないといけない.このケースではN=4ですが,Nが膨大な数になった場合,処理時間が膨大になってしまいます.
JOINを利用して表の結合
この問題を解決するために,SQLのJOIN句を使って,アニメと筆者テーブルを結合し,各アニメに対して筆者の名前と年齢を取得するためのSQLクエリの例です.
SELECT アニメ.アニメID,アニメ.タイトル,筆者.名前,筆者.年齢
FROM アニメ
JOIN 筆者 ON アニメ.筆者ID = 筆者.ID
Eager Loading
Eager LoadingはORMの文脈で使用され,アプリケーションコード内でのデータ取得方法です.Eager Loadingの利点は,関連するデータを事前にまとめて取得する手法です.これにより,データベースへのアクセス回数を大幅に削減して,システムのレスポンス時間を短縮します.
pythonのフレームワークとしてよく利用されているsqlalchemyでは,joinedloadを使用して,アニメテーブルをクエリする際に,関連する筆者情報を事前にロード(Eager Loading)します.これによって,各「アニメ」に対して個別に「筆者」をロードする必要がなくなり,N+1問題を回避できます.
from sqlalchemy.orm import sessionmaker, joinedload
# データベースエンジンを設定(ここではSQLiteメモリ内データベースを使用)
engine = create_engine('sqlite:///:memory:', echo=True)
Base.metadata.create_all(engine)
# セッションを作成
Session = sessionmaker(bind=engine)
session = Session()
# Eager Loadingを使って、Bookとそれに関連するUser情報をロード
animes_with_users = session.query(Anime).options(joinedload(Anime.user)).all()
for anime in animes_with_users:
print(f"Anime: {anime.title}, Borrowed by: {anime.user.name}")
最後に
最後まで読んでいただきありがとうございました.
N+1問題を解決する手段として,生のSQLを利用する場合はJOIN句,ORMを利用する際にはEager loadを利用することがよいでしょう.