[RUBY] Avez-vous vraiment besoin de cette précharge? ~ Utilisation de la charge retardée ~

Regardez d'abord le code ci-dessous.

review = Review.preload(:user, :book).find_by(id: review_id)

Que faites-vous lorsque vous voyez du code comme celui-ci? Je n'ai pas besoin d'ajouter «précharge». Je pense que je vais le souligner.

Dans cet article, j'aimerais expliquer pourquoi cette «précharge» est inutile.

Qu'est-ce que la précharge

Si vous ajoutez une précharge, vous pouvez obtenir les données associées spécifiées en même temps. Dans le cas de cet exemple, lorsque vous obtenez l'avis, vous obtenez également l'utilisateur associé et le livre en même temps.

Voici le résultat de l'exécution avec irb. Lorsque vous obtenez l'avis, l'utilisateur et le livre sont également sélectionnés et vous pouvez voir que SQL n'est pas émis là où vous l'utilisez réellement.

irb(main):011:0> review_id = 15
=> 15
irb(main):012:0> review = Review.preload(:user, :book).find_by(id: review_id)
  Review Load (0.8ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`id` = 15 LIMIT 1
  User Load (0.5ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1
  Book Load (0.8ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` = 1
=> #<Review id: 15, content: "hogehoge", user_id: 1, book_id: 1, status: "draft", created_at: "2020-06-15 14:21:23", updated_at: "2020-06-15 14:21:23">
irb(main):013:0> review.user
=> #<User id: 1, name: "1234567890", created_at: "2019-12-12 05:43:52", updated_at: "2019-12-12 05:43:52">
irb(main):014:0> review.book
=> #<Book id: 1, title: "book1", created_at: "2020-06-15 14:21:15", updated_at: "2020-06-15 14:21:15">

Quand faut-il utiliser la précharge?

Il est principalement utilisé comme contre-mesure pour N + 1. N + 1 ne sera pas décrit en détail ici, mais il s'agit d'un événement dans lequel l'acquisition SQL des données associées est émise une par une dans une boucle, etc. comme indiqué ci-dessous.

irb(main):022:0> Review.all.each do |review|
irb(main):023:1*   review.book
irb(main):024:1> end
  Review Load (0.6ms)  SELECT `reviews`.* FROM `reviews`
  Book Load (0.3ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` = 1 LIMIT 1
  Book Load (0.3ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` = 2 LIMIT 1
  Book Load (0.4ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` = 3 LIMIT 1
  Book Load (0.3ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` = 4 LIMIT 1
  Book Load (2.7ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` = 5 LIMIT 1

Dans ce qui précède, comme le livre n'est pas acquis à l'avance, SQL est publié un par un à review.book. Si vous attachez un pré-album et obtenez le livre à l'avance, ce sera comme suit.

irb(main):025:0> Review.all.preload(:book).each do |review|
irb(main):026:1*   review.book
irb(main):027:1> end
  Review Load (0.8ms)  SELECT `reviews`.* FROM `reviews`
  Book Load (0.7ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` IN (1, 2, 3, 4, 5)

Vous pouvez voir que le livre lié à la critique qui a été obtenu dans Review.all avant d'entrer dans la boucle est obtenu dans un SQL. SQL n'est pas émis pendant la boucle car il peut être acquis collectivement avant la boucle. L'émission de SQL est généralement un processus coûteux, donc un seul SQL améliore les performances. Même dans l'exemple ci-dessus, si vous regardez le temps d'exécution total de SQL, vous pouvez voir qu'il existe une différence de performances.

Pourquoi ne l'ajoutez-vous pas cette fois?

Mais qu'en est-il du premier exemple? Comme nous n'avons obtenu qu'un seul avis, il ne peut pas être N + 1 dans la boucle comme auparavant.

Je pense que le préchargement signifie que vous pouvez l'utiliser au moins plus tard. Regardons l'exemple suivant.

#Obtenir l'utilisateur
#J'utiliserai l'utilisateur et le réviserai plus tard, alors préchargez-le
review = Review.preload(:user, :book).find_by(id: review_id)

#utiliser l'utilisateur
review.user

#utiliser le livre
review.book

Comme la précharge est jointe, l'utilisateur et le livre sont également acquis lors de l'acquisition de la révision. Le résultat de l'exécution est le suivant.

irb(main):007:0> review = Review.preload(:user, :book).find_by(id: review_id)
  Review Load (0.8ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`id` = 36 LIMIT 1
  User Load (0.5ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1
  Book Load (0.4ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` = 1
=> #<Review id: 36, content: "", user_id: 1, book_id: 1, status: "draft", created_at: "2020-06-30 15:20:01", updated_at: "2020-06-30 15:20:01">
irb(main):008:0> review.user
=> #<User id: 1, name: "1234567890", created_at: "2019-12-12 05:43:52", updated_at: "2019-12-12 05:43:52">
irb(main):009:0> review.book
=> #<Book id: 1, title: "book1", created_at: "2020-06-15 14:21:15", updated_at: "2020-06-15 14:21:15">

Et si vous n'aviez pas de précharge?

irb(main):010:0> review = Review.find_by(id: review_id)
  Review Load (0.7ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`id` = 36 LIMIT 1
=> #<Review id: 36, content: "", user_id: 1, book_id: 1, status: "draft", created_at: "2020-06-30 15:20:01", updated_at: "2020-06-30 15:20:01">
irb(main):011:0> review.user
  User Load (0.5ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> #<User id: 1, name: "1234567890", created_at: "2019-12-12 05:43:52", updated_at: "2019-12-12 05:43:52">
irb(main):012:0> review.book
  Book Load (0.6ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` = 1 LIMIT 1
=> #<Book id: 1, title: "book1", created_at: "2020-06-15 14:21:15", updated_at: "2020-06-15 14:21:15">

Lorsque vous obtenez la critique, vous n'obtenez pas l'utilisateur et le livre, mais le SQL est émis là où vous l'utilisez. Cependant, comme il n'y a qu'une seule révision, le nombre de SQL émis est le même. Dans le cas de cet exemple, le rendement est le même avec ou sans précharge.

Mais qu'en est-il de l'exemple suivant?

#Obtenir l'utilisateur
#J'utiliserai l'utilisateur et le réviserai plus tard, alors préchargez-le
review = Review.preload(:user, :book).find_by(id: review_id)

#Utiliser l'utilisateur sous certaines conditions
if hoge
  review.user
end

#Utiliser le livre sous certaines conditions
if fuga
  review.book
end

Comme la précharge est jointe, l'utilisateur et le livre sont également acquis lors de l'acquisition de la révision. Si hoge ou fuga est vrai, l'utilisateur et l'avis sont utilisés, de sorte que le nombre de SQL est le même, que ce soit en préchargement ou non.

Mais qu'en est-il du faux? Par exemple, si hoge est false, l'utilisateur n'est pas utilisé, donc l'utilisateur obtenu par préchargement n'est pas utilisé. Si fuga est faux, le livre ne sera pas utilisé non plus.

Et si je n'ai pas ajouté de précharge cette fois?

#Obtenir l'utilisateur
review = Review.find_by(id: review_id)

#Utiliser l'utilisateur sous certaines conditions
if hoge
  review.user
end

#Utiliser le livre sous certaines conditions
if fuga
  review.book
end

Lorsque vous recevez un avis, vous n'obtenez ni utilisateur ni livre. Si hoge ou fuga est vrai, la critique et le livre seront acquis sur le lieu d'utilisation. Si faux, il ne sera pas récupéré.

Dans le cas de cette implémentation, il ne peut être obtenu que lors de son utilisation. À propos, une implémentation qui récupère les données en cas de besoin de cette manière est appelée chargement différé.

Saviez-vous lequel est le plus efficace? Lors du préchargement d'un modèle, le nombre de SQL est le même que lorsque le préchargement n'est pas ajouté même si tous les modèles acquis par préchargement sont utilisés. S'il y a même un modèle qui n'est pas utilisé selon les conditions, le nombre de SQL augmentera.

Si vous ne voulez obtenir qu'un seul élément comme le premier exemple, il est inutile de le précharger, mais ce sera inefficace, alors soyez prudent.

finalement

Si vous êtes nouveau sur Rails et que vous connaissez d'une manière ou d'une autre la précharge et le eager_load, vous devriez simplement l'ajouter, non? Je pense que beaucoup de gens le pensent.

De nombreux critiques soulignent N + 1, mais je pense que peu de gens signalent des précharges inutiles comme celle-ci (impression individuelle).

Il est facile de penser qu'il n'y a rien de mal à ajouter une précharge et un eager_load qui battent N + 1, mais il y a des modèles qui deviennent inefficaces comme cette fois, donc ceux qui n'en étaient pas conscients devraient en être conscients. Allons-y.

Recommended Posts

Avez-vous vraiment besoin de cette précharge? ~ Utilisation de la charge retardée ~
Avez-vous besoin d'une implémentation de Java compatible avec la mémoire?
Utilisation pratique de Map que vous ne connaissez pas de manière inattendue