От медленного к молниеносному: Оптимизация конкатенации строк в Ruby on Rails

От медленного к молниеносному: Оптимизация конкатенации строк в Ruby on Rails

Содержание
  1. Введение
  2. Обзор набора данных
  3. 1. Использование .map
  4. 2. Использование .pluck
  5. 3. Использование необработанных SQL-запросов
  6. 4. Использование Arel
  7. 5. Использование представлений SQL в Scenic
  8. 6. Использование виртуальных столбцов
  9. Анализ контрольных точек
  10. Результаты
  11. Интерпретация
  12. Рекомендации
  13. Заключение

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

Для этой статьи я использовал репозиторий, который вы можете найти здесь.

Введение

Наша задача заключается в следующем: у нас есть большое количество счетов в базе данных, и мы хотим легко извлечь основную информацию о счетах. Формат извлечения следующий:

{FirstName} {LastName} ({Role}), с вами можно связаться по {Email} или {Phone}.

Давайте вместе разберемся, как этого добиться!

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

Вот таблица, которую нам нужно экспортировать:

# 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.

Для наших тестовых данных мы будем использовать следующие семена:

# db/seeds.rb FactoryBot.define do factory :account do first_name { Faker::Name.first_name } last_name { Faker::Name.last_name } phone { Faker::PhoneNumber.phone_number } email { Faker::Internet.email } role { 'user' } end end end FactoryBot.create_list(:account, 1_000_000).

Все настроено и готово к работе!

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

Метод .map - это самый простой способ объединения полей. Он предполагает загрузку всех записей из таблицы “Account” в память, а затем использование цикла для итерации по каждой записи.

Account.all.map do |account| "#{account.first_name} #{account.last_name} (#{account.role}), с ним можно связаться по #{account.email} или #{account.phone}" end.

Однако этот подход наименее оптимизирован, поскольку загружает в память все записи из таблицы “Account”.

Честно говоря, это худший способ сделать это.

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

Метод .pluck() похож на использование .map(), но он более экономичен с точки зрения памяти. Он извлекает только значения нужных нам полей с помощью одного SQL-запроса.

Это уменьшает объем данных, загружаемых в память, что более эффективно.

Account.all.pluck(:first_name, :last_name, :role, :email, :phone).map do |info| "#{info[0]} #{info[1]} (#{info[2]}), с вами можно связаться по #{info[3]} или #{info[4]}" end.

Этот метод гораздо эффективнее первого! Но будьте внимательны, последующие методы будут еще эффективнее.

3. Использование необработанных SQL-запросов

Используя необработанные SQL-запросы, мы можем объединять поля непосредственно на уровне базы данных. Для создания конкатенированной строки мы используем функцию SQL “CONCAT”.

Account.select("CONCAT(first_name, ' ', last_name, ' (', role, ' ), можно связаться по ', email, ' или ', phone) AS insql_concatenated_string" ).map(&:insql_concatenated_string).

Этот метод очень эффективен с точки зрения производительности, поскольку конкатенация выполняется на уровне базы данных. Однако он требует использования необработанного SQL, что может сделать код менее читабельным и менее переносимым.

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

Использование Arel, языка построения SQL-запросов на основе Ruby, позволяет создавать сложные SQL-запросы.

# Нужен небольшой помощник
def to_sql(string)
  Arel.sql(string)
end

# Мы будем использовать таблицу account, поэтому я извлеку ее
account_table = Account.arel_table

# Каждый шаг по сути является тем, что вы можете сделать в сыром SQL-запросе, но это сахарный синтаксис благодаря Arel.
concatenate_arel = account_table[:first_name]
  .concat(to_sql("' '"))
  .concat(account_table[:last_name])
  .concat(to_sql("' ('"))
  .concat(account_table[:role])
  .concat(to_sql("'), можно связаться по '"))
  .concat(account_table[:email])
  .concat(to_sql("' или '"))
  .concat(account_table[:phone])
  .as('arel_concatenated_string')

# Теперь нам нужно прогнать наш select по всем записям и получить `arel_concatenated_string`
Account.select(concatenate_arel).map(&:arel_concatenated_string)

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

Недостатком Arel, на мой взгляд, является его многословность. Как видите, запрос довольно длинный. Однако он более портативен, чем необработанный SQL.

5. Использование представлений SQL в Scenic

Использование представлений SQL с камнем Scenic - это интересный подход к конкатенации.

