web-dev-qa-db-ja.com

n + 1の問題に対処する簡単な方法は何ですか?

PHPのパフォーマンスをよりよく理解しようとしています。私が考えている1つの問題は、n + 1の問題です。 n + 1とは、次のような意味です。

$posts = Posts::getPosts();

foreach($posts as $post) {
  $comments = Comments::getComments(array('post_id' => $post->id));
  // do something with comments..
}

コメントを取得するには、投稿ごとに多くのクエリを実行する必要があるため、非常に非効率的です。

このようなものが良いですか?より多くのPHPコードですが、実行されるクエリは2つだけです:

$posts = Posts::getPosts();

// get the ids into an array
$post_ids = array();
foreach($posts as $post) {
  array_Push($post_ids, $post->id);
}

// get comments for ALL posts in one query by passing array of post ids (post_id IN (...))
$comments = Comments::getComments(array('post_id' => $post_ids));

// map comments to posts
foreach($posts as $key => $post) {
  foreach($comments as $comment) {
    if($post->id == $comment->post_id) {
      $post->pushComment($comment);
    }
  }
}

foreach($posts as $post) {
  $comment = $post->comments;
  // do something with comments..
}

これははるかに多くのPHPであり、多少厄介ですが、今回は2つのクエリのみを使用しています(1つは投稿用、もう1つはこれらの投稿のすべてのコメントを1つのクエリで取得するため)。これは、PHPのn + 1問題に取り組むための良い提案ですか?

また、フレームワークは一般的にこれを内部でどのように処理しますか?

7
Martyn

元のアプローチでは遅延読み込みが行われますが、変更されたコードでは熱心読み込みが行われます。

熱心な読み込みの方があなたの状況でより効率的であるということは絶対に正しいです。ほとんどの場合、クエリの数を最小限に抑えることが、アプリを高速化するために実行できる最善の方法です。投稿の1つだけを見ようとすると、遅延読み込みの方が速くなります。

ほとんどのORM(少なくとも、すべての優れたORM!)は遅延ロードと熱心なロードをサポートしています。たとえば、 SQLAlchemy は広範囲にサポートされています。あなたはPHPについて尋ねていました。メインのPHP ORMには同様の機能があります。通常、ORMのデフォルトは遅延ロードですが、クエリを実行するときに特定のテーブルを積極的にロードするように指示できます。

実際、1つのクエリですべてのデータを読み込むことができます。 SQLAlchemyには、結合とサブクエリの2つの熱心なロードモードがあります。 Joinedは、すべてを1つのクエリで実行します。これは、最も効率的な方法ですが、最終的には重複データのクエリを実行します。サブクエリの熱心な負荷は、アプローチとまったく同じです。 PHP ORMがこの区別をサポートしているかどうかはわかりません。

4
paj28

非効率性/乱雑さは、データの「水和」が早すぎ、頻繁すぎることが原因です。 「水和」とは、データベース内の(私が想定している)レコードからデータオブジェクトをインスタンス化することを意味します。

データオブジェクトを常に処理する必要はありません。たとえば、このコードでは...

_$posts = Posts::getPosts();

// get the ids into an array
$post_ids = array();
foreach($posts as $post) {
  array_Push($post_ids, $post->id);
}
_

...Posts::getPosts()がデータベースからいくつかの行をフェッチし、各行に新しいPostデータオブジェクトを作成したと仮定します。IDを抽出してデータオブジェクトを破棄するためだけです。数値の配列を返すPosts::getPostIds()のような関数を追加した場合、そのコードブロックの代わりにそれを呼び出すことができます。

_$post_ids = Posts::getPostIds();
_

あなたのコードの残りの部分はより難しい質問を持ち出します。投稿ごとに個別のクエリを実行するのではなく、事前に取得したレコードセットから「コメント」データオブジェクトを入力したい場合を除いて、元の例のようにできることが理想的です。多分_Comments::getComments_の代わりに、getCommentsインスタンスメソッドをPostに追加してこれを処理し、それにレコードセットを渡すことができます。

これは、実際に複数の投稿のコメントを一度に取得する必要があることを前提としています。私はあなたがそれをオフハンドで行う必要がある状況を考えることはできませんが、あなたがあなたが何をしているか知っていると思います。

他のフレームワークがORMをどのように処理するかを理解するには、 redbean のドキュメントを参照して、それがどのように使用されるかを確認することをお勧めします。興味がある場合は、ソースを確認してください。

4
Hey

記録としては、テックスタックのORMにはN + 1の問題がある可能性があります。手書きのコードでもこの問題が発生する可能性があります。

N + 1問題に最も効率的に対処する方法は、実際にはSQLのJOINです。これは、ソフトウェアエンジニアがSQLを絶対に学ぶ必要がある場所であり、プログラミングコードがSQLに変換される方法です。

投稿した元のコードは、これらのSQLステートメントを実行することになります。

SELECT * FROM posts; # returns, let's say, posts 1-20

SELECT * FROM post_comments WHERE post_id = 1;
SELECT * FROM post_comments WHERE post_id = 2;
SELECT * FROM post_comments WHERE post_id = 3;
SELECT * FROM post_comments WHERE post_id = 4;
SELECT * FROM post_comments WHERE post_id = 5;
SELECT * FROM post_comments WHERE post_id = 6;
SELECT * FROM post_comments WHERE post_id = 7;
SELECT * FROM post_comments WHERE post_id = 8;
SELECT * FROM post_comments WHERE post_id = 9;
SELECT * FROM post_comments WHERE post_id = 10;
SELECT * FROM post_comments WHERE post_id = 11;
SELECT * FROM post_comments WHERE post_id = 12;
SELECT * FROM post_comments WHERE post_id = 13;
SELECT * FROM post_comments WHERE post_id = 14;
SELECT * FROM post_comments WHERE post_id = 15;
SELECT * FROM post_comments WHERE post_id = 16;
SELECT * FROM post_comments WHERE post_id = 17;
SELECT * FROM post_comments WHERE post_id = 18;
SELECT * FROM post_comments WHERE post_id = 19;
SELECT * FROM post_comments WHERE post_id = 20;

これにより、多くのネットワークチャタリングが発生します。

あなたが本当に欲しいのはこれです:

SELECT posts.*,
       comments.*
FROM posts
LEFT JOIN post_comments comments ON comments.post_id = posts.id;

はい、必要以上に「より多く」の行が返されますが、パフォーマンスのボトルネックの原因となっているのは、ネットワークのやりとりです。これを行うには、適切なORMを構成する必要があります。これにより、各投稿の重複レコードがインテリジェントに処理され、オブジェクトが結合されます。

0
Greg Burghardt