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とかでクエリを見るのもいいかも