У меня есть массив под названием records с тысячами хешей (см. Первый массив, показанный ниже). Каждый хэш в настоящее время содержит два поля id и parent_id. Я хочу добавить новое поле под названием updated_at, которое хранится в базе данных (см. Второй массив ниже).

records = [{"id"=>3, "parent_id"=>2}, 
           {"id"=>4, "parent_id"=>2}]

records = [{"id"=>3, "parent_id"=>2, "updated_at"=>"2014-03-21 20:44:35 UTC"}, 
           {"id"=>4, "parent_id"=>2, "updated_at"=>"2014-03-21 20:44:34 UTC"}] 

Мой первый подход заключается в следующем, но он выполняет запрос к базе данных для каждого хэша, поэтому, если у меня есть 1K хэшей в массиве, он будет выполнять 1K запросов, что, на мой взгляд, не очень хорошо с точки зрения производительности Посмотреть.

records.each do |record|
  record['updated_at'] = Record.find(record['id']).updated_at.utc.to_s
end

Вы можете предложить мне лучшее решение?

2
Rafa Paez 22 Мар 2014 в 01:14
1
Хороший вопрос, чтобы следовать в закладках :-)
 – 
Arup Rakshit
22 Мар 2014 в 01:22

3 ответа

Лучший ответ

Как насчет этого? Увеличьте количество запросов, объединяя идентификаторы по кусочкам за раз. Отрегулируйте сумму each_slice на то, что работает хорошо ...

records.each_slice(250) do |records|
  ids = records.map { |r| r['id'] }
  results = Record.select([:id, :updated_at]).find(ids)
  records.each do |rec|
    result = results.find { |res| res.id == rec.id }
    rec['updated_at'] = result.updated_at.utc.to_s
  end
end
2
Nick Veys 22 Мар 2014 в 01:49
Разделение задачи «n+1 запросов» на куски по-прежнему остается проблемой «n+1 запросов».
 – 
Mark Thomas
22 Мар 2014 в 01:39
Но этот запрос «n+1» может быть намного лучше, чем выполнение 1 запроса, а затем выполнение Ruby .find в длинном списке (с элементами 1K или более). Я говорю это, потому что я уже пробовал это с 1 запросом, а затем я перенес проблему производительности с MySQL на Ruby, и это было даже медленнее, чем выполнение 1K запросов. Так что что-то смешанное может сработать.
 – 
Rafa Paez
22 Мар 2014 в 01:44
Согласовано. Вы также можете ограничить выбор .select([:id, :updated_at]). Просто добавил это к моему ответу. Если Record велико, он может выдать еще немного скорости.
 – 
Nick Veys
22 Мар 2014 в 01:50
Спасибо @Nick за ответ, это имеет смысл. Хорошее исправление, заменяющее .select .. fist на .find. Я также думаю, что вы можете получить меньше полей из вашего выбора, но не всю модель записи (вы только что поняли, прежде чем я написал это, спасибо!). Надеюсь увидеть больше подходов.
 – 
Rafa Paez
22 Мар 2014 в 01:51
1
Казалось бы, веселое упражнение для пятничного дня. :)
 – 
Nick Veys
22 Мар 2014 в 01:55

Как насчет этого?

plucked_records = Record.pluck(:id, :updated_at).find(records.map { |a| a.fetch("id") })

records.map! do |record|
  plucked_records.each do |plucked_record|
    record["updated_at"] = plucked_record.last.utc.to_s if plucked_record.first == record["id"]
  end
  record
end

Может быть, кто-то лучше импровизирует. :)

1
Kirti Thorat 22 Мар 2014 в 01:56
Спасибо @Kirti, скоро попробую. Я буду использовать select вместо pluck, так как я работаю на Rails 3, а pluck не принимает более одного аргумента в этой версии. Но оставьте как есть, потому что я не упомянул версию Rails, и она лучше, чем select для этого случая.
 – 
Rafa Paez
22 Мар 2014 в 02:08
Нет проблем. Я думал, вы используете Rails 4. Дайте мне знать.
 – 
Kirti Thorat
22 Мар 2014 в 02:10

Проведя множество тестов и попробовав разные алгоритмы, я пришел к решению, которое работает очень быстро и кажется наиболее эффективным на данный момент.

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

Время результатов было получено из тестов, запущенных с использованием массива примерно из 4,5 КБ хэшей.

# My last approach
# Converting the returning records Array into a Hash (thus faster searchs)
# Benchmarks average results: 0.5 seconds
ids = records.map { |rec| rec['id'] }
db_records = Record.select([:id, :updated_at]).find(ids)
hash_records = Hash[db_records.map { |r| [r.id, r.updated_at.utc.to_s] }]
records.each do |rec|
  rec["updated_at"] = hash_records[rec["id"]]
end

# Original approach
# Doing a SQL query for each pair (4.5K queries against MySQL)
# Benchmarks average results: ~10 seconds
records.each do |rec|
  db_rec = Record.find(pair['id'])
  rec["updated_at"] = db_rec.updated_at.utc.to_s
end

# Kirti's approach (slightly improved). Thanks Kirti! 
# Unfortunaly searching into a lar
# Doing a single SQL query for all the pairs (then find in the array)
# Benchmarks average results: ~18 seconds
ids = records.map { |rec| rec['id'] }
db_records = Record.select([:id, :updated_at]).find(ids)
records.each do |rec|
  db_rec = db_records.find { |f| f.id == pair["id"] }
  rec["updated_at"] = db_rec.updated_at.utc.to_s
end  

# Nick's approach. Thanks Nick! very good solution.
# Mixed solution levering in SQL and Ruby using each_slice.
# Very interesting results:
# [slice, seconds]:
# 5000, 18.0 
# 1000, 4.3
#  500, 2.6
#  250, 1.5
#  100, 1.0
#   50, 0.9 <- :)
#   25, 1.0
#   10, 1.8
#    5, 2.3
#    1, 10.0
# Optimal slice value is 50 elements! (for this scenario)
# An scenario with a much costly SQL query might require a higher slice number
slice = 50
records.each_slice(slice) do |recs|
  ids = recs.map { |pair| pair['id'] }
  db_records = Record.select([:id, :updated_at]).find(ids)
  recs.each do |rec|
    db_rec = db_records.find { |f| f.id == rec["id"] }
    rec["updated_at"] = db_rec.updated_at.utc.to_s
  end
end 
1
Rafa Paez 25 Мар 2014 в 16:54