За последнюю неделю я пытался понять, как работает рельсовая загрузка в Rails, чтобы устранить печально известную проблему N + 1 запросов за счет уменьшения числа выполненных запросов. Моя первоначальная гипотеза заключалась в том, что целью было максимально сократить количество запросов. Однако я был удивлен тем, что обнаружил.
Использование includes
для уменьшения количества запросов
В большинстве сообщений, которые вы читаете о печально известной проблеме N + 1 запросов, упоминается метод include для решения этой проблемы. includes
используется для активизации ассоциаций загрузки, связанных с моделью, с использованием минимально возможного количества запросов. Для этого под капотом используется предварительная нагрузка или левое внешнее соединение, в зависимости от ситуации. Я объясню обе ситуации в следующих разделах.
Это хорошо объясняется примерами в документации по методам запросов Active Record .
Когда и как использовать includes
?
Предположим, что наш пользователь может иметь много постов и может комментировать любые посты. Каждый пост может иметь много комментариев. Основная структура показана во фрагменте, показанном ниже:
# models/users.rb class User < ApplicationRecord has_many :posts has_many :comments end # models/posts.rb class Post < ApplicationRecord has_many :comments belongs_to :user end # models/comments.rb class Comment < ApplicationRecord belongs_to :user belongs_to :post end
Теперь, если мы хотим получить информацию о пользователях с сообщениями, сделанными пользователем, вместе с их комментариями, просто вызов User.all
сначала загрузит пользователей, а затем будет получать сообщения каждого пользователя. После получения сообщений, он будет получать комментарии, сделанные пользователем для этих сообщений. Если у нас есть 10 пользователей, каждый из которых имеет 5 сообщений, и в среднем по 2 комментария к каждому сообщению, один User.all
итоге выполнит около 1 + 5 + 10 запросов.
# users_controller.rb def index @users = User.all render json: @users end
Простым решением является использование includes
чтобы сообщить Active Record, что мы хотим получить пользователей и все связанные сообщения:
@users = User.all.includes(:posts)
Это немного повышает производительность, так как сначала выбирает пользователей, а затем в следующем запросе выбирает сообщения, связанные с этими пользователями. Теперь предыдущие 1 + 5 + 10 запросов сокращены до 1 + 1 + 10 запросов. Но это будет намного лучше, если комментарии, связанные с постами, также загружаются заранее. Это уменьшит все это до 1 + 1 + 1, всего 3 запроса для извлечения всех данных. Посмотрите на фрагмент, показанный ниже, чтобы понять:
# users_controller.rb def index @users = User.all.includes(:posts => [:comments]) render json: @users end
Все данные загружаются всего за 3 запроса: один для пользователей, один для сообщений и один для комментариев, связанных с сообщениями.
Передача комментариев в массиве указывает активной записи также предварительно загружать комментарии, относящиеся к сообщениям. Если необходимо предварительно загрузить некоторые отношения комментариев, мы можем изменить аргументы, передаваемые методам include, следующим образом:
User.all.includes(:posts => [:comments => [:another_relationship]])
Таким образом, любое количество вложенных отношений может быть предварительно загружено. Для всех вышеперечисленных запросов используется предварительная загрузка.
Получение сообщений с определенным названием
User.all.includes(:posts => [:comments]).where('posts.title = ?', some_title)
Это вызовет ошибку. В то время как,
User.all.includes(:posts => [:comments]).where(posts: { title: some_title })
даст нам ожидаемый результат. Это происходит потому, что при выполнении условий хеширования выполняется левое внешнее объединение пользователей и сообщений для извлечения пользователей с сообщениями с определенным заголовком.
Но что, если мы хотим использовать условия чистой строки или массива вместо условий хеша, чтобы указать условия для включенных отношений? Посмотрите на следующий пример:
User.all.includes(:posts => [:comments]).where('posts.title = ?', some_title).references(:posts)
Заметьте references(:posts)
часть? references
рассказывает includes
в includes
чтобы принудительно соединить связь posts
с левым внешним соединением . Чтобы понять это, посмотрите пример запроса, сгенерированного приведенной выше строкой кода:
Мы сократили количество запросов с 1 + 5 + 10 до 1 запроса. Замечательно!
Но меньше не всегда больше
Посмотрите на последние два примера запросов. Оба имеют длину от 3 до 4 строк и имеют подстроки типа t0_r1
, t0_r2
,…, t2_r5
. Это кажется необычным. Я не эксперт по SQL и не знал, что это значит. Они известны как CROSS JOIN или CARTESIAN join.
Таким образом, использование ссылок или условий хеширования для указания условий для включенных отношений может вызвать очень длинные запросы и ненужные внешние объединения, что может отрицательно повлиять на производительность и память. Вместо этого было бы выгоднее разделить большой запрос на несколько запросов.
В документации Active Record четко сказано, что когда вам нужно запрашивать ассоциации, вы должны использовать запросы на соединение с включениями вместо references
.
Несмотря на то, что Active Record позволяет вам указывать условия для загруженных ассоциаций, как и для
joins
, рекомендуется использовать вместо этого объединения.
Лучший способ указать условия для загруженных ассоциаций:
User.all.joins(:posts).where('posts.title = ? ', some_title).includes(:posts => [:comments])
Это генерирует запросы 1 + 1 + 1 и загружает только пользователей, имеющих сообщения, соответствующие заданным условиям, таким как определенный заголовок и т. Д.
Вывод
Стремительная загрузка ассоциаций может быть очень полезной и в значительной степени улучшить производительность, но также может нанести серьезный вред, когда загружено много вложенных ассоциаций. Я, например, был немного удивлен, что сокращение поездок в базу данных может на самом деле ухудшить ситуацию.
Этот пост в Engine Yard очень хорошо объясняет проблемы, связанные с энергичной загрузкой. Например, не забудьте добавить нумерацию страниц и ограничить выборку записей.
Надеюсь, вам понравился этот быстрый пост по N + 1 запросам.