Optimización de consultas en un rango de marcas de tiempo (dos columnas)

96

Yo uso PostgreSQL 9.1 en Ubuntu 12.04.

Necesito seleccionar registros dentro de un rango de tiempo: mi tabla time_limitstiene dos timestampcampos y una integerpropiedad. Hay columnas adicionales en mi tabla real que no están involucradas con esta consulta.

create table (
   start_date_time timestamp,
   end_date_time timestamp, 
   id_phi integer, 
   primary key(start_date_time, end_date_time,id_phi);

Esta tabla contiene aproximadamente 2 millones de registros.

Consultas como las siguientes tomaron enormes cantidades de tiempo:

select * from time_limits as t 
where t.id_phi=0 
and t.start_date_time <= timestamp'2010-08-08 00:00:00'
and t.end_date_time   >= timestamp'2010-08-08 00:05:00';

Así que intenté agregar otro índice, el inverso de la PK:

create index idx_inversed on time_limits(id_phi, start_date_time, end_date_time);

Tengo la impresión de que el rendimiento mejoró: el tiempo para acceder a los registros en el medio de la tabla parece ser más razonable: entre 40 y 90 segundos.

Pero todavía son varias decenas de segundos para valores en el medio del rango de tiempo. Y dos veces más al apuntar al final de la tabla (cronológicamente hablando).

Intenté explain analyzepor primera vez obtener este plan de consulta:

 Bitmap Heap Scan on time_limits  (cost=4730.38..22465.32 rows=62682 width=36) (actual time=44.446..44.446 rows=0 loops=1)
   Recheck Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
   ->  Bitmap Index Scan on idx_time_limits_phi_start_end  (cost=0.00..4714.71 rows=62682 width=0) (actual time=44.437..44.437 rows=0 loops=1)
         Index Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
 Total runtime: 44.507 ms

Ver los resultados en depesz.com.

¿Qué puedo hacer para optimizar la búsqueda? Puede ver todo el tiempo dedicado a escanear las dos columnas de marcas de tiempo una vez que id_phise establece en 0. Y no entiendo el gran escaneo (¡60K filas!) En las marcas de tiempo. ¿No están indexados por la clave principal y idx_inversedagregué?

¿Debo cambiar de tipos de marca de tiempo a otra cosa?

He leído un poco sobre los índices GIST y GIN. Supongo que pueden ser más eficientes en ciertas condiciones para tipos personalizados. ¿Es una opción viable para mi caso de uso?

Stephane Rolland
fuente
1
bueno es 45s. No sé por qué dice 45ms. Ni siquiera comenzaría a quejarme si eso fuera tan rápido como 45 ms ... :-) Tal vez un error en la salida de explicar analizar. O tal vez es el momento del análisis para realizar. No sé. Pero 40/50 segundos es lo que mido.
Stephane Rolland
2
El tiempo informado en la explain analyzesalida es el tiempo que la consulta necesitó en el servidor . Si su consulta tarda 45 segundos, el tiempo adicional se gasta transfiriendo los datos de la base de datos al programa que ejecuta la consulta Después de todo, son 62682 filas y si cada fila es grande (por ejemplo, larga varcharo textcolumnas), esto puede afectar el tiempo de transferencia drásticamente
a_horse_with_no_name
@a_horse_with_no_name: rows=62682 rowses la estimación del planificador . La consulta devuelve 0 filas. (actual time=44.446..44.446 rows=0 loops=1)
Erwin Brandstetter
@ErwinBrandstetter: ah, cierto. Pasé por alto eso. Pero aún así nunca he visto el resultado de explicar analizar mentiras sobre el tiempo de ejecución.
a_horse_with_no_name

Respuestas:

162

Para Postgres 9.1 o posterior:

CREATE INDEX idx_time_limits_ts_inverse
ON time_limits (id_phi, start_date_time, end_date_time DESC);

En la mayoría de los casos, el orden de clasificación de un índice apenas es relevante. Postgres puede escanear hacia atrás prácticamente tan rápido. Pero para las consultas de rango en varias columnas, puede hacer una gran diferencia. Estrechamente relacionada:

Considere su consulta:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    start_date_time <= '2010-08-08 00:00'
AND    end_date_time   >= '2010-08-08 00:05';

El orden de clasificación de la primera columna id_phidel índice es irrelevante. Como se verifica la igualdad ( =), debería ser lo primero. Tienes razón. Más en esta respuesta relacionada:

Postgres puede saltar id_phi = 0en muy poco tiempo y considerar las siguientes dos columnas del índice coincidente. Estos se consultan con condiciones de rango de orden inverso ( <=, >=). En mi índice, las filas de calificación son lo primero. Debería ser la forma más rápida posible con un índice B-Tree 1 :

  • Desea start_date_time <= something: index tiene la marca de tiempo más temprana primero.
    • Si califica, también verifique la columna 3.
      Repita hasta que la primera fila no califique (superrápido).
  • Desea end_date_time >= something: index tiene la última marca de tiempo primero.
    • Si califica, sigue buscando filas hasta que la primera no (súper rápido).
      Continúe con el siguiente valor para la columna 2 ..

