Tenemos poco menos de 30 millones de publicaciones en nuestra tabla de publicaciones. Esto hace que una operación de rebake (después de importar desde un software de foro diferente y realizar modificaciones de markdown) sea bastante insostenable. Estoy investigando cómo optimizar esto.
Esta llamada en particular tarda horas en completarse en mi sistema de prueba actual:
Post.update_all('baked_version = NULL')
¿Qué hace exactamente esta bandera? El campo es un entero, pero ¿es necesario restablecerlo para todas las publicaciones antes de realizar cualquier operación de rebake? Como dije, esta única llamada tarda horas en ejecutarse debido al tamaño de nuestra tabla de publicaciones. He modificado ligeramente el código de rebake para hacer esta llamada para cada publicación justo antes de que se llame al rebake. ¿Hay una mejor manera?
Además, lo que parece ser el mayor impacto en el rendimiento es el uso de OFFSET en el código. Esta línea causa enormes problemas:
Post.order(id: :desc).offset(i).limit(batch).each do |post|
Cuando llegas a millones de publicaciones, esto se convierte en un problema porque offset requiere que se procese toda la tabla hasta el offset actual incluido. Luego, el motor descarta todo lo que está por debajo del rango de offset antes de devolver el conjunto de resultados final. Esto hace que la llamada tarde varios minutos, incluso cuando solo hay unos pocos millones de publicaciones. El número de veces que esto ocurre se basa en el tamaño del lote (codificado de forma fija en 1000 por defecto).
Me doy cuenta de que el retraso variará según la configuración del hardware, pero creo que esto se puede mejorar para que no sea necesario abordar el problema con más hardware.
En su lugar, he modificado este código para operar basándose en un operador BETWEEN en el rango de IDs que se procesan. Entiendo que esto no cambiará la lógica de un rebake en absoluto, sino que simplemente mejorará la consulta SQL a algo mucho más rápido.
De hecho, en mi caso, estoy viendo una mejora de 3 órdenes de magnitud (1000x) al realizar este cambio:
(0..(total).abs).step(batch) do |i|
Post.order(id: :desc).where('id BETWEEN ? AND ?', i, (i + batch - 1)).each do |post|
i es el índice del lote actual y BETWEEN es inclusivo, por lo que excluyo la última publicación en cada lote para que nada se procese dos veces. Esto debería obtener todas las publicaciones, pero es posible que tenga un pequeño error de lógica en alguna parte…
Aquí está la diferencia en el tiempo de procesamiento entre la versión original y la nueva de la consulta final:
Original:
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)
Observe el número de filas en el Index Scan en la sección de tiempo real: 3691000
Esta es una consulta de casi 5 minutos.
Aquí está la nueva versión usando 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)
Solo se examinan y devuelven las 1000 filas de interés. Se ejecuta en 274 ms en lugar de 289.000 ms.
Esta es una buena mejora, pero sería aún mejor si el proceso pudiera ser multihilo. Probablemente esté un poco por encima de mi experiencia, pero podría idear alguna forma de ejecutar varios procesos de rebake simultáneamente utilizando el MOD del ID para segmentar la tabla de publicaciones entre los diversos procesos.
Se aceptan comentarios. ¿Se puede mejorar aún más?
