#n+1 problem ##Что же такое проблема N+1? Эта проблема возникает, при загрузке дочерних обьектов используя ассоциацию (belongs_to-has_many). Многие ORM (ORM - технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования)по умолчанию реализуют ленивую загрузку - соответственно делается запрос на поиск одной записи для родительского обьекта и для КАЖДОЙ дочерней записи. Как вы понимаете, делая N+1 запрос вместо одного вы перенапрягаете БД, чего мы и должны избегать.
Давайте рассмотрим пример приложения, в котором содержаться девайсы, их модели, бренды и категории:
class Device < ActiveRecord::Base
belongs_to :device_model
has_many :orders
validates :title, presence: true
validates :serial, :presence => true
validates :serials_additional, :presence => false
validates :bought, presence: false
validates :device_model_id, presence: true
end
class DeviceModel < ActiveRecord::Base
belongs_to :device_category
belongs_to :device_brand
has_many :devices
end
class DeviceBrand < ActiveRecord::Base
has_many :device_models
end
class DeviceCategory < ActiveRecord::Base
has_many :device_models
end
Мы хотим отобразить список девайсов вместе с названиями их моделей, брендов и категорий:
#in our device_controller.rb
def index
@devices = Device.all
end
#in our index.html.haml
- @devices.each do |t|
%tr
%td= logger.debug t.title #я добавил logger.debug для наглядности в логах (названия девайсов)
%td= t.serial
%td= t.serials_additional
%td= t.bought
%td= t.device_model.title
%td= t.device_model.device_brand.title
%td= t.device_model.device_category.title
Смотрим лог запросов к базе данных:
Started GET "/ac/devices" for 127.0.0.1 at 2015-02-21 22:45:38 +0200
Processing by DevicesController#index as HTML
�[1m�[36mDevice Load (1.0ms)�[0m �[1mSELECT `devices`.* FROM `devices`�[0m
HTC Eclipse
�[1m�[35mDeviceModel Load (0.0ms)�[0m SELECT `device_models`.* FROM `device_models` WHERE `device_models`.`id` = 1 LIMIT 1
�[1m�[36mDeviceBrand Load (12.0ms)�[0m �[1mSELECT `device_brands`.* FROM `device_brands` WHERE `device_brands`.`id` = 4 LIMIT 1�[0m
�[1m�[35mDeviceCategory Load (1.0ms)�[0m SELECT `device_categories`.* FROM `device_categories` WHERE `device_categories`.`id` = 2 LIMIT 1
HTC Eclipse-2
�[1m�[36mDeviceModel Load (0.0ms)�[0m �[1mSELECT `device_models`.* FROM `device_models` WHERE `device_models`.`id` = 2 LIMIT 1�[0m
�[1m�[35mDeviceBrand Load (0.0ms)�[0m SELECT `device_brands`.* FROM `device_brands` WHERE `device_brands`.`id` = 1 LIMIT 1
�[1m�[36mDeviceCategory Load (0.0ms)�[0m �[1mSELECT `device_categories`.* FROM `device_categories` WHERE `device_categories`.`id` = 5 LIMIT 1�[0m
HTC Eclipse-3
�[1m�[35mDeviceModel Load (0.0ms)�[0m SELECT `device_models`.* FROM `device_models` WHERE `device_models`.`id` = 3 LIMIT 1
�[1m�[36mDeviceBrand Load (0.0ms)�[0m �[1mSELECT `device_brands`.* FROM `device_brands` WHERE `device_brands`.`id` = 2 LIMIT 1�[0m
�[1m�[35mDeviceCategory Load (0.0ms)�[0m SELECT `device_categories`.* FROM `device_categories` WHERE `device_categories`.`id` = 4 LIMIT 1
HTC Eclipse-4
�[1m�[36mDeviceModel Load (1.0ms)�[0m �[1mSELECT `device_models`.* FROM `device_models` WHERE `device_models`.`id` = 4 LIMIT 1�[0m
�[1m�[35mDeviceBrand Load (0.0ms)�[0m SELECT `device_brands`.* FROM `device_brands` WHERE `device_brands`.`id` = 3 LIMIT 1
�[1m�[36mDeviceCategory Load (0.0ms)�[0m �[1mSELECT `device_categories`.* FROM `device_categories` WHERE `device_categories`.`id` = 3 LIMIT 1�[0m
HTC Eclipse-5
�[1m�[35mDeviceModel Load (20.0ms)�[0m SELECT `device_models`.* FROM `device_models` WHERE `device_models`.`id` = 5 LIMIT 1
�[1m�[36mDeviceBrand Load (1.0ms)�[0m �[1mSELECT `device_brands`.* FROM `device_brands` WHERE `device_brands`.`id` = 5 LIMIT 1�[0m
�[1m�[35mDeviceCategory Load (1.0ms)�[0m SELECT `device_categories`.* FROM `device_categories` WHERE `device_categories`.`id` = 1 LIMIT 1
Rendered devices/index.html.haml within layouts/application (729.0ms)
�[1m�[36mUser Load (1.0ms)�[0m �[1mSELECT `users`.* FROM `users` WHERE `users`.`id` = 3 ORDER BY `users`.`id` ASC LIMIT 1�[0m
Rendered shared/_top.html.haml (117.0ms)
Rendered shared/_flash.html.haml (9.0ms)
Rendered shared/_footer.html.haml (0.0ms)
Completed 200 OK in 2427ms (Views: 1958.1ms | ActiveRecord: 110.0ms)
Мы видим, что для одной записи к девайсу мы лезем в базу каждый раз, чтобы вытащить для нее модель, бренд и категорю.
Нас это НЕ устраивает, т.к. в будущем мы не хотим ждать 5 минут открытия страницы со списком. Поэтому добавляем ЖАДНУЮ (Жадная загрузка - это механизм загрузки ассоциаций записей обьекта) загрузку в код контроллера, для этого мы используем метод includes, который и используется для описания какие ассоциативные данные должны загружаться вместе с основным телом запроса:
#in our device_controller.rb
def index
@devices = Device.includes(:device_model).all
end
Смотрим лог запросов к базе данных:
Started GET "/ac/devices" for 127.0.0.1 at 2015-02-21 22:56:51 +0200
Processing by DevicesController#index as HTML
�[1m�[35mDevice Load (0.0ms)�[0m SELECT `devices`.* FROM `devices`
�[1m�[36mDeviceModel Load (1.0ms)�[0m �[1mSELECT `device_models`.* FROM `device_models` WHERE `device_models`.`id` IN (1, 2, 3, 4, 5)�[0m
HTC Eclipse
�[1m�[35mDeviceBrand Load (1.0ms)�[0m SELECT `device_brands`.* FROM `device_brands` WHERE `device_brands`.`id` = 4 LIMIT 1
�[1m�[36mDeviceCategory Load (0.0ms)�[0m �[1mSELECT `device_categories`.* FROM `device_categories` WHERE `device_categories`.`id` = 2 LIMIT 1�[0m
HTC Eclipse-2
�[1m�[35mDeviceBrand Load (0.0ms)�[0m SELECT `device_brands`.* FROM `device_brands` WHERE `device_brands`.`id` = 1 LIMIT 1
�[1m�[36mDeviceCategory Load (0.0ms)�[0m �[1mSELECT `device_categories`.* FROM `device_categories` WHERE `device_categories`.`id` = 5 LIMIT 1�[0m
HTC Eclipse-3
�[1m�[35mDeviceBrand Load (0.0ms)�[0m SELECT `device_brands`.* FROM `device_brands` WHERE `device_brands`.`id` = 2 LIMIT 1
�[1m�[36mDeviceCategory Load (1.0ms)�[0m �[1mSELECT `device_categories`.* FROM `device_categories` WHERE `device_categories`.`id` = 4 LIMIT 1�[0m
HTC Eclipse-4
�[1m�[35mDeviceBrand Load (1.0ms)�[0m SELECT `device_brands`.* FROM `device_brands` WHERE `device_brands`.`id` = 3 LIMIT 1
�[1m�[36mDeviceCategory Load (0.0ms)�[0m �[1mSELECT `device_categories`.* FROM `device_categories` WHERE `device_categories`.`id` = 3 LIMIT 1�[0m
HTC Eclipse-5
�[1m�[35mDeviceBrand Load (0.0ms)�[0m SELECT `device_brands`.* FROM `device_brands` WHERE `device_brands`.`id` = 5 LIMIT 1
�[1m�[36mDeviceCategory Load (1.0ms)�[0m �[1mSELECT `device_categories`.* FROM `device_categories` WHERE `device_categories`.`id` = 1 LIMIT 1�[0m
Rendered devices/index.html.haml within layouts/application (160.0ms)
�[1m�[35mUser Load (0.0ms)�[0m SELECT `users`.* FROM `users` WHERE `users`.`id` = 3 ORDER BY `users`.`id` ASC LIMIT 1
Rendered shared/_top.html.haml (94.0ms)
Rendered shared/_flash.html.haml (0.0ms)
Rendered shared/_footer.html.haml (0.0ms)
Completed 200 OK in 609ms (Views: 558.0ms | ActiveRecord: 33.0ms)
Видим, что проблему с моделями девайса мы решили, НО, чтобы узнать названия бренда и категории мы продолжаем лезть в базу КАЖДЫЙ раз для КАЖДОГО девайса. ###Вложенные таблицы Вопрос - как включить записи вложенных таблиц (device_brands and device_category)???
Ответ был найден здесь:
http://stackoverflow.com/questions/24397640/rails-nested-includes-on-active-records
Теперь, еще раз заменив код в контроллере на:
#in our device_controller.rb
def index
@devices = Device.includes(:device_model => [:device_brand, :device_category]).all
end
Смотрим лог запросов к базе данных:
Started GET "/ac/devices" for 127.0.0.1 at 2015-02-21 23:06:58 +0200
Processing by DevicesController#index as HTML
�[1m�[36mDevice Load (0.0ms)�[0m �[1mSELECT `devices`.* FROM `devices`�[0m
�[1m�[35mDeviceModel Load (0.0ms)�[0m SELECT `device_models`.* FROM `device_models` WHERE `device_models`.`id` IN (1, 2, 3, 4, 5)
�[1m�[36mDeviceBrand Load (0.0ms)�[0m �[1mSELECT `device_brands`.* FROM `device_brands` WHERE `device_brands`.`id` IN (4, 1, 2, 3, 5)�[0m
�[1m�[35mDeviceCategory Load (1.0ms)�[0m SELECT `device_categories`.* FROM `device_categories` WHERE `device_categories`.`id` IN (2, 5, 4, 3, 1)
HTC Eclipse
HTC Eclipse-2
HTC Eclipse-3
HTC Eclipse-4
HTC Eclipse-5
Rendered devices/index.html.haml within layouts/application (99.0ms)
�[1m�[36mUser Load (1.0ms)�[0m �[1mSELECT `users`.* FROM `users` WHERE `users`.`id` = 3 ORDER BY `users`.`id` ASC LIMIT 1�[0m
Rendered shared/_top.html.haml (41.0ms)
Rendered shared/_flash.html.haml (0.0ms)
Rendered shared/_footer.html.haml (0.0ms)
Completed 200 OK in 356ms (Views: 310.0ms | ActiveRecord: 26.0ms)
И плачем от счастья! Мы избавились от n+1 проблемы!!!
З.Ы. сравните время выполнения задачи для всех трех вариантов)))