Проблема N+1 в монгоиде

Я использую Mongoid для работы с MongoDB в Rails.

Я ищу что-то вроде активной записи include. В настоящее время мне не удалось найти такой метод в монгоидной форме.

Кто-нибудь знает, как решить эту проблему в mongoid или, возможно, в mongomapper, который известен как еще одна хорошая альтернатива.


person Alexey Zakharov    schedule 12.10.2010    source источник
comment
Пожалуйста, подумайте о том, чтобы переоценить свои ответы сейчас, когда прошло некоторое время. Я считаю, что вокруг этой темы есть некоторые неверные представления.   -  person tybro0103    schedule 28.10.2012


Ответы (8)


Обновление: прошло два года с тех пор, как я опубликовал этот ответ, и все изменилось. Подробности см. в ответе tybro0103.


Старый ответ

Судя по документации обоих драйверов, ни один из них не поддерживает то, что вы ищете. Вероятно потому, что это ничего не решит.

Функциональность :include ActiveRecord решает проблему N+1 для баз данных SQL. Сообщив ActiveRecord, какие связанные таблицы следует включить, он может построить один SQL-запрос, используя операторы JOIN. Это приведет к одному вызову базы данных, независимо от количества таблиц, которые вы хотите запросить.

MongoDB позволяет вам запрашивать только одну коллекцию за раз. Он не поддерживает ничего похожего на JOIN. Таким образом, даже если бы вы могли указать Mongoid, какие другие коллекции он должен включать, ему все равно пришлось бы выполнять отдельный запрос для каждой дополнительной коллекции.

person Niels van der Rest    schedule 12.10.2010
comment
Это неправильно. Опции Rails :include не выполняют соединение. Он жадно загружается. Стремительная загрузка необходима как для SQL, так и для Mongo. Это не позволяет вам запрашивать одну и ту же коллекцию снова и снова, пока вы находитесь в цикле, и вместо этого он запрашивает коллекцию один раз, прежде чем вы начнете цикл. - person tybro0103; 28.10.2012
comment
Рассмотрим представление со списком постов и под каждым постом список комментариев. Если вы не хотите загружать, вам придется запрашивать коллекции комментариев один раз для каждого сообщения (N+1). К счастью, у Mongoid есть includes()... см. мой ответ. - person tybro0103; 28.10.2012

Теперь, когда прошло некоторое время, Mongoid действительно добавил поддержку для этого. См. раздел «Нетерпеливая загрузка» здесь:
http://docs.mongodb.org/ecosystem/tutorial/ruby-mongoid-tutorial/#eager-loading

Band.includes(:albums).each do |band|
  p band.albums.first.name # Does not hit the database again.
end

Я хотел бы отметить:

  1. Rails :include не выполняет соединение
  2. SQL и Mongo нуждаются в быстрой загрузке.
  3. Проблема N+1 возникает в сценарии такого типа (запрос генерируется внутри цикла):

.

<% @posts.each do |post| %>
  <% post.comments.each do |comment| %>
    <%= comment.title %>
  <% end %>
<% end %>

Похоже, ссылка, которую разместил @amrnt, была объединена с Mongoid.

person tybro0103    schedule 11.10.2011
comment
просто чтобы добавить комментарий. Вы должны включить идентификацию карты, чтобы она работала mongoid.org/en/mongoid/docs/identity_map .html - person VP.; 17.12.2012
comment
Mongoid includes поддерживает активную загрузку прямых отношений, но не поддерживает активную загрузку двухуровневых отношений или активную загрузку полиморфного belongs_to. Если вам нужны эти функции, я создал библиотеку, которая расширяет Mongoid для поддержки этих вариантов использования. - person Maximo Mussini; 06.08.2015
comment
@ВП. mongoid5 - больше нет карты идентичности. так что больше не нужно. - person Hertzel Guinness; 13.04.2016
comment
@HertzelGuinness ага, спустя 3 года многое меняется... правильно! - person VP.; 13.04.2016

Хотя другие ответы верны, в текущих версиях Mongoid метод включения - лучший способ достичь желаемых результатов. В предыдущих версиях, где включение было недоступно, я нашел способ избавиться от проблемы n+1 и подумал, что стоит упомянуть об этом.

В моем случае это была проблема n+2.

class Judge
  include Mongoid::Document

  belongs_to :user
  belongs_to :photo

  def as_json(options={})
    {
      id: _id,
      photo: photo,
      user: user
    }
  end
end

class User
  include Mongoid::Document

  has_one :judge
end

class Photo
  include Mongoid::Document

  has_one :judge
end

действие контроллера:

def index
  @judges = Judge.where(:user_id.exists => true)
  respond_with @judges
end

Этот ответ as_json приводит к проблеме запроса n+2 из записи Judge. в моем случае предоставление серверу разработки времени отклика:

Выполнено 200 OK за 816 мс (просмотры: 785,2 мс)

Ключом к решению этой проблемы является загрузка пользователей и фотографий в одном запросе, а не по одному на каждого судью.

