Руководство по производительности для создания 100 тыс. записей менее чем за 3 с с помощью Ruby on Rails

Руководство по производительности для создания 100 тыс. записей менее чем за 3 с с помощью Ruby on Rails

Содержание
  1. Обзор набора данных
  2. 1. Использование .save
  3. 2. Использование .create
  4. 3. Использование .insert_all
  5. 4. Использование .upsert_all
  6. 5. Использование ActiveRecord-Import
  7. Бенчмарк
    1. Эталон производительности
  8. Интерпретация
  9. Рекомендации
  10. Заключение

При работе над масштабными проектами быстрое создание тестовых или фиктивных данных может иметь решающее значение. В этой статье мы рассмотрим различные методы эффективного создания 100 000 записей в Ruby on Rails.

Обзор набора данных

Для сегодняшнего теста мы начнем с базы данных Postgres, использованной в моей предыдущей статье:

# db/schema.rb create_table "accounts", force: :cascade do |t| t.string "first_name" t.string "last_name" t.string "phone" t.string "email" t.string "role" end.

Кстати, если вы еще не читали мою предыдущую статью, приглашаю вас сделать это.

Чтобы тщательно проверить эффективность методов, о которых пойдет речь, я заранее сгенерирую две переменные, которые мы будем использовать:

accounts = FactoryBot.build_list(:account, 100_000) accounts_attributes = accounts.map(&:attributes).

Благодаря FactoryBot у нас есть 100 000 неперсистентных объектов ActiveRecord в переменной accounts и их атрибуты в виде хэшей в переменной accounts_attributes.

Наконец, для каждого из наших тестов мы будем использовать метод и вносить некоторые изменения, чтобы попытаться максимально увеличить производительность Ruby on Rails.

1. Использование .save

Один из самых простых способов создания записи - это использование метода .save. Для большого количества записей вы можете перебирать объекты ActiveRecord и вызывать .save для каждого экземпляра. Однако при большом количестве записей это может быть довольно медленно.

accounts.each do |account| account.save end

Этот подход может быть медленным, поскольку при каждом вызове .save выполняется SQL-запрос, что может привести к неудовлетворительной производительности.

Первый вариант метода .save, который я хотел бы протестировать, - это .save!. Теоретически, производительность .save! должна быть эквивалентна производительности .save. Это одни и те же методы, просто .save! будет вызывать исключение, если произойдет ошибка.

accounts.each do |account| account.save! end

Последний вариант, который я хотел бы протестировать для .save, включает в себя одну SQL-транзакцию. В наших двух предыдущих примерах ActiveRecord будет генерировать 100 000 транзакций с базой данных, когда захочет записать наши записи. То есть 100 000 раз он откроет соединение с базой данных, отправит данные и закроет соединение. Однако этот процесс занимает очень много времени!

Account.transaction do accounts.each do |account| account.save end end.

В этом примере кода мы можем выполнить одну транзакцию с базой данных, которая отправит все наши 100 000 записей. Это намного быстрее!

2. Использование .create

Метод .create очень похож на метод .save. Единственное отличие заключается в том, что .create принадлежит модели ActiveRecord. В нашем случае он относится к модели Account. В то время как метод .save принадлежит экземпляру нашей модели, которым является Account.new.

accounts_attributes.each do |account_attributes| Account.create(account_attributes) end.

Как правило, .create и .save должны иметь одинаковую производительность.

Второй вариант, который я хотел бы протестировать, - это использование хэшей для создания записей. Согласно документации, если передать массив хэшей в .create, то можно создавать несколько записей одновременно. Давайте вместе посмотрим, будет ли это быстрее!

Account.create(accounts_attributes)

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

Раз уж мы взялись за это, давайте проверим эффективность .create!, как мы это делали для .save и .save!.

accounts_attributes.each do |account_attributes| Account.create!(account_attributes) end.

Последний вариант, который я хотел бы протестировать, предполагает выполнение одной транзакции. Здесь применимо то же обсуждение, что и в случае с .save; мы проверим, что произойдет, если мы выполним только одну SQL-транзакцию.

Account.transaction do accounts_attributes.each do |account_attributes| Account.create!(account_attributes) end end.

3. Использование .insert_all

Ruby on Rails предоставляет метод .insert_all, который позволяет вставлять несколько записей в один SQL-запрос.

Account.insert_all(accounts_attributes)

