Skip to content

Instantly share code, notes, and snippets.

@elvisgiv
Last active August 29, 2015 14:15
Show Gist options
  • Save elvisgiv/c643f3683d052c6d3988 to your computer and use it in GitHub Desktop.
Save elvisgiv/c643f3683d052c6d3988 to your computer and use it in GitHub Desktop.

#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 проблемы!!!

З.Ы. сравните время выполнения задачи для всех трех вариантов)))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment