p_chinのおっぱいブログ

UnityとPerlなど

n+1問題を体験しました

ORM使ってて、便利で余裕キメてたらn+1問題を起こしてた。

n+1問題とは

1回目

のselectで、あるテーブルのレコードの集合を取ってきて、更にまた別のテーブルから先ほど取得したレコードデータ使用して

n回

selectクエリを発行してしまう現象だ。

うまく説明出来ないので下の具体例書いてみた

 

解決策(今回の事案の場合)

  • JOIN使ってクエリを一回にまとめる
  • WHERE IN使ってクエリを2回にまとめる

のどちらかの対処でクエリ減らす努力をするべきだった。

 

具体的にどうやってしまったか

friendテーブルからplayerのフレンドのレコードの集合を1回引っ張ってきて、そのレコードからまたplayerテーブルへ、フレンドのplayerデータをn回取りに行っている。

sub _get_friends_info {
    # 自分のフレンド関係データ(player, target)のレコードの集合を取得
    my $rows = $teng->search('player_friend', { player_id => $my_player_id });
 
    my $result;
    # n回クエリ飛ばしちゃってる所
    # 自分とフレンド関係にあるtarget_idからplayer情報を取ってきてる
    while (my $row = $rows->next) {
        my $player = $teng->single('player', {id => $row->target_id});
        push @$result, $player->user_info;
    }

    return $result;
}

上のを実際のクエリにすると

mysql> select * from player_friend where player_id = $my_player_id;
mysql> select * from player where id = $row->target_id; をn回繰り返す

2行目のクエリをn回繰り返すのでクエリ数は1+nになる

 

今回はWHERE INで解決する事にする

sub _get_friends_info {
    my $rows = $teng->search('player_friend', { player_id => $my_player_id });

    my @ids = map {$_->target_id} $rows->all;
    my $result = $teng->search('player', {
        id => {in => [@ids] },
    });

    return $result;
}

上のを実際のクエリにすると

mysql> select * from player_friend where player_id = $my_player_id;
mysql> select * from player where id in (target_id1, target_id2, target_id3……);

クエリ数2になる

 

まとめ

4月の技術研修で教わったはずだけど、すっかりリフレッシュしていた。

ORMを使っていたりするとカジュアルにクエリを投げられるので、たくさんORMのメソッドからクエリ発行する場合はDBIx::Querylogとかでクエリを見るのもいいかも

 

参考

平凡なエンジニアの独り言:n+1問題を回避する