[RUBY] [Rails] N + 1 est le mal! Si cela se produit, résolvez-le pour le moment! !! Est dangereux

N+1

C'est la racine de tout mal! L'ennemi de la performance! !! C'est une mauvaise existence que vous devez repousser dès que vous la trouvez! !! !!

Je pense que beaucoup de gens le pensent.

En fait, c'est vrai, et même si vous le recherchez en ligne, vous trouverez beaucoup de savoir-faire pour éliminer N + 1.

En gros, vous pouvez le corriger en fonction du savoir-faire, mais dans de rares cas, il vaut mieux ne pas éliminer N + 1, je vais donc l'introduire avec un exemple concret.

Ce dont je veux parler dans cet article

Ce dont je veux parler dans cet article, c'est pourquoi N + 1 devrait être corrigé? à propos de ça. N + 1 est si célèbre que j'ai beaucoup de savoir-faire pour le réparer, mais je sens que j'ai oublié pourquoi je devrais le réparer. Une chose que je voudrais mentionner est que la correction de N + 1 n'est pas parce que nous voulons réduire le nombre de requêtes émises. Parce que je veux améliorer les performances! En d'autres termes, si les performances ne s'améliorent pas même si le nombre de requêtes diminue, il n'est pas nécessaire de corriger N + 1.

Qu'est-ce que N + 1

Passons d'abord en revue le N + 1 typique.

Je vais vous expliquer en utilisant le modèle ci-dessous.

class User
  has_many :articles
end

class Article
  belongs_to :user
  has_many :images
end

class Image
  belongs_to :article
end

Considérez une API qui utilise ce modèle pour obtenir une liste d'articles pour un utilisateur particulier. La réponse est la suivante

{
  articles: [
    id: 1
    body: "hogehoge"
    images: [
      {
        id: 1
        alt: "alt"
        src: "https://example.com/hoge1.img"
      }
    ]
  ]
}

Tout le monde aime ça (?) Si vous utilisez Jbuilder, vous obtiendrez l'implémentation suivante.

class ArticlesController
  def index
    #Obtenez 10 éléments dans l'ordre décroissant de la date de mise à jour(using kaminari)
    @articles = Articles.where(user_id: params[:user_id]).order(updated_at: :desc).page(params[:page]).per(10)
  end
end

# articles/index.json.jbuilder
json.articles do
  json.array!(@articles) do |article|
    json.id article.id
    json.body article.body
    json.images do
      json.array!(article.images) do |image|
        json.id image.id
        json.alt image.alt
        json.src image.src
    end
  end
end

Et si je fais ce qui précède? Comme indiqué ci-dessous, SQL pour obtenir la table des images est émis pour le nombre d'articles.

--1 requête pour obtenir des articles
SELECT `articles` FROM `articles` WHERE `articles`.`user_id` = 1 ORDER BY `articles`.`updated_at` DESC LIMIT 10

--Une requête d'acquisition d'images est émise pour le nombre d'articles
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 1
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 2
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 3
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 4
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 5
...
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 10

Rails a une fonction pour résoudre ce problème. C'est preload, ʻeager_load, ʻincludes. Je n'entrerai pas dans ces détails dans cet article, mais cette fois j'utiliserai preload pour éliminer N + 1.

class ArticlesController
  def index
    # preload(:images)ajouter à
    @articles = Articles.where(user_id: params[:user_id]).preload(:images).order(updated_at: :desc, id: :desc).page(params[:page]).per(10)
  end
end
--1 requête pour obtenir des articles
SELECT `articles` FROM `articles` WHERE `articles`.`user_id` = 1 ORDER BY `articles`.`updated_at` DESC LIMIT 10

