LoginSignup
31
32

More than 5 years have passed since last update.

ちゃんとEager Loadingをした結果、逆に遅くなった件

Last updated at Posted at 2015-06-30

背景

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)

そもそも

本番のデータ規模で確認したほうがいいと思うので、以下のような事例を参考にしてもいいと思います

31
32
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
32