¿Cómo manejar un plan de consulta incorrecto causado por la igualdad exacta en el tipo de rango?


28

Estoy realizando una actualización donde requiero una igualdad exacta en una tstzrangevariable. Se modifican ~ 1M filas, y la consulta tarda ~ 13 minutos. El resultado de EXPLAIN ANALYZEse puede ver aquí , y los resultados reales son extremadamente diferentes de los estimados por el planificador de consultas. El problema es que la exploración de índice en t_rangeespera que se devuelva una sola fila.

Esto parece estar relacionado con el hecho de que las estadísticas sobre los tipos de rango se almacenan de manera diferente a las de otros tipos. Mirando la pg_statsvista de la columna, n_distinctes -1 y otros campos (por ejemplo most_common_vals, most_common_freqs) están vacíos.

Sin embargo, debe haber estadísticas almacenadas en t_rangealgún lugar. Una actualización extremadamente similar en la que utilizo un 'dentro' en t_range en lugar de una igualdad exacta tarda aproximadamente 4 minutos en realizarse, y utiliza un plan de consulta sustancialmente diferente (ver aquí ). El segundo plan de consulta tiene sentido para mí porque se usarán todas las filas de la tabla temporal y una fracción sustancial de la tabla de historial. Más importante aún, el planificador de consultas predice un número aproximadamente correcto de filas para el filtro t_range.

La distribución de t_rangees un poco inusual. Estoy usando esta tabla para almacenar el estado histórico de otra tabla, y los cambios en la otra tabla ocurren todos a la vez en grandes volcados, por lo que no hay muchos valores distintos de t_range. Aquí están los recuentos correspondientes a cada uno de los valores únicos de t_range:

                              t_range                              |  count  
-------------------------------------------------------------------+---------
 ["2014-06-12 20:58:21.447478+00","2014-06-27 07:00:00+00")        |  994676
 ["2014-06-12 20:58:21.447478+00","2014-08-01 01:22:14.621887+00") |   36791
 ["2014-06-27 07:00:00+00","2014-08-01 07:00:01+00")               | 1000403
 ["2014-06-27 07:00:00+00",infinity)                               |   36791
 ["2014-08-01 07:00:01+00",infinity)                               |  999753

Los recuentos para los distintos t_rangeanteriores están completos, por lo que la cardinalidad es de ~ 3 M (de los cuales ~ 1 M se verá afectado por cualquiera de las consultas de actualización).

¿Por qué la consulta 1 funciona mucho peor que la consulta 2? En mi caso, la consulta 2 es un buen sustituto, pero si realmente se requiere una igualdad de rango exacta, ¿cómo puedo hacer que Postgres use un plan de consulta más inteligente?

Definición de tabla con índices (descartar columnas irrelevantes):

       Column        |   Type    |                                  Modifiers                                   
---------------------+-----------+------------------------------------------------------------------------------
 history_id          | integer   | not null default nextval('gtfs_stop_times_history_history_id_seq'::regclass)
 t_range             | tstzrange | not null
 trip_id             | text      | not null
 stop_sequence       | integer   | not null
 shape_dist_traveled | real      | 
Indexes:
    "gtfs_stop_times_history_pkey" PRIMARY KEY, btree (history_id)
    "gtfs_stop_times_history_t_range" gist (t_range)
    "gtfs_stop_times_history_trip_id" btree (trip_id)

Consulta 1:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range = '["2014-08-01 07:00:01+00",infinity)'::tstzrange;

Consulta 2:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND '2014-08-01 07:00:01+00'::timestamptz <@ sth.t_range;

Q1 actualiza 999753 filas y Q2 actualiza 999753 + 36791 = 1036544 (es decir, la tabla temporal es tal que cada fila que coincida con la condición de rango de tiempo se actualiza).

Intenté esta consulta en respuesta al comentario de @ ypercube :

Consulta 3:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range <@ '["2014-08-01 07:00:01+00",infinity)'::tstzrange
AND '["2014-08-01 07:00:01+00",infinity)'::tstzrange <@ sth.t_range;