Мы увидим в тесте производительности, но .insert_all будет очень быстрым.

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

4. Использование .upsert_all

По аналогии с .insert_all, Ruby on Rails предоставляет метод .upsert_all, который позволяет создать или обновить запись, если она уже существует в базе данных.

.upsert_all очень удобен для массовых обновлений.

Account.upsert_all(accounts_attributes)

Этот метод следует той же логике, что и insert_all; он не вызывает валидацию и обратные вызовы ActiveRecord, что значительно повышает производительность.

5. Использование ActiveRecord-Import

Для оптимальной производительности существует гем под названием activerecord-import, который добавляет волшебную функцию .import.

bundle add assetecord-import

После того как гем будет установлен, вы можете использовать его следующим образом:

Account.import(accounts_attributes).

.import - это, на мой взгляд, лучший подход, потому что activerecord-import оптимизирует производительность, минимизируя сетевые накладные расходы и позволяя блочную обработку. Кроме того, он обрабатывает валидации ActiveRecord. Таким образом, по сравнению с .insert_all и .upsert_all, он совместим с базами данных, поддерживаемыми Rails, независимо от вашей системы; он отлично интегрируется.

Бенчмарк

Теперь, когда мы знаем все эти методы, давайте выясним, какой из них самый быстрый!

Здесь указано время, необходимое для создания 100 000 записей. Делайте ваши ставки!

Эталон производительности

МеткаПользовательСистемаВсегоРеально
.save98.4820277.339702105.821729174.001099
.save!82.0368587.22173189.258589145.422204
.save! с транзакцией38.4101472.44457340.85472068.257510
.создать105.9348377.278972113.213809185.792927
.create с хэшами118.7485998.459169127.207768204.100991
.create!121.1615957.396354128.557949203.611114
.create! с транзакцией48.7882142.51046751.29868179.932584
.insert_all1.4504110.1435631.5939743.064136
.upsert_all1.4429540.1164611.5594152.935700
activerecord-import3.5113530.0823713.5937244.778761

🥇 Upsert All (в 69 раз быстрее, чем .create с хэшами) 🥈 Insert All (в 66 раз быстрее, чем .create с хэшами) 🥉 ActiveRecord-Import (в 42 раза быстрее, чем .create с хэшами)

Мне кажется очень интересным, что в целом .save немного быстрее, чем .create.

Интерпретация

.save & .save! : Методы .save и .save! очень медленные, на обработку 100 000 записей уходит от 2 до 3 минут. В основном это связано с тем, что они выполняют проверку и сохранение по очереди, что приводит к частым обращениям к базе данных.

.create & .create! : Методы .create и .create! работают немного медленнее, чем .save.

.save! с транзакцией и .create! с транзакцией : Использование транзакции значительно повышает производительность по сравнению с .save и .create, занимая около 1 минуты на обработку того же объема данных. Поэтому при работе с большим объемом данных важно использовать одну транзакцию.

.create с хэшами : Этот метод немного медленнее, чем .create, и занимает около 3 минут и 24 секунд. Он является самым медленным из всех в бенчмарке. Избегайте его в любых ситуациях!

.insert_all & .upsert_all : Методы .insert_all и .upsert_all намного быстрее предыдущих. Для обработки 100 000 записей им требуется около 3-4 секунд. Эти методы используют пакетные SQL-запросы для вставки или обновления данных. Важно отметить, что валидация и обратные вызовы в этих методах не вызываются.

ActiveRecord-Import: метод ActiveRecord-Import также отличается высокой производительностью: на обработку 100 000 записей уходит около 4,8 секунды. Важно отметить.

Рекомендации

Для создания большого количества записей я предпочту insert_all и upsert_all, если валидация не нужна. Если валидация необходима, я предпочту activerecord-import. Для большого набора данных следует избегать использования .save и .create. Однако для небольшого набора данных, если он заключен в блок транзакций, потери производительности можно свести к минимуму.

Заключение

Создание 100 000 записей в Ruby on Rails может быть непростой задачей, но с помощью правильных методов вы сможете сделать это за считанные секунды.

Для быстрого создания большого количества записей я настоятельно рекомендую использовать такие методы, как .insert_all, .upsert_all или ActiveRecord-Import. Использование транзакций также может повысить производительность таких методов, как .save! и .create!. Для оптимальной производительности очень важно выбрать метод, который соответствует вашим потребностям, и избегать более медленных последовательных методов, таких как .save и .create.