Очень TL; DR версия: Проверьте суть :)

Еще одна статья «включает»?

Многие руководства обсуждают стратегии загрузки ассоциаций Active Record, поэтому вы можете спросить: а действительно ли нам нужен другой?

Что ж, если вы похожи на меня, ничто не заставляет ваши глаза тускнеть быстрее, чем влажная дымящаяся куча машинно-сгенерированного SQL. Так что мой идеальный гид - это руководство, в котором много мотивирующих факторов и легкие записи журнала. Надеюсь, я смогу дать вам объяснение, которого у меня никогда не было.

Я предполагаю, что вы знаете, что такое ассоциация, и привыкли к SQL-соединениям. Я также предполагаю, что вы знакомы с проблемой запроса N + 1. Если что-то из этого звучит чуждо, я рекомендую сначала поиграть с простым SQL, выбирая и объединяясь.

Давайте начнем!

Почему нетерпеливый груз?

Другими словами: чем плох N + 1 запросов? Это потому, что поездки к базе данных дороги. Запросы связаны с накладными расходами, транзакции - с накладными расходами, а в производственной среде общение с удаленным сервером базы данных сопряжено с накладными расходами. Не то, что вы хотите делать в цикле.

Хорошая новость заключается в том, что если мы сообщим Active Record об ассоциациях, которые мы хотим использовать позже, он может предварительно загрузить все с помощью нескольких запросов, и любое зацикливание произойдет с записями, которые уже находятся в памяти.

#eager_load: один запрос с объединениями

Взгляните на этот пример:

Используя магию объединений, Active Record может справиться с этим с помощью одного запроса. eager_load использует LEFT OUTER JOIN, что означает, что вы получаете всех пользователей, а не только тех, у которых есть адреса.

Я избавлю вас от записи полного журнала, но знайте, что SELECT содержит много записей, таких как «user.name» AS t0_r0. Теперь вы, возможно, знали, что t0_r0 - это смайлик, который плачет под прямым углом, но знаете ли вы, что он и его многочисленные друзья делают доступными все столбцы из каждой таблицы во время запроса? Это означает, что мы можем ссылаться на атрибуты связанных моделей в where или group:

Итак, мы закончили, правда?

Этот SQL-код фабричной фермы немного уродлив, но мы ограничились одним запросом, и наши вызовы where работают. Мы все?

Что ж, оказывается, эти чудовищные запросы eager_load не только неприятны для глаз, но они также могут быть медленнее (на порядок или больше!), Чем запросы, которые создает следующий вариант. Частично это связано с тем, что объединения дороги, а частично потому, что объединения могут приводить к дублированию, а это увеличивает количество создаваемых объектов.

И Кодд поможет вам, если объединения не находятся в индексированных столбцах. ;)

#preload: запрос для каждой таблицы.

Это просто - Active Record генерирует запрос для каждой указанной таблицы. И даже с учетом накладных расходов на запросы, в целом он обычно выполняется быстрее.

Но хотя наш первый пример отлично работает с заменой preload на eager_load, наш второй, который ссылается на addresses.city в where , не работает. Active Record будет включать этот столбец в предложение WHERE в первом запросе, но поскольку он выбирает только пользователей FROM , завершенный запрос не имеет смысла.

#includes: Присоединяйтесь только при необходимости

Хотя это наиболее часто используемый метод, если вы дочитали до этого момента, он требует минимум объяснений. При использовании includes по умолчанию Active Record загружает каждую таблицу отдельно, как preload. Однако он переключается на поведение join-’em-all eager_load, если вы упоминаете связанную таблицу в вызове where или group.

  • Совет: не забывайте использовать ссылки, если вы вызываете либо со строками SQL, а не с хеш-аргументами.

#joins: ¯ \ _ (ツ) _ / ¯

Возможно, вас не будет шокировать, узнав, что объединения также генерирует запрос, который использует объединения. Но на этот раз они ВНУТРЕННИЕ СОЕДИНЕНИЯ.

Это также дает вам больше контроля над сгенерированным запросом. Помните, что eager_load и includes загружают каждый столбец из объединенных таблиц. Это верно даже при наличии select!

Часто вы достигнете объединений, когда это диктует набор результатов - когда вам нужны только записи, описываемые внутренним объединением, или когда вам нужно предложение WHERE, которое опирается на связанные таблицы, но вы этого не делаете Я не хочу загружать каждый столбец из этих таблиц.

Другими словами, хотя объединения часто встречается в статьях об активной загрузке, вы, вероятно, будете использовать его, чтобы ограничить то, что вы загружаете. Его также можно использовать вместе с select для некоторых предварительных хирургических нагрузок. Давайте посмотрим на один:

Здесь «city» становится новым методом для этих пользовательских экземпляров, потому что мы использовали псевдоним в аргументе для select.

  • Совет: это приведет к тому, что некоторые экземпляры будут иметь методы, которых нет у других того же класса. Все может запутаться, если вы также не примете дисциплину определения дополнительного метода в модели (выполнение вызова joins над дополнительным ускорением).

Обратите внимание, что при использовании объединений трюк «select + alias → новый метод» - это единственный способ загрузить значения из другой таблицы. Вызов user.address.city в этом цикле по-прежнему вызовет столько новых запросов, сколько пользователей.

Прочие оптимизации

При первом запуске я обнаружил, что было непонятно разбирать, какие ускорения исходили от каких частей Active Record. Для полноты картины давайте рассмотрим пару несвязанных приемов, которые Active Record использует для выполнения меньшего количества запросов.

  • Active Record лениво загружает, используя прокси-объекты для представления запросов, и заменяет их только наборами результатов, когда вы вызываете такой метод, как all, first , count или каждый на них . Этот дизайн также позволяет нам создавать запросы с помощью цепочки методов.
  • Active Record кэширует результаты последних запросов. Если вы запускаете один и тот же точный запрос несколько раз, к базе данных обращаются только один раз. Когда на консоли отображается CACHE вместо LOAD, это потому, что этот запрос уже был выполнен, и результаты для более поздних экземпляров были прочитаны из памяти.

К сожалению, ни один из них не помогает решить проблему N + 1, когда цикл создает несколько уникальных запросов. Чтобы решить эту проблему, нам нужна активная загрузка.

Заключительные советы

  • Обычно есть несколько способов получить одни и те же данные. Разберитесь в каждом, чтобы использовать самое быстрое.
  • При тестировании попробуйте сделать это в настройке, аналогичной вашей производственной среде - локальная база данных будет иметь гораздо меньше накладных расходов на запрос, чем удаленная.
  • Обычно вам следует индексировать столбцы, к которым вы собираетесь присоединиться, но учтите, что индексация - не панацея. Индексы замедляют вставку и обновление и даже могут замедлять чтение (когда достаточно «порядка хранения», например, запрос всей таблицы).
  • Когда активная загрузка все еще недостаточна, полезно создать хэш-карту от одной модели к другой и с ее помощью искать значения. Это может быть особенно полезно для ведущего. Это не обман :)
  • Если все это кажется слишком большим объемом ручной работы для настройки производительности, попробуйте goldiloader.

дальнейшее чтение