El plan de consulta y los resultados (ver aquí ) fueron intermedios entre los dos casos anteriores (~ 6 minutos).

05/02/2016 EDITAR

Al no tener acceso a los datos después de 1,5 años, creé una tabla de prueba con la misma estructura (sin índices) y una cardinalidad similar. La respuesta de jjanes propuso que la causa podría ser el orden de la tabla temporal utilizada para la actualización. No pude probar la hipótesis directamente porque no tengo acceso track_io_timing(usando Amazon RDS).

  1. Los resultados generales fueron mucho más rápidos (por un factor de varios). Supongo que esto se debe a la eliminación de los índices, de acuerdo con la respuesta de Erwin .

  2. En este caso de prueba, las consultas 1 y 2 básicamente tomaron la misma cantidad de tiempo, porque ambas usaron la combinación de combinación. Es decir, no pude activar lo que sea que estaba causando que Postgres eligiera la combinación hash, por lo que no tengo claro por qué Postgres estaba eligiendo la combinación hash de bajo rendimiento en primer lugar.


1
¿Qué sucede si convierte la condición de igualdad (a = b)en dos condiciones "contiene" (a @> b AND b @> a):? ¿Cambia el plan?
ypercubeᵀᴹ

@ypercube: el plan cambia sustancialmente, aunque todavía no es del todo óptimo - vea mi edición # 2.
abeboparebop

1
Otra idea sería agregar un índice btree regular (lower(t_range),upper(t_range))ya que verifica la igualdad.
ypercubeᵀᴹ

Respuestas:


9

La mayor diferencia de tiempo en sus planes de ejecución está en el nodo superior, la ACTUALIZACIÓN misma. Esto sugiere que la mayor parte de su tiempo irá a IO durante la actualización. Puede verificar esto activando track_io_timingy ejecutando las consultas conEXPLAIN (ANALYZE, BUFFERS)

Los diferentes planes presentan filas para actualizar en diferentes órdenes. Uno está en trip_idorden, y el otro está en el orden en que estén físicamente presentes en la tabla temporal.

La tabla que se actualiza parece tener su orden físico correlacionado con la columna trip_id, y la actualización de filas en este orden conduce a patrones de E / S eficientes con lecturas de lectura anticipada / secuenciales. Si bien el orden físico de la tabla temporal parece conducir a muchas lecturas aleatorias.

Si puede agregar una order by trip_ida la declaración que creó la tabla temporal, eso podría resolver el problema por usted.

PostgreSQL no tiene en cuenta los efectos del pedido de E / S al planificar la operación de ACTUALIZACIÓN. (A diferencia de las operaciones SELECT, donde las tiene en cuenta). Si PostgreSQL fuera más inteligente, se daría cuenta de que un plan produce un orden más eficiente, o interpondría un nodo de clasificación explícito entre la actualización y su nodo secundario para que la actualización se alimentara de filas en orden ctid.

Tiene razón en que PostgreSQL hace un mal trabajo al estimar la selectividad de las uniones de igualdad en los rangos. Sin embargo, esto solo está relacionado tangencialmente con su problema fundamental. Una consulta más eficiente en la parte seleccionada de su actualización podría suceder accidentalmente para alimentar filas en la actualización adecuada en un mejor orden, pero si es así, eso se debe principalmente a la suerte.


Lamentablemente, no puedo modificar track_io_timingy (¡ya que ha pasado un año y medio!) Ya no tengo acceso a los datos originales. Sin embargo, probé su teoría creando tablas con el mismo esquema y un tamaño similar (millones de filas), y ejecutando dos actualizaciones diferentes: una en la que la tabla de actualización temporal se clasificó como la tabla original, y otra en la que se ordenó casi al azar. Desafortunadamente, las dos actualizaciones toman aproximadamente la misma cantidad de tiempo, lo que implica que el orden de la tabla de actualización no afecta esta consulta.
abeboparebop

