Estoy realizando una actualización donde requiero una igualdad exacta en una tstzrange
variable. Se modifican ~ 1M filas, y la consulta tarda ~ 13 minutos. El resultado de EXPLAIN ANALYZE
se 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_range
espera 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_stats
vista de la columna, n_distinct
es -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_range
algú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_range
es 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_range
anteriores 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).
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 .
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.
(a = b)
en dos condiciones "contiene"(a @> b AND b @> a)
:? ¿Cambia el plan?(lower(t_range),upper(t_range))
ya que verifica la igualdad.Respuestas:
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_timing
y ejecutando las consultas conEXPLAIN (ANALYZE, BUFFERS)
Los diferentes planes presentan filas para actualizar en diferentes órdenes. Uno está en
trip_id
orden, 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_id
a 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.
fuente
track_io_timing
y (¡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.No estoy exactamente seguro de por qué la selectividad de un predicado de igualdad se sobreestima tan radicalmente por el índice GiST en la
tstzrange
columna. Si bien eso sigue siendo interesante per se, parece irrelevante para su caso particular.Dado que
UPDATE
modifica 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 cuentaUPDATE
.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 deUPDATE
. Esto aún sería más rápido que mantener el índice durante la granUPDATE
.Para un
UPDATE
tercio de todas las filas, probablemente también valga la pena eliminar todos los demás índices y volver a crearlos después deUPDATE
. El único inconveniente: necesita privilegios adicionales y un bloqueo exclusivo en la mesa (solo por un breve momento si lo usaCREATE 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 quetstzrange
no 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:
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:Y las consultas tendrían que usar las mismas expresiones en la
WHERE
cláusula.Uno podría tener la tentación de indexar todo el valor convertido a
text
:- pero esta expresión no es así,(cast(t_range AS text))
IMMUTABLE
ya que la representación de lostimestamptz
valores en el texto depende de latimezone
configuración actual . Tendría que poner pasos adicionales en unaIMMUTABLE
función de contenedor que produce una forma canónica, y crear un índice funcional sobre eso ...Medidas adicionales / ideas alternativas
Si
shape_dist_traveled
ya puede tener el mismo valor quett.shape_dist_traveled
para más de unas pocas de sus filas actualizadas (y no confía en los efectos secundarios de susUPDATE
desencadenantes similares ...), puede hacer que su consulta sea más rápida excluyendo actualizaciones vacías: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 FULL
sería un veneno para ti, ya que algunas tuplas muertas (o espacio reservado porFILLFACTOR
) son beneficiosas para elUPDATE
rendimiento.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:
fuente