Короткий список практичних порад щодо того, як позбутись N+1 запитів у Rails додатку
Ця стаття є частиною серії: Посібник з оптимізації продуктивності Rails ⚡️
Що таке N+1 запит? 🤷♂️
N+1 запит — це тип проблеми з продуктивністю, яка часто виникає при використанні об'єктно-реляційного мапера (ORM), такого як Active Record у Rails додатку. Вона виникає, коли ORM виконує окремий запит для кожного об'єкта з списку об'єктів, замість того, щоб використати один запит для завантаження всіх об'єктів одразу.
Наприклад, розглянемо наступний код:
# Отримуємо всі пости
posts = Post.all
# Для кожного поста буде виконано новий запит для автора
posts.each do |post|
post.author.name
end
Якщо таблиця posts
має 1000 записів, цей код виконає 1000 окремих запитів для отримання автора кожного поста, що може бути дуже неефективним. Це приклад N+1 запиту.
N+1 запити можуть виникати, коли ви працюєте з асоціаціями, такими як has_many
або belongs_to
, якщо ви неуважно підходите до того, як завантажуєте асоційовані об'єкти.
N+1 запити можуть також значно знижувати продуктивність Rails додатку, особливо якщо запити виконують дорогі операції, такі як JOIN або агрегації. Щоб оптимізувати продуктивність Rails додатку, важливо виявити та виправити всі N+1 запити, що спричиняють проблеми з продуктивністю. Саме про це ми будемо говорити в цій статті!
Як виявити N+1 запити в Rails додатку? 🕵🏻♂️
Існує кілька способів виявити N+1 запити в Rails додатку:
- Використовувати інструмент, такий як bullet gem (або вбудований режим строгого завантаження в Rails), який може автоматично виявляти та сповіщати вас про N+1 запити.
- Використовувати
NewRelic
для моніторингу продуктивності вашого додатку і, в процесі, для виявлення N+1 запитів. - Так само, як і в New Relic, ви можете локально використовувати як rack-mini-profiler, так і stackprof gems для побудови графіків полум'я і, таким чином, виявляти N+1 запити.
- Ручно переглядати лог-файли вашого додатку і шукати повторювані запити, які виконуються.
Пам'ятайте, що не завжди можливо повністю позбутись N+1 запитів у Rails додатку. Проте, використовуючи ці інструменти та техніки, ви повинні змогти виявити N+1 запити, які спричиняють проблеми з продуктивністю.
Як виправити N+1 запити в Rails? 👨🏻🏭
Існує кілька поширених технік, які ви можете використовувати для виправлення N+1 запитів у Rails додатку:
Попереднє завантаження асоціацій (рекомендується)
Використовуйте метод includes
, щоб попередньо завантажити асоційовані записи, що дозволить зменшити кількість запитів, необхідних для завантаження даних.
Але будьте обережні! Існує три різні методи, які дозволяють досягти практично однакових результатів, але роблять це абсолютно різними способами. Це методи preload
, eager_load
і includes
.
Як правило, якщо ви не знаєте, що робите, обирайте includes
, оскільки він сам вирішить, чи розділити ваш запит на два підзапити, чи використовувати один запит з left outer join
. У будь-якому разі, я настійно рекомендую вам прочитати цю статтю від BigBinary з цього питання.
Кешування матрьошки (сміливий альтернативний підхід)
Це суперечлива думка, але можна стверджувати, що N+1 запити — це особливість Rails.
І саме це пояснює Девід, творець Ruby on Rails, у цій прямій дискусії про продуктивність разом з Нейтом Беркопеком, спеціалістом та відомим автором з продуктивності Rails:
“N+1 — це особливість” - DHH
https://www.youtube.com/watch?v=ktZLpjCanvg&t=267s (на 4:27)
Якщо ви обираєте цей підхід, можете спробувати стратегію Russian Doll Caching.
Russian Doll Caching працює, використовуючи кешування фрагментів для кешування всього відрендереного виведення сторінки чи часткового шаблону разом з будь-якими вкладеними частковими шаблонами, які він включає. Це оптимізує продуктивність, зменшуючи кількість запитів до бази даних, необхідних для відображення сторінки, а також зменшує обсяг обчислень, необхідних для генерації HTML виведення.
Ось приклад того, як можна використовувати Russian Doll Caching у Rails додатку:
# app/views/posts/show.html.erb
<% cache @post do %>
<%= @post.title %>
<%= @post.body %>
<% cache @post.comments do %> <% @post.comments.each do |comment| %> <% cache comment do %>
<%= comment.title %>
<%= comment.body %>
<% end %> <% end %> <% end %> <% end %>
У цьому прикладі, виведення шаблону posts/show
містить список коментарів до поста. Коли шаблон рендериться:
- Найзовнішній блок кешу зберігає все відрендерене виведення шаблону, використовуючи об'єкт
@post
як ключ кешу. - Другий блок кешу зберігає відрендерене виведення часткового шаблону коментарів, використовуючи об'єкт
@post.comments
як ключ кешу. - І нарешті, найглибший блок кешу зберігає відрендерене виведення кожного окремого коментаря, використовуючи об'єкт коментаря як ключ кешу.
Це дозволяє кешувати все відрендерене виведення шаблону разом з відрендереним виведенням будь-яких вкладених часткових шаблонів, використовуючи ієрархію ключів кешу. Коли шаблон рендериться знову, кеш буде перевірений на кожному рівні ієрархії, і відрендерене виведення буде використано, якщо воно доступне, що зменшує кількість запитів до бази даних, необхідних для рендерингу шаблону.
Проте є кілька аспектів, на які слід звернути увагу:
- Інвалідність кешу: За допомогою Russian Doll Caching необхідно бути обережним і правильно планувати ієрархію ключів кешу, щоб гарантувати, що ключі кешу будуть правильно інвалідовані, коли дані змінюються. Rails допомагає з цим, але якщо ви неправильно інвалідовуєте ключі кешу, можна отримати застарілі дані, що відображаються користувачам.
- Використання пам'яті: Кешування всього відрендереного виведення шаблону або часткового шаблону може споживати значну кількість пам'яті, особливо для сторінок з великою кількістю даних або складними макетами. Можливо, вам буде потрібно моніторити використання пам'яті додатку і переконатися, що у вас достатньо пам'яті для кешу.
- Витрати: Якщо ви використовуєте Russian Doll Caching для кешування великих обсягів даних у додатку з високим трафіком, можливо, вам буде потрібно використовувати велику кількість інстансів Redis або машину з великою кількістю пам'яті для підтримки кешу. Це може збільшити витрати на запуск додатку.
- Міркування щодо розгортання: Коли ви розгортаєте нову версію додатку, вам, можливо, потрібно буде очистити кеш, щоб переконатися, що нова версія правильно відображається користувачам.
Це може спричинити тимчасове погіршення продуктивності, поки кеш знову не заповниться.
В цілому, Russian Doll Caching може бути потужним інструментом для оптимізації продуктивності Rails додатку, але важливо ретельно оцінити компроміси і переконатися, що це підхід, який підходить саме вам.
Перегляди бази даних (якщо уникнути не можна)
Як останній ресурс, ви також можете створити перегляд бази даних, який попередньо агрегує дані, що потім можуть бути легше повторно використані в шаблоні.
Наприклад, припустимо, у вас є таблиця posts
і таблиця comments
, і ви хочете отримати список всіх постів разом з кількістю коментарів для кожного поста. Замість того, щоб виконувати окремий запит для кожного поста для підрахунку коментарів, ви можете створити перегляд, який попередньо об'єднує таблиці posts
та comments
і агрегує необхідну інформацію в стовпчик comment_count
.
IMHO, для більшості випадків це рішення є небажаним. Створення та підтримка переглядів бази даних супроводжується високою складністю. Вам потрібно буде написати власні SQL-запити для визначення переглядів, а потім гарантувати, що перегляди оновлюються щоразу, коли змінюються вихідні дані.
Ванільний Active Record цілком достатньо!
(і ось ще одна чудова стаття, якщо ви пропустили посилання)
Як запобігти запитам N+1 в Rails? 👨🏻⚕️
Ось кілька стратегій, які можна використовувати для запобігання запитам N+1 у Rails додатку:
Використовуйте гем bullet
(старий, але надійний)
Гем bullet
може автоматично виявляти і попереджати вас про запити N+1 у вашому додатку. Він також пропонує способи оптимізації запитів, наприклад, за допомогою вже згаданого includes
. Однак, якщо ви використовуєте більш нову версію Rails, наступний варіант може бути кращим для вас.
Використовуйте strict_loading (рекомендовано)
Strict loading було введено в Rails 6.1. До Rails 6.1 не було вбудованого способу увімкнути strict loading в Rails додатку. Вам доводилося вручну перевіряти, чи було завантажено асоціацію перед її використанням, або використовувати інструмент на кшталт гема bullet
для виявлення запитів N+1.
За замовчуванням Rails не видасть помилку, якщо ви спробуєте звернутися до асоціації, яка не була завантажена. Однак ви можете вручну використати опцію strict_loading
, щоб викликати помилку, якщо ви спробуєте звернутися до не завантаженої асоціації. Це може допомогти запобігти запитам N+1, попереджаючи вас про випадки, коли ви випадково виконали зайві запити.
Щоб використовувати strict loading у Rails додатку, ви можете встановити опцію strict_loading
на true
для асоціації, для якої хочете увімкнути strict loading.
Наприклад, припустимо, у вас є модель Post
, яка має багато моделей Comment
, і ви хочете увімкнути strict loading для асоціації comments
. Ви можете встановити опцію strict_loading
наступним чином:
# Увімкнути strict loading для асоціації comments в моделі Post
class Post < ApplicationRecord
has_many :comments, strict_loading: true
end
# Це викличе виняток StrictLoadingError, оскільки коментарі не були завантажені
Post.first.comments.each do |comment|
puts comment.body
end
Якщо ви не хочете увімкнути strict loading для всіх запитів, які надходять від цієї асоціації, ви також можете зробити це для кожного запиту окремо:
post = Post.strict_loading.first
# Це викличе ActiveRecord::StrictLoadingViolationError
post.comments.to_a
Ви також можете вибрати активувати strict loading для всього додатку або тільки для логів — це на ваш розсуд. Якщо вам цікаво більше деталей щодо цих опцій, знову ж таки рекомендую ще одну статтю BigBinary.
Ця стаття була корисною для вас? Подумайте про підписку на Medium:
[
Приєднуйтесь до Medium за моїм реферальним посиланням - Flavio Wuensche
Читайте всі історії від Flavio та тисяч інших авторів на Medium.
Ваш внесок у членство безпосередньо підтримує всіх авторів.
fwuensche.medium.com
](https://fwuensche.medium.com/membership?source=post_page-----6b30d9cfbbaf---------------------------------------)
Або просто натискайте кнопку підписки нижче ⚡️
Перекладено з: How To Find, Fix, and Prevent, N+1 Queries on Rails