タイトルの通り、JOINしてORDERしてLIMITをかけたら、取得したレコードが期待した結果と違かった。ということで備忘録。Railsのバージョンは4系、5系両方で再現。

具体的にはこんな感じのクエリを発行しました。UserはTaskに対してhas_manyの関係です。

DBにはusersテーブルに2レコード、tasksはusers1レコードに対して3レコードずつ紐付いています。

usersレコードは3件抽出されて紐づくtasksはidで並び替えられている、という挙動を期待しましたが、結果は1件分しか取得できず、以下のSQLが発行されていました。

  • 対象テーブルのid列とORDERした列に対してDISTINCTをかけて、対象のusers.idを取得
  • users.idで絞込をかけたSQLをLEFT OUTER JOIN付きで再取得する

と、2回SQLを発行しています。この挙動はJOINしてLIMITをかけたときだけで、JOINもLIMITもしていなければ、SQLの回数は1回だけ(だと思う)。

今回の場合は、ORDERした列であるtasks.idにもDISTINCTをかけていて、それに対してLIMIT句が効いているので、件数が合わなくなっています。

コードリーディング

ActiveRecordの該当コードは以下の部分。確かにorder_valuesに対してDISTINCTをかけています。

limited_ids_forはどこで呼び出されているかというと、apply_join_dependencyから呼び出されています。このメソッドはjoinをかけたときだけ呼び出され、さらにLIMITがかかっているときだけlimited_ids_forを使ったデータ抽出を行っています。

ということで、回避方法としては

  • LEFT JOINをした結果、usersとtasksが1:1になるようにwhere句で調整する
  • JOINしてLIMITをかけたい場合はORDER句を使わずにソート系のメソッドで頑張る
  • N+1クエリに気をつけながらassociationのスコープブロックで頑張る(joinしたらassociationのスコープブロックのorderが効かなかったので個別にassociationを呼ばないとorderが効かないっぽい)

という感じですかね。