Postgres puede escanear hacia adelante o hacia atrás. De la forma en que tenía el índice, tiene que leer todas las filas que coinciden en las dos primeras columnas y luego filtrar en la tercera. Asegúrese de leer el capítulo Índices yORDER BY el manual. Se ajusta bastante bien a tu pregunta.

¿Cuántas filas coinciden en las dos primeras columnas?
Solo unos pocos con un start_date_timeinicio cercano al rango de tiempo de la tabla. ¡Pero casi todas las filas con id_phi = 0en el extremo cronológico de la tabla! Por lo tanto, el rendimiento se deteriora con tiempos de inicio posteriores.

Estimaciones del planificador

El planificador estima rows=62682su consulta de ejemplo. De ellos, ninguno califica ( rows=0). Puede obtener mejores estimaciones si aumenta el objetivo de estadísticas para la tabla. Para 2.000.000 de filas ...

ALTER TABLE time_limits ALTER start_date_time SET STATISTICS 1000;
ALTER TABLE time_limits ALTER end_date_time   SET STATISTICS 1000;

... podría pagar. O incluso más alto. Más en esta respuesta relacionada:

Supongo que no necesita eso para id_phi(solo unos pocos valores distintos, distribuidos uniformemente), sino para las marcas de tiempo (muchos valores distintos, distribuidos de manera desigual).
Tampoco creo que importe mucho con el índice mejorado.

CLUSTER / pg_repack

Si lo desea más rápido, puede optimizar el orden físico de las filas en su tabla. Si puede permitirse bloquear su mesa exclusivamente durante un corto período de tiempo (por ejemplo, fuera de horario) para reescribir su mesa y ordenar las filas de acuerdo con el índice:

ALTER TABLE time_limits CLUSTER ON idx_time_limits_inversed;

Con acceso concurrente, considere pg_repack , que puede hacer lo mismo sin bloqueo exclusivo.

De cualquier manera, el efecto es que es necesario leer menos bloques de la tabla y todo está ordenado previamente. Es un efecto de una sola vez que se deteriora con el tiempo con escrituras en la tabla que fragmentan el orden físico.

Índice GiST en Postgres 9.2+

1 Con la página 9.2+ hay otra opción, posiblemente más rápida: un índice GiST para una columna de rango.

  • Hay tipos de rango integrados para timestampy timestamp with time zone: tsrange,tstzrange . Un índice btree suele ser más rápido para una integercolumna adicional como id_phi. Más pequeño y más barato de mantener, también. Pero la consulta probablemente seguirá siendo más rápida en general con el índice combinado.

  • Cambie la definición de su tabla o use un índice de expresión .

  • Para el índice GiST multicolumna disponible, también necesita el módulo adicional btree_gistinstalado (una vez por base de datos) que proporciona las clases de operador para incluir un integer.

La trifecta! Un índice GiST funcional de varias columnas :

CREATE EXTENSION IF NOT EXISTS btree_gist;  -- if not installed, yet

CREATE INDEX idx_time_limits_funky ON time_limits USING gist
(id_phi, tsrange(start_date_time, end_date_time, '[]'));

Utilice el operador "contiene rango"@> en su consulta ahora:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    tsrange(start_date_time, end_date_time, '[]')
    @> tsrange('2010-08-08 00:00', '2010-08-08 00:05', '[]')

Índice SP-GiST en Postgres 9.3+

Un índice SP-GiST podría ser aún más rápido para este tipo de consulta, excepto que, citando el manual :

Actualmente, solo los tipos de índice B-tree, GiST, GIN y BRIN admiten índices de varias columnas.

Sigue siendo cierto en Postgres 12.
Tendría que combinar un spgistíndice solo (tsrange(...))con un segundo btreeíndice encendido (id_phi). Con la sobrecarga adicional, no estoy seguro de que esto pueda competir.
Respuesta relacionada con un punto de referencia para solo una tsrangecolumna:

Erwin Brandstetter
fuente
78
Debo decir esto al menos solo una vez, que cada una de sus respuestas en SO y DBA tiene un alto valor agregado / expresión , y la mayoría de las veces es la más completa. Solo para decirlo una vez: ¡Respeto !.
Stephane Rolland
1
Merci bien! :) ¿Entonces obtuviste resultados más rápidos?
Erwin Brandstetter
Tengo que dejar que termine la copia masiva generada a partir de la consulta intensamente incómoda mía, por lo que hacer que el proceso sea realmente lento, estuvo cambiando durante horas antes de hacer la pregunta. Pero lo calculé y decidí dejarlo girar hasta mañana por la mañana, estará terminado y la nueva mesa lista para llenarse mañana. Intenté crear su índice simultáneamente durante el trabajo, pero debido a demasiado acceso (creo), la creación del índice debería estar bloqueada. Repetiré este mismo tiempo de prueba nuevamente mañana con su solución. También he visto cómo actualizar a 9.2 ;-) para debian / ubuntu.
Stephane Rolland
2
@StephaneRolland: aún sería interesante por qué la salida de análisis de explicación muestra 45 milisegundos mientras ve que la consulta tarda más de 40 segundos.
a_horse_with_no_name
1
@John: Postgres puede atravesar un índice hacia adelante o hacia atrás, pero no puede cambiar de dirección en el mismo escaneo. Idealmente, tiene todas las filas calificadas por nodo primero (o último), pero tiene que ser la misma alineación (predicados de consulta coincidentes) para que todas las columnas obtengan los mejores resultados.
Erwin Brandstetter
5