Прежде чем мы начнем, нам нужно добавить гем в наш проект и сгенерировать наше SQL-представление.

bundle add scenic rails generate scenic:model account_information

# db/views/account_informations_v01.sql SELECT CONCAT(first_name, ' ', last_name, ' (', role, '), можно связаться по ', email, ' или ', phone) as concatenated_string FROM accounts # app/models/account_information.rb class AccountInformation < ApplicationRecord def readonly? true end end

Таким образом, наше представление становится доступным через связанную модель:

AccountInformation.pluck(:concatenated_string).

Этот подход очень мощный! Представления SQL - это концепция, которая не используется в Ruby on Rails. В данном случае запрос очень прост, мы просто конкатенируем строку. Но если ваш запрос включает в себя соединения и поиск, использование представлений SQL становится еще более актуальным!

6. Использование виртуальных столбцов

Использование виртуальных колонок - это интересный подход, который заключается в добавлении колонки “account_information” в таблицу “Account” с помощью миграции.

# db/migrate/20231018094315_add_virtual_column_on_accounts.rb class AddVirtualColumnOnAccounts < ActiveRecord::Migration[7.0] def change add_column :accounts, :account_information, :virtual, type: :string, as: "first_name || ' ' || last_name || ' (' || role || ') можно связаться по ' || email || ' или ' || phone", # stored: true end end.

После этого мы получаем доступ к нашей информации с помощью:

Account.pluck(:account_information).

Этот подход очень эффективен, поскольку виртуальный столбец синхронизируется с полями в его выражении. Если вы обновляете значение first_name, столбец account_information обновляется автоматически, без необходимости ручного обновления или дополнительных транзакций.

Примите во внимание аргумент stored: true, который позволяет сохранить значение в базе данных. В моей версии PostgreSQL аргумент stored: false не поддерживается. Когда stored имеет значение false, значение столбца пересчитывается при каждом вызове.

Анализ контрольных точек

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

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

Результаты

МеткаПользовательСистемаВсегоРеально
Использование .map10.8487561.55634412.40510012.750942
Использование .pluck1.5013430.1591921.6605351.809598
Использование Raw SQL2.7749490.4091053.1840543.425237
Использование Arel2.6024650.7268043.3292693.509664
Использование Scenic0.3585930.0308250.3894180.628602
Использование виртуальной колонны0.2957810.0416970.3374780.452611

🥇 Виртуальные колонки (в 28 раз быстрее, чем .map) 🥈 Сценические виды (в 20 раз быстрее, чем .map) 🥉 Pluck (в 7 раз быстрее, чем .map)

Честно говоря, я удивлен, что .pluck настолько быстр в масштабе!

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

.map: Этот метод является самым медленным: на обработку 650 000 записей уходит примерно 12,75 секунды. Это связано с загрузкой всех записей ActiveRecord в память.

.pluck: По сравнению с .map этот метод работает примерно в 7 раз быстрее, затрачивая на ту же задачу всего 1,81 секунды. Выбирая только необходимые столбцы, он значительно сокращает потребление памяти.

Raw SQL Query: Этот метод выполняет конкатенацию непосредственно на уровне базы данных. Он быстрее, чем .map, но медленнее, чем .pluck. Однако это решение лучше подходит для сложных запросов, требующих объединения или использования других функций SQL.

Использование Arel: Использование Arel позволяет генерировать сложные SQL-запросы в Ruby. Хотя он более многословен, он обеспечивает хорошую производительность, незначительно уступая сырому SQL.

Использование представлений Scenic: Использование SQL-представлений в Scenic примерно в 20 раз быстрее, чем .map. Это демонстрирует возможности представлений SQL для повышения производительности конкатенации. В целом, использование представлений SQL является хорошей практикой для очень больших баз данных.

Использование виртуальных колонок: Виртуальные колонки оказались самыми быстрыми, примерно в 28 раз быстрее, чем .map. Преимущество виртуальных колонок заключается в автоматической синхронизации данных с полями источника, не требуя ручного обновления или дополнительных транзакций.

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

Для очень простых запросов, связанных только с чтением, .pluck более чем достаточно. Если ваши требования еще более обширны и требуют минимальной обработки, используйте виртуальные колонки. Для очень сложных запросов, включающих несколько таблиц и требующих регулярного обновления, используйте SQL Views.

И, прежде всего, никогда не используйте .map для обработки строк из базы данных!

Заключение

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