Вы можете сделать это, используя Mongoids IdentityMap Mongoid 2 и Mongoid 3 поддерживает эту функцию.

Сначала включите карту идентификации в файле конфигурации mongoid.yml:

development:
  host: localhost
  database: awesome_app
  identity_map_enabled: true

Теперь измените действие контроллера, чтобы вручную загрузить пользователей и фотографии. Примечание. Запись Mongoid::Relation будет лениво оценивать запрос, поэтому вы должны вызвать to_a, чтобы фактически запросить записи и сохранить их в IdentityMap.

def index
  @judges ||= Awards::Api::Judge.where(:user_id.exists => true)
  @users = User.where(:_id.in => @judges.map(&:user_id)).to_a
  @photos = Awards::Api::Judges::Photo.where(:_id.in => @judges.map(&:photo_id)).to_a
  respond_with @judges
end

Всего получается всего 3 запроса. 1 для судей, 1 для пользователей и 1 для фотографий.

Выполнено 200 OK за 559 мс (просмотры: 87,7 мс)

Как это работает? Что такое IdentityMap?

IdentityMap помогает отслеживать, какие объекты или записи уже были загружены. Поэтому, если вы получите первую запись пользователя, IdentityMap сохранит ее. Затем, если вы попытаетесь снова получить того же пользователя, Mongoid запрашивает IdentityMap для пользователя, прежде чем снова запросить базу данных. Это позволит сохранить 1 запрос в базе данных.

Таким образом, загружая всех пользователей и фотографии, которые, как мы знаем, нам понадобятся для судей json в ручных запросах, мы предварительно загружаем данные в IdentityMap все сразу. Затем, когда судья требует своего пользователя и фотографию, он проверяет IdentityMap и ему не нужно запрашивать базу данных.

person brianp    schedule 23.10.2012
comment
Почему бы вам просто не сделать Judge.where(...).includes(:photo, :user)? - person tybro0103; 10.05.2013
comment
Это было решение до того, как «включает» было добавлено в монгоид, и все еще может быть полезно для понимания. - person brianp; 05.06.2013
comment
Хотя другие ответы верны, нет метода объединения запросов между коллекциями. Я нашел способ избавиться от проблемы n + 1 ... кажется, что этот ответ решает проблему N + 1, а другие ответы - нет. Однако использование includes действительно решает проблему N + 1, и она была реализована в Mongoid более чем за год до того, как это было опубликовано, поскольку на это ссылались более старые ответы. - person tybro0103; 06.06.2013
comment
Я согласен. Я не говорил, что опубликовал это решение до того, как было добавлено слово «включает». Я заявил, что это было решением до того, как было добавлено слово «включает». и я разместил его в образовательных целях в любом случае. Я также не думаю, что что-либо в нем упоминает о том, что другие ответы не решают проблему. Почти наоборот я открываю с Другие ответы верны. - person brianp; 13.06.2013

ActiveRecord :include обычно не выполняет полное соединение для заполнения объектов Ruby. Он делает два вызова. Сначала для получения родительского объекта (скажем, сообщения), затем второго вызова для получения связанных объектов (комментариев, принадлежащих сообщению).

Mongoid работает практически так же для связанных ассоциаций.

def Post
    references_many :comments
end

def Comment
    referenced_in :post
end

В контроллере вы получаете сообщение:

@post = Post.find(params[:id])

По вашему мнению, вы повторяете комментарии:

<%- @post.comments.each do |comment| -%>
    VIEW CODE
<%- end -%>

Mongoid найдет пост в коллекции. Когда вы нажимаете итератор комментариев, он выполняет один запрос, чтобы получить комментарии. Mongoid оборачивает запрос в курсор, поэтому он является настоящим итератором и не перегружает память.

Mongoid lazy загружает все запросы, чтобы разрешить такое поведение по умолчанию. Тег :include не нужен.

person Dave South    schedule 03.12.2010
comment
Это также неверно. Рассмотрим случай, когда вы отображаете список сообщений со списком комментариев под каждым сообщением. Использование post.comments во время циклического просмотра сообщений будет генерировать новый запрос каждый раз, если только вы не используете Rails :include. К счастью, у Mongoid есть includes()... см. мой ответ. - person tybro0103; 28.10.2012

Это может помочь https://github.com/flyerhzm/mongoid-eager-loading

person amrnt    schedule 29.06.2011

Вам нужно обновить свою схему, чтобы избежать этого N + 1, в MongoDB нет решения для совместной работы.

person shingara    schedule 12.10.2010

Встраивайте подробные записи/документы в основную запись/документ.

person TTT    schedule 13.10.2010

В моем случае у меня была не вся коллекция, а ее объект, который вызвал n+1 (об этом говорит пуля).

Поэтому вместо того, чтобы писать ниже, что вызывает n+1

quote.providers.officialname

я написал

Quote.includes(:provider).find(quote._id).provider.officialname

Это не вызвало проблем, но заставило меня задуматься, если я повторяюсь или проверка n+1 не нужна для монгоида.

person Caner Çakmak    schedule 29.07.2014