7

No estoy exactamente seguro de por qué la selectividad de un predicado de igualdad se sobreestima tan radicalmente por el índice GiST en la tstzrangecolumna. Si bien eso sigue siendo interesante per se, parece irrelevante para su caso particular.

Dado que UPDATEmodifica un tercio (!) De todas las filas de 3M existentes, un índice no ayudará en absoluto . Por el contrario, la actualización incremental del índice además de la tabla agregará un costo considerable a su cuenta UPDATE.

Simplemente mantenga su consulta simple 1 . La solución simple y radical es soltar el índice antes de UPDATE. Si lo necesita para otros fines, vuelva a crearlo después de UPDATE. Esto aún sería más rápido que mantener el índice durante la gran UPDATE.

Para un UPDATEtercio de todas las filas, probablemente también valga la pena eliminar todos los demás índices y volver a crearlos después de UPDATE. El único inconveniente: necesita privilegios adicionales y un bloqueo exclusivo en la mesa (solo por un breve momento si lo usa CREATE INDEX CONCURRENTLY).

La idea de @ypercube de usar un btree en lugar del índice GiST parece buena en principio. Pero no para un tercio de todas las filas (donde no hay ningún índice bueno para empezar), y no solo en (lower(t_range),upper(t_range)), ya que tstzrangeno es un tipo de rango discreto.

La mayoría de los tipos de rango discreto tienen una forma canónica, lo que simplifica el concepto de "igualdad": el límite inferior y superior del valor en forma canónica lo define. La documentación:

Un tipo de rango discreto debe tener una función de canonicalización que tenga en cuenta el tamaño de paso deseado para el tipo de elemento. La función de canonicalización se encarga de convertir valores equivalentes del tipo de rango para tener representaciones idénticas, en particular límites consistentemente inclusivos o exclusivos. Si no se especifica una función de canonicalización, los rangos con un formato diferente siempre se tratarán como desiguales, aunque en realidad representen el mismo conjunto de valores.

El incorporada en tipos de rango int4range, int8rangey daterangetodo el uso de una forma canónica que incluye el límite inferior y excluye el límite superior; es decir, [). Sin embargo, los tipos de rango definidos por el usuario pueden usar otras convenciones.

Este no es el caso tstzrange, donde la inclusión de los límites superior e inferior debe considerarse para la igualdad. Un posible índice btree tendría que estar activado:

(lower(t_range), upper(t_range), lower_inc(t_range), upper_inc(t_range))

Y las consultas tendrían que usar las mismas expresiones en la WHEREcláusula.

Uno podría tener la tentación de indexar todo el valor convertido a text: (cast(t_range AS text))- pero esta expresión no es así, IMMUTABLEya que la representación de los timestamptzvalores en el texto depende de la timezoneconfiguración actual . Tendría que poner pasos adicionales en una IMMUTABLEfunción de contenedor que produce una forma canónica, y crear un índice funcional sobre eso ...

Medidas adicionales / ideas alternativas

Si shape_dist_traveledya puede tener el mismo valor que tt.shape_dist_traveledpara más de unas pocas de sus filas actualizadas (y no confía en los efectos secundarios de sus UPDATEdesencadenantes similares ...), puede hacer que su consulta sea más rápida excluyendo actualizaciones vacías:

WHERE ...
AND   shape_dist_traveled IS DISTINCT FROM tt.shape_dist_traveled;

Por supuesto, se aplican todos los consejos generales para la optimización del rendimiento. El Wiki de Postgres es un buen punto de partida.

VACUUM FULLsería un veneno para ti, ya que algunas tuplas muertas (o espacio reservado por FILLFACTOR) son beneficiosas para el UPDATErendimiento.

Con tantas filas actualizadas, y si puede permitírselo (sin acceso concurrente u otras dependencias), podría ser aún más rápido escribir una tabla completamente nueva en lugar de actualizar en su lugar. Instrucciones en esta respuesta relacionada:

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.