0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

N+1問題とその解決方法についてしらべてみました.

Posted at

はじめに

今回は私が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種類があります.

これらの解決策についてそれぞれ説明したいと思います.
まずは例として以下のようなケースがあったとします.

ケース

以下のような「アニメ」のテーブルとアニメの「筆者」テーブルが存在するとします.

スクリーンショット 2024-02-20 192730.png

各アニメに対して,そのアニメの筆者の名前と年齢を知りたいとすると,まず以下のような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を利用することがよいでしょう.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?