--1 requête d'acquisition d'images
SELECT `images`.* FROM `images` WHERE `images`.`article_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Les requêtes étant coûteuses à émettre, la réduction du nombre de requêtes peut considérablement améliorer les performances. Dans cet exemple également, 11 requêtes ont été considérablement réduites à 2 requêtes.

Répulsion N + 1 terminée! !!

Modèles qui ne devraient pas éliminer N + 1

Dans l'exemple ci-dessus, N + 1 a été résolu avec succès, mais s'il y a beaucoup d'utilisateurs qui ont beaucoup d'images, cette correspondance était-elle vraiment bonne? À titre d'exemple extrême, considérons le cas où en moyenne 1 000 images sont liées par article.

Dans ce cas, si la réponse ci-dessus est laissée telle quelle, une image de 1 000 * 10 = 10 000 sera renvoyée, et la réponse deviendra trop importante et les performances se détérioreront. Par conséquent, dans la plupart des cas, la liste d'articles sera remplacée par une spécification qui renvoie une partie de l'image (comme uniquement les 5 premiers).

Changeons les spécifications. J'ai changé la partie jbuilder sans changer le contrôleur.

# articles/index.json.jbuilder
json.articles do
  json.array!(@articles) do |article|
    json.id article.id
    json.body article.body
    json.images do
      #Obtenez seulement les 5 premiers
      json.array!(article.images.first(5)) do |image|
        json.id image.id
        json.alt image.alt
        json.src image.src
    end
  end
end

Lorsqu'il est exécuté avec cela, le nombre d'images de la réponse sera jusqu'à 5 pour chaque article. Félicitations ... ce ne sera pas le cas! !! !! !!

Qu'est-ce qui ne va pas? En regardant les requêtes, seules les deux mêmes requêtes que dans l'exemple précédent ont été émises.

--1 requête pour obtenir des articles
SELECT `articles` FROM `articles` WHERE `articles`.`user_id` = 1 ORDER BY `articles`.`updated_at` DESC LIMIT 10

--1 requête d'acquisition d'images
SELECT `images`.* FROM `images` WHERE `images`.`article_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Avez-vous compris le problème? Dans cet exemple, le problème est la requête d'images qui a résolu N + 1 la dernière fois. Selon cette condition préalable, 1 000 images sont liées à un article. Cela signifie que cette requête d'images récupère 10 000 objets image. Bien qu'ActiveRecord soit pratique, la taille de l'objet est très grande. La création de 10 000 de ces objets consomme beaucoup de mémoire. De plus, cette fois je n'utiliserai que les 5 premières cartes.

Alors, que se passe-t-il si vous supprimez preload et revenez à l'état avant que N + 1 ne soit résolu?

--1 requête pour obtenir des articles
SELECT `articles` FROM `articles` WHERE `articles`.`user_id` = 1 ORDER BY `articles`.`updated_at` DESC LIMIT 10

--Une requête d'acquisition d'images est émise pour le nombre d'articles
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 1 limit 5
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 2 limit 5
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 3 limit 5
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 4 limit 5
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 5 limit 5
...
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 10 limit 5

La requête d'image sera à nouveau émise autant que le nombre d'articles, mais comme nous n'en obtiendrons que 5 chacun, le nombre d'objets ActiveRecord générés sera considérablement réduit de 10000 à 50.

Ce n'est pas absolu car cela dépend des performances de l'environnement d'exécution, mais il vaut souvent mieux économiser de la mémoire que d'éliminer N + 1. (* La réponse réellement appropriée ne peut être connue sans vérification des performances dans un environnement équivalent à l'environnement d'exécution) Si N + 1 est mauvais! Si vous pensez que vous devez absolument le corriger, lorsque vous voyez la requête d'images émise comme décrit ci-dessus, vous ajouterez une précharge, ce qui peut augmenter considérablement l'utilisation de la mémoire et entraîner une dégradation des performances. Peut être.

finalement

N + 1 est très célèbre et facile à voir, et dans Rails, il peut être résolu rapidement en ajoutant simplement précharge etc., donc je pense que cela est souvent résolu sans réfléchir profondément. Cependant, si vous envisagez de has_many comme dans cet exemple, considérez combien de cas vous pouvez vous attendre avant de l'implémenter. Si vous les obtenez tous sans tenir compte du nombre de cas, cela peut entraîner un manque de mémoire, une dégradation des performances et, dans le pire des cas, geler le serveur.

Aussi, j'ai écrit un peu au début, mais reconnaissons le but de la correction de N + 1. Souhaitez-vous confirmer que le nombre de requêtes a diminué lorsque vous corrigez N + 1 et complétez la réponse? L'élimination de N + 1 ne consiste pas à réduire les requêtes émises, mais à améliorer les performances. Ne vous contentez pas de vérifier moins de requêtes, mais assurez-vous d'améliorer les performances.

Du point de vue de l'amélioration des performances, en plus de N + 1, il existe différents points de vue tels que la réduction du nombre de processus tels que l'utilisation de la mémoire et les boucles. Gardez à l'esprit que N + 1 n'est qu'un moyen d'améliorer les performances et gardez à l'esprit d'autres perspectives.

Recommended Posts

[Rails] N + 1 est le mal! Si cela se produit, résolvez-le pour le moment! !! Est dangereux
Installez Amazon Corretto (préversion) pour le moment
Utilisez une bibliothèque Java externe pour le moment
Exécutez Dataflow, Java, Streaming pour le moment
Commande pour essayer d'utiliser Docker pour le moment