El índice de la clave principal no se usa en la unión simple

16

Tengo las siguientes definiciones de tabla e índice:

CREATE TABLE munkalap (
    munkalap_id serial PRIMARY KEY,
    ...
);

CREATE TABLE munkalap_lepes (
    munkalap_lepes_id serial PRIMARY KEY,
    munkalap_id integer REFERENCES munkalap (munkalap_id),
    ...
);

CREATE INDEX idx_munkalap_lepes_munkalap_id ON munkalap_lepes (munkalap_id);

¿Por qué no se utiliza ninguno de los índices en munkalap_id en la siguiente consulta?

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id);

QUERY PLAN
Hash Join  (cost=119.17..2050.88 rows=38046 width=214) (actual time=0.824..18.011 rows=38046 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.005..4.574 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=3252 width=4) (actual time=0.810..0.810 rows=3253 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 115kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=3252 width=4) (actual time=0.003..0.398 rows=3253 loops=1)
Total runtime: 19.786 ms

Es lo mismo incluso si agrego un filtro:

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id) WHERE NOT lezarva;

QUERY PLAN
Hash Join  (cost=79.60..1545.79 rows=1006 width=214) (actual time=0.616..10.824 rows=964 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.007..5.061 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=86 width=4) (actual time=0.587..0.587 rows=87 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 4kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=86 width=4) (actual time=0.014..0.560 rows=87 loops=1)
              Filter: (NOT lezarva)
Total runtime: 10.911 ms
dezso
fuente

Respuestas:

21

Muchas personas han escuchado la orientación de que "los escaneos secuenciales son malos" y buscan eliminarlos de sus planes, pero no es tan simple. Si una consulta va a cubrir todas las filas de una tabla, un análisis secuencial es la forma más rápida de obtener esas filas. Esta es la razón por la que su consulta de combinación original utilizaba la exploración seq, porque se requerían todas las filas en ambas tablas.

Al planificar una consulta, el planificador de Postgres estima los costos de varias operaciones (cálculo, secuencial y E / S aleatoria) bajo diferentes esquemas posibles, y elige el plan que estima que tiene el costo más bajo. Al hacer IO desde almacenamiento rotativo (discos), IO aleatorio suele ser sustancialmente más lento que IO secuencial, la configuración de pg predeterminada para random_page_cost y seq_page_cost estima una diferencia de 4: 1 en el costo.

Estas consideraciones entran en juego cuando se considera un método de combinación o filtro que utiliza un índice frente a uno que escanea secuencialmente una tabla. Cuando se usa un índice, el plan puede encontrar una fila rápidamente a través del índice, luego debe tener en cuenta una lectura de bloque aleatorio para resolver los datos de la fila. En el caso de su segunda consulta que agregó un predicado de filtrado WHERE NOT lezarva, puede ver cómo esto afectó las estimaciones de planificación en los resultados de EXPLAIN ANALYZE. El planificador estima 1006 filas resultantes de la unión (que coincide bastante con el conjunto de resultados real de 964). Dado que la tabla más grande munkalap_lepes contiene aproximadamente 38K filas, el planificador ve que la unión tendrá que acceder a aproximadamente 1006/38046 o 1/38 de las filas de la tabla. También sabe que el ancho de fila promedio es de 214 bytes y un bloque es de 8K, por lo que hay aproximadamente 38 filas / bloque.

Con estas estadísticas, el planificador considera probable que la unión tenga que leer todos o la mayoría de los bloques de datos de la tabla. Dado que las búsquedas de índice tampoco son gratuitas, y el cálculo para escanear un bloque que evalúa una condición de filtro es muy barato en relación con IO, el planificador ha elegido escanear secuencialmente la tabla y evitar la sobrecarga del índice y las lecturas aleatorias mientras calcula el escaneo de secuencia Será más rápido.

En el mundo real, los datos a menudo están disponibles en la memoria a través de la memoria caché de la página del sistema operativo, por lo que no todas las lecturas de bloque requieren IO. Puede ser bastante difícil predecir qué tan efectivo será un caché para una consulta dada, pero el planificador de Pg usa algunas heurísticas simples. El valor de configuración efectivo_caché_tamaño informa a los planificadores de la probabilidad de incurrir en costos reales de E / S. Un valor mayor hará que calcule un costo menor para IO aleatorio y, por lo tanto, puede sesgarlo hacia un método impulsado por índice en una exploración secuencial.

dbenhur
fuente
Gracias, hasta ahora esta es la mejor (y más concisa) descripción que he leído. Aclaró algunos puntos clave.
dezso
1
Excelente explicación Sin embargo, el cálculo de la página de filas / datos está un poco fuera de lugar. Debe tener en cuenta el encabezado de página (24 bytes) + 4 bytes para cada puntero de elemento por fila + el encabezado de fila HeapTupleHeader(23 bytes por fila) + máscara de bits NULL + alineación de acuerdo con MAXALIGN. Finalmente, una cantidad desconocida de relleno debido a la alineación de datos dependiendo de los tipos de datos de las columnas y su secuencia. En total, no hay más de 33 filas en una página de 8 kb en este caso. (Sin tener en cuenta TOAST.)
Erwin Brandstetter
1
@ErwinBrandstetter Gracias por completar cálculos de tamaño de fila más exactos. Siempre supuse que el resultado de la estimación del ancho de fila por explicación incluiría consideraciones por fila como el encabezado y la máscara de bits NULL, pero no la sobrecarga del nivel de página.
dbenhur
1
@dbenhur: puede ejecutar un rápido EXPLAIN ANALYZE SELECT foo from barcon una tabla ficticia básica para verificar. Además, el espacio real en el disco depende de la alineación de datos, lo que sería difícil de tener en cuenta cuando solo se recuperan algunas filas. El ancho de fila en EXPLAINrepresenta el requisito de espacio básico para el conjunto de columnas recuperado.
Erwin Brandstetter
5

Está recuperando todas las filas de ambas tablas, por lo que no hay un beneficio real al usar una exploración de índice. Una exploración de índice solo tiene sentido si selecciona solo unas pocas filas de una tabla (generalmente menos del 10% -15%)

un caballo sin nombre
fuente
Sí, tienes razón :) Traté de aclarar la situación con un caso más específico, mira la última consulta.
dezso
@dezso: lo mismo. Si tiene un índice activado (lezarva, munkalap_id)y es lo suficientemente selectivo, entonces puede usarse. Eso NOThace que sea menos probable.
ypercubeᵀᴹ
Agregué un índice parcial basado en su sugerencia y se utiliza, por lo que se resuelve la mitad del problema. Pero yo no esperaría que el índice de la clave externa ser inútil dado que me quiero unir contra sólo 87 valores en comparación con el original 3252.
Dezso
1
@dezso Las filas promedian 214 bytes de ancho, por lo que tendrá un poco menos de 40 filas por bloque de datos de 8K. La selectividad del índice también es aproximadamente 1/40 (1006/38046). Entonces, Pg calcula que leer todos los bloques secuencialmente es más barato que la lectura probable de aproximadamente el mismo número de bloques al azar cuando se usa el índice. Estos intercambios estimados pueden estar influenciados por los valores de configuración efectivo_caché_tamaño y aleatorio_página_costo.
dbenhur
@dbenhur: ¿podrías hacer que tu comentario sea una respuesta adecuada?
dezso