У меня есть приложение Rails, которое хранит свою конфигурацию в 34 MySQL таблицах, состоящих из различных объектов и ассоциаций, всего около 900 записей. До недавнего времени бизнес-логика c была построена на ActiveRecord, но производительность была нестабильной, потому что у меня не было достаточного контроля над количеством выполненных запросов. Недавно я «портировал» бизнес-логи c на Dry::Struct
, продублировав все задействованные классы ActiveRecord на Dry::Struct
модели значений и предварительно загрузив все объекты конфигурации внутри экземпляра Configuration
: это резко сократило количество запросов до небольшое и фиксированное количество, а также улучшенная производительность с заметным запасом, потому что вся ассоциация «ходьба» выполняется в памяти, и ее много.
Пока все хорошо, но загрузка 34 таблиц, большинство из которых мне нужно при каждом запросе, все еще занимает 34 запроса и около 160 мс. В ActiveRecord стратегия работала на низком уровне, загружая все записи во всех таблицах в виде простых хэшей, а затем инициализируя структуры с этими и сохраняя все в объекте Configuration
.
Я хотел еще больше повысить производительность Таким образом, у меня была идея собрать все данные одним запросом, сделав UNION
из всех полей во всех таблицах. Это составляет громоздкий 30-килобайтный запрос SQL, который неожиданно выполняется всего за 20 мс. Отлично! Теперь мне просто нужно развернуть эту большую структуру за ~ 10 мс, и я победил!
Ну, нет. Оказывается, что простое сканирование массива результатов занимает 48 мс (20 из которых - запрос SQL), это время увеличивается до 78 мс при разборе некоторых полей JSON тут и там и подготовке хэшей для инициализации структур. .. и затем для инициализации этих операций требуется дополнительные 89 мс. Я бы не поверил, если бы я не измерял каждый шаг, повторяя алгоритм 100 раз (после предварительного подогрева запомненных значений, конечно), но это так. В целом, по сравнению с предыдущим, гораздо более простым алгоритмом загрузки каждой таблицы отдельно, выигрыш в производительности вообще отсутствует, хотя один запрос эффективен.
Вот как выглядит SQL:
SELECT a1, a2, NULL, NULL, NULL, NULL FROM table_a
UNION ALL
SELECT NULL, NULL, b1, b2, NULL, NULL FROM table_b
UNION ALL
SELECT NULL, NULL, NULL, NULL, c1, c2 FROM table_c
, которая дает «диагональную» структуру, подобную этой
"string", 3, NULL, NULL, NULL, NULL -- from table_a
"other string", 5, NULL, NULL, NULL, NULL -- from table_a
NULL, NULL, 1, "{\"json\":true}", NULL, NULL -- from table_b
NULL, NULL, 2, NULL, NULL, NULL -- from table_b
NULL, NULL, NULL, NULL, 7, 10 -- from table_c
NULL, NULL, NULL, NULL, 9, 51 -- from table_c
, затем следующий алгоритм разворачивает ее в исходные записи:
def preload_all!
ranges = self.class.preload_field_ranges.invert
logger.measure_debug("Preloaded configuration") do
ApplicationRecord.connection.execute(self.class.preload_query).each do |data|
# finding where the first significant column is
pos = data.index { |i| !i.nil? }
# resolving the table name based on where the significant value was found, exiting early
range, table_name = ranges.select { |k, v| break [ k, v ] if pos.in?(k) }
v_class = self.class.tables_to_value_classes[table_name]
values = data[range].map do |i|
case i
when String
# horrible kludge to parse JSON fields, because I wasn't able to inspect AR
# classes to ask them which fields are serialized, any help is appreciated
case
when i[0].in?([ "{", "[" ]) then JSON.parse(i)
else i
end
else i
end
end
# assignments are for clarity, doing these operations inline shaves about 10 ms
ivar = "@#{table_name}"
hash = self.class.ar_attribute_names[table_name].zip(values).to_h
v_model = v_class.new(hash.merge(configuration: self))
vhash = instance_variable_get(ivar) || {}
instance_variable_set(ivar, vhash.tap { |h| h[v_model.id] = v_model })
end
end
self
end
Я пытался сжать код как насколько я мог, но просто удаление части v_class.new
сокращает время вдвое. Есть ли место для улучшения?
В качестве примечания, загрузка объекта Marshal
ed Закончено Configuration
из Redis занимает всего 10 мс, но я хотел избежать использования Redis для предотвращения смещения.