Оптимизация повторной публикации всех постов

В нашей таблице постов чуть меньше 30 миллионов записей. Из-за этого операция повторной обработки (после импорта из другого программного обеспечения для форумов и внесения изменений в форматирование Markdown) становится практически невыполнимой. Я изучаю способы оптимизации этого процесса.

Этот конкретный вызов занимает часы на моей текущей тестовой системе:

Post.update_all('baked_version = NULL')

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

Кроме того, самым большим фактором, снижающим производительность, является использование оператора OFFSET в коде. Эта строка вызывает серьёзные проблемы:

Post.order(id: :desc).offset(i).limit(batch).each do |post|

Когда количество постов исчисляется миллионами, это становится проблемой, поскольку OFFSET требует обработки всей таблицы до текущего смещения и включительно. Затем движок отбрасывает всё, что находится ниже диапазона смещения, прежде чем вернуть конечный набор результатов. Из-за этого вызов занимает несколько минут даже при наличии всего нескольких миллионов постов. Количество таких повторений зависит от размера пакета (по умолчанию жёстко задано как 1000).

Я понимаю, что задержка будет варьироваться в зависимости от конфигурации оборудования, но, думаю, ситуацию можно улучшить так, чтобы решение проблемы не сводилось к простому увеличению мощности железа.

Вместо этого я изменил этот код, чтобы он работал на основе оператора BETWEEN для диапазона обрабатываемых ID. По моему пониманию, это не должно изменить логику повторной обработки, а лишь ускорить SQL-запрос.

Фактически, в моём случае я наблюдаю улучшение на три порядка (в 1000 раз) благодаря этому изменению:

(0..(total).abs).step(batch) do |i|
      Post.order(id: :desc).where('id BETWEEN ? AND ?', i, (i + batch - 1)).each do |post|

i — это текущий индекс пакета, а BETWEEN включает границы, поэтому я исключаю последний пост в каждом пакете, чтобы избежать повторной обработки. Это должно охватить все посты, но возможно, где-то есть небольшая логическая ошибка…

Вот разница во времени обработки между оригинальной и новой версиями конечного запроса:

Оригинал:

                                                                         QUERY PLAN                                                          
-------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=12541237.13..12544635.84 rows=1000 width=783) (actual time=289375.555..289456.445 rows=1000 loops=1)
   ->  Index Scan Backward using posts_pkey on posts  (cost=0.56..100746846.76 rows=29642680 width=783) (actual time=0.038..288798.236 rows=3691000 loops=1)
         Filter: (deleted_at IS NULL)
 Planning Time: 0.175 ms
 JIT:
   Functions: 6
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 80.019 ms, Inlining 47.933 ms, Optimization 261.731 ms, Emission 106.263 ms, Total 495.947 ms
 Execution Time: 289538.294 ms
(9 rows)

Обратите внимание на количество строк в Index Scan в разделе фактического времени: 3 691 000.
Это запрос почти на 5 минут.

А вот новая версия с использованием BETWEEN:

                                                               QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------
 Index Scan Backward using posts_pkey on posts  (cost=0.56..4137.79 rows=1100 width=783) (actual time=1.956..273.467 rows=1001 loops=1)
   Index Cond: ((id >= 3690000) AND (id <= 3691000))
   Filter: (deleted_at IS NULL)
 Planning Time: 26.421 ms
 Execution Time: 274.035 ms
(5 rows)

Проверяются и возвращаются только 1000 интересующих строк. Выполняется за 274 мс вместо 289 000 мс.

Это отличное улучшение, но было бы ещё лучше, если бы процесс можно было распараллелить. Это, вероятно, немного выше моего уровня компетенции, но я мог бы придумать способ одновременного запуска нескольких процессов повторной обработки, используя остаток от деления ID (MOD) для разделения таблицы постов между различными процессами.

Буду рад любым отзывам. Можно ли улучшить это ещё больше?

дай мне GIF

Вы можете просто выполнить post.rebake! для каждого поста, требующего rebake.
Нет необходимости возиться с baked_version.

Значит, этот фрагмент кода можно удалить из задачи rake? Почему он там был, если не нужен?

Насколько я помню, baked_version существует для поддержки повторной выпечки в случае изменения логики повторной выпечки в Discourse (в отличие от случаев, когда изменено исходное содержимое поста).