[RAILS] Ne sous-estimez pas le problème N + 1!

introduction

Je connaissais le "problème N + 1" jusqu'à présent, mais je n'avais jamais fait un développement à une telle échelle, j'ai donc passé mon temps sans m'en rendre compte. .. Cependant, lorsque j'ai effectivement travaillé sur le terrain, j'ai réalisé que dans le développement à grande échelle, il y avait des dizaines de milliers de données dans Zara, donc je dois en être conscient à partir de maintenant (pleurer). Je pense que tout le monde a besoin de connaissances qui sont presque toujours nécessaires quand ils entrent réellement sur le site, alors veuillez les maintenir ici! !!

Tout d'abord, quel est le problème N + 1?

En termes simples, le fait que SQL soit émis à chaque fois pendant le traitement de la boucle pose un problème et qu'une grande quantité (plus que nécessaire) de SQL est émise, ce qui entraîne de mauvaises performances. Laissez-moi vous donner une analogie. Lorsque vous souhaitez obtenir 10 produits de l'utilisateur nommé M. A et afficher la page de liste des produits de M. A,

Une fois pour obtenir les données utilisateur de M. A 10 fois pour obtenir 10 données produits Vous émettez un total de 11 requêtes pour obtenir les données que vous souhaitez afficher.

Ce n'est pas un gros problème avec environ 11 cas, mais c'est difficile s'il s'agit de 10 000 ou 20 000 cas. En supposant qu'une requête prend 0,001 seconde, 10000 ou 20000 requêtes prendront 10 ou 20 secondes pour répondre. C'est un problème d'attendre si loin, non? Ce problème peut être résolu en fonction de la manière dont le code est écrit, Si vous l'écrivez correctement, même si vous avez 10000 produits, vous pouvez l'obtenir avec deux requêtes (rires)

Méthodes de résolution de problèmes

Voyons maintenant comment résoudre ce problème. Fondamentalement, vous pouvez utiliser les quatre méthodes présentées ci-dessous. L'utilisation est légèrement différente, rendons donc possible de l'utiliser correctement.

Méthode cache Requete Utilisation Référence des données
joins ne pas faire INNER JOIN Rétrécir ça peut
eager_load Faire LEFT JOIN Trésorerie et raffinement ça peut
preload Faire SELECT chacun sans JOIN cache Ne peux pas
includes Faire Selon le cas Espèces, affiner si nécessaire ça peut

joins INNER JOIN est exécuté par défaut. Utilisez left_joins lorsque vous souhaitez effectuer une jointure externe gauche. Cette méthode ne met pas en cache, donc la mémoire peut être réduite au minimum. En outre, vous pouvez l'utiliser lorsque vous n'avez besoin que du résultat de réduction sans faire référence aux données de la destination JOIN.


User.joins(:products).where(products: { id: 1 })
# SELECT `users`.* FROM `users` INNER JOIN `products` ON `products`.`user_id` = `users`.`id` WHERE `products`.`id` = 1

eager_load Dessinez l'association spécifiée avec LEFT OUTER JOIN et mettez-la en cache. Le traitement à grande vitesse est possible car une seule requête est requise. Vous pouvez l'utiliser lorsque vous souhaitez REJOINDRE une association 1-to-1 ou N-to-1 (appartient_à, has_one), ou lorsque vous souhaitez vous référer aux informations de la table à laquelle vous vous êtes joint (par exemple, réduire par Où).


User.eager_load(:products)
# SELECT `users`.`id`, `users`.`name`, `users`.`created_at`, `users`.`updated_at`, `products`.`id`, `products`.`user_id`, `products`.`created_at`, `products`.`updated_at` FROM `users` LEFT OUTER JOIN `products` ON `products`.`user_id` = `users`.`id`

preload L'association spécifiée est divisée en plusieurs requêtes et soustraite pour la mise en cache. Cependant, il n'est pas possible de se référer aux données de la destination de l'association (comme la réduction par Où). Utilisez-le pour les associations plusieurs-à-plusieurs. En guise de mise en garde, si la quantité de données est importante, la clause IN a tendance à être importante, ce qui peut exercer une pression sur la mémoire.


User.preload(:products)
# SELECT `users`.* FROM `users`
# SELECT `products`.* FROM `products` WHERE `products`.`user_id` IN (1, 2, 3, ...)

