背景
N+1を起こしている箇所があったので必要なデータをEager Loadingしてデプロイをした
その結果、逆にレスポンスが劣化した…
tl;dr
1:N のような関連の場合、eager_load(LEFT OUTER JOIN)した上で1の側にLIMITをかけると非常に遅いクエリが出来るケースがある
関連するモデル
Message/Attachmentの2つ
Messageは複数のAttachmentを持つ
class Message
has_many :attachments
scope :latest, -> { order(id: :desc) }
end
class Attachment
belongs_to :message
end
遅かったコード
Message.eager_load(:attachments).limit(5).latest
発行されていたSQL
SQLは2つ発行されいてた
対象となるMessage#idの抽出
SELECT
DISTINCT `messages`.`id`
FROM `messages`
LEFT OUTER JOIN `attachments`
ON `attachments`.`message_id` = `messages`.`id`
ORDER BY
`messages`.`id` DESC
LIMIT 5
実際に使うデータの読み出し
SELECT
`messages`.*,
`attachments`.*
FROM
`messages`
LEFT OUTER JOIN `attachments`
ON `attachments`.`message_id` = `messages`.`id`
WHERE
`message`.`id` IN(1, 2 3, 100, 100000)
ORDER BY `messages`.`id` DESC
何が遅かったのか?
対象となるMessage#idの抽出の中にあるDISTINCT message.id の部分
Message/AttachmentをJOINしているのでその結果は以下の表のような感じになる
このままではMessageに対してにLIMITはかけられない←Message#idの重複がある(1,2,2,5,5,5...)
そこで DISTINCT Message#id をして重複を除いた上でLIMITをかけることになる
Message | Attachement |
---|---|
1 | 1 |
2 | 5 |
2 | 7 |
5 | 10 |
5 | 11 |
5 | 18 |
6 | 22 |
6 | 23 |
手元のデータ量では全く問題ないクエリだったのだが、本番ではJOINをした結果10万件ぐらいのデータに対して
DISTNCTをかけることになり、これが非常に遅かった
解決策
eager_loadではなくpreloadを使う
Message
.preload(:attachments)
.limit(5)
.latest
発行されたクエリ
SELECT
`messages`.*
FROM
`messages`
ORDER BY `messages`.`id` DESC
LIMIT 5
SELECT
`attachments`.*
FROM
`attachments`
WHERE
`attachments`.`message_id` IN (1, 2 3, 100, 100000)
そもそも
本番のデータ規模で確認したほうがいいと思うので、以下のような事例を参考にしてもいいと思います