Gran desajuste entre el tamaño del índice informado y el número de buffers en el plan de ejecución

10

La cuestión

Tenemos una consulta como

SELECT COUNT(1) 
  FROM article
  JOIN reservation ON a_id = r_article_id 
 WHERE r_last_modified < now() - '8 weeks'::interval 
   AND r_group_id = 1 
   AND r_status = 'OPEN';

Como se agota el tiempo de espera (después de 10 minutos) la mayoría de las veces, decidí investigar el problema.

El EXPLAIN (ANALYZE, BUFFERS)resultado se ve así:

 Aggregate  (cost=264775.48..264775.49 rows=1 width=0) (actual time=238960.290..238960.291 rows=1 loops=1)
   Buffers: shared hit=200483 read=64361 dirtied=666 written=8, temp read=3631 written=3617
   I/O Timings: read=169806.955 write=0.154
   ->  Hash Join  (cost=52413.67..264647.65 rows=51130 width=0) (actual time=1845.483..238957.588 rows=21644 loops=1)
         Hash Cond: (reservation.r_article_id = article.a_id)
         Buffers: shared hit=200483 read=64361 dirtied=666 written=8, temp read=3631 written=3617
         I/O Timings: read=169806.955 write=0.154
         ->  Index Scan using reservation_r_article_id_idx1 on reservation  (cost=0.42..205458.72 rows=51130 width=4) (actual time=34.035..237000.197 rows=21644 loops=1)
               Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
               Rows Removed by Filter: 151549
               Buffers: shared hit=200193 read=48853 dirtied=450 written=8
               I/O Timings: read=168614.105 write=0.154
         ->  Hash  (cost=29662.22..29662.22 rows=1386722 width=4) (actual time=1749.392..1749.392 rows=1386814 loops=1)
               Buckets: 32768  Batches: 8  Memory Usage: 6109kB
               Buffers: shared hit=287 read=15508 dirtied=216, temp written=3551
               I/O Timings: read=1192.850
               ->  Seq Scan on article  (cost=0.00..29662.22 rows=1386722 width=4) (actual time=23.822..1439.310 rows=1386814 loops=1)
                     Buffers: shared hit=287 read=15508 dirtied=216
                     I/O Timings: read=1192.850
 Total runtime: 238961.812 ms

El nodo del cuello de botella es obviamente el escaneo del índice. Entonces, veamos la definición del índice:

CREATE INDEX reservation_r_article_id_idx1 
    ON reservation USING btree (r_article_id)
 WHERE (r_status <> ALL (ARRAY['FULFILLED', 'CLOSED', 'CANCELED']));

Tamaños y números de fila

Su tamaño (informado por \di+o al visitar el archivo físico) es de 36 MB. Como las reservas generalmente pasan solo un tiempo relativamente corto en todos los estados no mencionados anteriormente, se están realizando muchas actualizaciones, por lo que el índice está bastante hinchado (alrededor de 24 MB se desperdician aquí), aún así, el tamaño es relativamente pequeño.

La reservationtabla tiene un tamaño de aproximadamente 3,8 GB y contiene alrededor de 40 millones de filas. El número de reservas que aún no están cerradas es de aproximadamente 170,000 (el número exacto se informa en el nodo de exploración de índice anterior).

Ahora la sorpresa: el escaneo del índice informa que obtiene una gran cantidad de buffers (es decir, páginas de 8 kb):

Buffers: shared hit=200193 read=48853 dirtied=450 written=8

¡Los números leídos del caché y el disco (o el caché del sistema operativo) suman 1.9 GB!

Peor de los casos

Por otro lado, el peor de los casos, cuando cada tupla se sienta en una página diferente de la tabla, explicaría las visitas (21644 + 151549) + 4608 páginas (filas totales obtenidas de la tabla más el número de página de índice del físico) Talla). Esto sigue siendo solo inferior a 180,000, muy por debajo de los casi 250,000 observados.

Interesante (y tal vez importante) es que la velocidad de lectura del disco es de alrededor de 2.2 MB / s, lo cual es bastante normal, supongo.

¿Y qué?

¿Alguien tiene una idea de dónde puede venir esta discrepancia?

Nota: Para ser claros, tenemos ideas sobre qué mejorar / cambiar aquí, pero realmente me gustaría entender los números que obtuve, de eso se trata la pregunta.

Actualización: comprobar el efecto del almacenamiento en caché o microvacuuming

