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 reservation
tabla 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
article
? Parece que todas las columnas involucradas son de lareservation
tabla y (suponiendo) que hay un FK, el resultado debería ser el mismo.pg_stat_reset()
, y luego ejecutar la consulta, y luego mirarpg_statio_user_tables
para ver dónde atribuye los bloques.Respuestas:
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
VACUUM
la 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.fuente