includes En termes simples, il utilise correctement eager_load et preload. Cependant, il semble préférable de ne pas utiliser d'inclus. Parce que comprend les sortes de préchargement et eager_load bien. Si vous comprenez les caractéristiques de preload et eager_load, vous verrez rarement des inclusions. Cela peut ne pas poser de problème si vous les incluez alors que les données sont petites, mais à mesure que les données augmentent, des plis et des problèmes deviendront apparents, assurez-vous donc de connaître correctement le comportement des inclusions.


User.includes(:products)
# SELECT `users`.* FROM `users`
# SELECT `products`.* FROM `products` WHERE `products`.`user_id` IN (1, 2, 3, ...)

Maîtrisez la gemme Bullet et détectez N + 1 problèmes tôt

Peu importe à quel point vous êtes prudent, les êtres humains sortiront. Bullet est le joyau qui couvre cela.

Comment utiliser Bullet

  1. Ajoutez ce qui suit au Gemfile.

group :optimization do
  gem 'bullet', '~> 6.1.0'
end
  1. Avec l'environnement d'exécution comme optimisation, copiez development.rb et décrivez ce qui suit.

config/enviroments/optomization.rb




config.after_initialize do
  #Permet l'optimisation.
  Bullet.enable = true

  #Il est permis d'afficher le problème avec JS Alert.
  Bullet.alert = true

  #Permet la journalisation dans un fichier.
  Bullet.bullet_logger = true

  #N sur la console du navigateur+1 Vous permet de visualiser le problème.
  Bullet.console = true

  #Permet à Bullet de se connecter à Rails.
  Bullet.rails_logger = true

  #Permet d'afficher le problème dans le pied de page.
  Bullet.add_footer = true
end
  1. Ajoutez ce qui suit à webpacker.yml.

webpacker.yml



optimization:
  <<: *development
  1. Exécutez et recherchez les problèmes N + 1.

$ bundle exec rails server -e optimization

Comment définir Bullet dans un environnement de test

  1. Ajoutez ce qui suit à test.rb.

config/enviroments/test.rb



config.after_initialize do
  Bullet.enable = true
  Bullet.bullet_logger = true
  Bullet.raise = false
end
  1. Ajoutez ce qui suit à spec_helper.rb.

spec/spec_helper.rb



if Bullet.enable?
  config.before(:each) do
    Bullet.start_request
  end

  config.after(:each) do
    Bullet.perform_out_of_channel_notifications if Bullet.notification?
    Bullet.end_request
  end
end

À la fin

Jusqu'à présent, nous avons vu comment traiter le problème N + 1. Je pense que la réponse sera beaucoup plus rapide rien qu'en changeant un peu de conscience! Maintenant, développons consciemment à partir de ce moment! !! !!

référence

[Découvrez les problèmes de requête N + 1 cachés dans le code Ruby on Rails avec le gem Bullet et optimisez la réponse du site Rails](https://www.123ish.com/jp/entries/2236-ruby-on- rails% E3% 81% AE% E3% 82% B3% E3% 83% BC% E3% 83% 89% E3% 81% AB% E6% BD% 9C% E3% 82% 80n-1% E3% 82% AF% E3% 82% A8% E3% 83% AA% E5% 95% 8F% E9% A1% 8C% E3% 82% 92bullet-gem-% E3% 81% A7% E7% 99% BA% E8% A6 % 8B% E3% 81% 97% E3% 81% A6% E3% 80% 81rail% E3% 82% B5% E3% 82% A4% E3% 83% 88% E3% 81% AE% E3% 83% AC % E3% 82% B9% E3% 83% 9D% E3% 83% B3% E3% 82% B9% E3% 82% 92% E6% 9C% 80% E9% 81% A9% E5% 8C% 96)

Exécutez Bullet dans l'environnement de test

Les raisons d'utiliser correctement précharge et eager_load sans utiliser ActiveRecord incluent

L'utilisation personnelle d'ActiveRecord inclut, préchargement, eager_load

Différences entre les jointures ActiveRecord, les préchargements, les inclusions et les fichiers eager_load

Recommended Posts

Ne sous-estimez pas le problème N + 1!
[Problème N + 1]
[Débutant] Découvrez le problème N + 1! Comment utiliser Bullet
Résolvons le problème FizzBuzz!
J'ai essayé le problème FizzBuzz
Un mémorandum du problème FizzBuzz
Fonction Strict_loading pour supprimer l'occurrence de problème N + 1 ajoutée à partir des rails 6.1