Según la respuesta de jjanes , he comprobado lo que sucede cuando vuelvo a ejecutar la misma consulta de inmediato. El número de memorias intermedias afectadas no cambia realmente. (Para hacer esto, simplifiqué la consulta al mínimo que todavía muestra el problema). Esto es lo que veo desde la primera ejecución:

 Aggregate  (cost=240541.52..240541.53 rows=1 width=0) (actual time=97703.589..97703.590 rows=1 loops=1)
   Buffers: shared hit=413981 read=46977 dirtied=56
   I/O Timings: read=96807.444
   ->  Index Scan using reservation_r_article_id_idx1 on reservation  (cost=0.42..240380.54 rows=64392 width=0) (actual time=13.757..97698.461 rows=19236 loops=1)
         Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
         Rows Removed by Filter: 232481
         Buffers: shared hit=413981 read=46977 dirtied=56
         I/O Timings: read=96807.444
 Total runtime: 97703.694 ms

y después del segundo:

 Aggregate  (cost=240543.26..240543.27 rows=1 width=0) (actual time=388.123..388.124 rows=1 loops=1)
   Buffers: shared hit=460990
   ->  Index Scan using reservation_r_article_id_idx1 on reservation  (cost=0.42..240382.28 rows=64392 width=0) (actual time=0.032..385.900 rows=19236 loops=1)
         Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
         Rows Removed by Filter: 232584
         Buffers: shared hit=460990
 Total runtime: 388.187 ms
dezso
fuente
1
Probablemente irrelevante, pero ¿necesitas unirte article? Parece que todas las columnas involucradas son de la reservationtabla y (suponiendo) que hay un FK, el resultado debería ser el mismo.
ypercubeᵀᴹ
Esa es una muy buena pregunta. Y tiene razón, no es necesario: esta es una consulta utilizada en la supervisión por otro equipo. Aún así, al menos mirando el plan de consulta, todo lo demás es solo decoración para ese desagradable escaneo de índice :)
dezso
1
Permítanme agregar que eliminar la unión no hace una gran diferencia: el escaneo de índice exagerado permanece allí.
dezso
Acceso a la mesa de pan tostado? Aunque dudo que alguna de las columnas que muestres esté tostada. Si tiene un clon inactivo de la base de datos con fines de prueba, puede ejecutarlo pg_stat_reset(), y luego ejecutar la consulta, y luego mirar pg_statio_user_tablespara ver dónde atribuye los bloques.
jjanes

Respuestas:

4

Creo que la clave aquí es la gran cantidad de actualizaciones y la hinchazón en el índice.

El índice contiene punteros a filas en la tabla que ya no están "activas". Estas son las versiones antiguas de las filas actualizadas. Las versiones antiguas de la fila se mantienen durante un tiempo, para satisfacer las consultas con una instantánea antigua, y luego se mantienen durante un tiempo más porque nadie quiere hacer el trabajo de eliminarlas con más frecuencia de la necesaria.

Al escanear el índice, debe ir a visitar estas filas y luego se da cuenta de que ya no son visibles, por lo que las ignora. La explain (analyze,buffers)declaración no informa sobre esta actividad explícitamente, excepto a través del recuento de los buffers leídos / aciertos en el proceso de inspección de estas filas.

Existe un código de "microvacío" para btrees, de modo que cuando el escaneo vuelve al índice nuevamente, recuerda que el puntero que persiguió ya no estaba activo y lo marca como muerto en el índice. De esa manera, la siguiente consulta similar que se ejecute no necesita perseguirla nuevamente. Entonces, si ejecuta exactamente la misma consulta nuevamente, probablemente verá que los accesos al búfer caen más cerca de lo que predijo.

También puede hacer VACUUMla tabla con más frecuencia, eso eliminará las tuplas muertas de la tabla en sí, no solo del índice parcial. En general, es probable que las tablas con un índice parcial de alta rotación se beneficien de un vacío más agresivo que el nivel predeterminado.

jjanes
fuente
Por favor, vea mi edición: para mí, parece almacenamiento en caché, no microvacuum.
dezso
Sus nuevos números son muy diferentes a los anteriores (aproximadamente el doble), por lo que es difícil interpretar lo que significan sin ver también los nuevos números para las filas reales y las filas filtradas para el escaneo del índice.
jjanes
Se agregaron los planes completos como se ven hoy. El número de buffer afectado creció mucho desde el viernes, al igual que el recuento de filas.
dezso
¿Tiene transacciones de larga vida dando vueltas? Si es así, es posible que el escaneo del índice siga rastreando filas que no son visibles para él (lo que causa los impactos adicionales del búfer), pero aún no puede eliminarlos por microaspiración porque podrían ser visibles para alguien más con un viejo instantánea.
jjanes
No tengo ninguno: la transacción típica lleva menos de un segundo. Ocasionalmente, algunos segundos, pero no más.
dezso