La respuesta de Erwin ya es exhaustiva, sin embargo:

Los tipos de rango para marcas de tiempo están disponibles en PostgreSQL 9.1 con la extensión Temporal de Jeff Davis: https://github.com/jeff-davis/PostgreSQL-Temporal

Nota: tiene funciones limitadas (usa Timestamptz, y solo puede tener el estilo '[)' superpuesto afaik). Además, hay muchas otras buenas razones para actualizar a PostgreSQL 9.2.

nathan-m
fuente
3

Podría intentar crear el índice de varias columnas en un orden diferente:

primary key(id_phi, start_date_time,end_date_time);

Publiqué una vez una pregunta similar también relacionada con el orden de los índices en un índice de varias columnas. La clave es tratar de usar primero las condiciones más restrictivas para reducir el espacio de búsqueda.

Editar : Mi error. Ahora veo que ya tienes este índice definido.

jap1968
fuente
Ya tengo ambos índices. Excepto que la clave primaria es la otra, pero el índice que propone ya existe, y es el que se usa si mira la explicación:Bitmap Index Scan on idx_time_limits_phi_start_end
Stephane Rolland
1

Logré aumentar rápidamente (de 1 segundo a 70 ms)

Tengo una tabla con agregaciones de muchas medidas y muchos niveles ( lcolumna) (30s, 1m, 1h, etc.), hay dos columnas de rango: $spara el inicio y $epara el final.

Creé dos índices de varias columnas: uno para inicio y otro para final.

Ajusté la consulta de selección: seleccione los rangos donde su límite inicial está en el rango dado. además, seleccione rangos donde su límite final esté en un rango dado.

Explain muestra dos flujos de filas utilizando nuestros índices de manera eficiente.

Índices:

drop index if exists agg_search_a;
CREATE INDEX agg_search_a
ON agg (measurement_id, l, "$s");

drop index if exists agg_search_b;
CREATE INDEX agg_search_b
ON agg (measurement_id, l, "$e");

Seleccionar consulta:

select "$s", "$e", a, t, b, c from agg
where 
    measurement_id=0 
    and l =  '30s'
    and (
        (
            "$s" > '2013-05-01 02:05:05'
            and "$s" < '2013-05-01 02:18:15'
        )
        or 
        (
             "$e" > '2013-05-01 02:00:05'
            and "$e" < '2013-05-01 02:18:05'
        )
    )

;

Explique:

[
  {
    "Execution Time": 0.058,
    "Planning Time": 0.112,
    "Plan": {
      "Startup Cost": 10.18,
      "Rows Removed by Index Recheck": 0,
      "Actual Rows": 37,
      "Plans": [
    {
      "Startup Cost": 10.18,
      "Actual Rows": 0,
      "Plans": [
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 26,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone))",
          "Plan Rows": 29,
          "Parallel Aware": false,
          "Actual Total Time": 0.016,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.016,
          "Total Cost": 5,
          "Actual Loops": 1,
          "Index Name": "agg_search_a"
        },
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 36,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone))",
          "Plan Rows": 39,
          "Parallel Aware": false,
          "Actual Total Time": 0.011,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.011,
          "Total Cost": 5.15,
          "Actual Loops": 1,
          "Index Name": "agg_search_b"
        }
      ],
      "Node Type": "BitmapOr",
      "Plan Rows": 68,
      "Parallel Aware": false,
      "Actual Total Time": 0.027,
      "Parent Relationship": "Outer",
      "Actual Startup Time": 0.027,
      "Plan Width": 0,
      "Actual Loops": 1,
      "Total Cost": 10.18
    }
      ],
      "Exact Heap Blocks": 1,
      "Node Type": "Bitmap Heap Scan",
      "Plan Rows": 68,
      "Relation Name": "agg",
      "Alias": "agg",
      "Parallel Aware": false,
      "Actual Total Time": 0.037,
      "Recheck Cond": "(((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone)) OR ((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone)))",
      "Lossy Heap Blocks": 0,
      "Actual Startup Time": 0.033,
      "Plan Width": 44,
      "Actual Loops": 1,
      "Total Cost": 280.95
    },
    "Triggers": []
  }
]

El truco es que los nodos de su plan contienen solo las filas deseadas. Anteriormente obtuvimos miles de filas en el nodo del plan porque seleccionó all points from some point in time to the very end, luego el siguiente nodo eliminó las filas innecesarias.

borovsky
fuente