Optimización de consultas en un rango de marcas de tiempo (una columna)

8

Estoy usando Postgres 9.3 a través de Heroku.

Tengo una tabla, "tráfico", con 1M + registros que tiene muchas inserciones y actualizaciones todos los días. Necesito realizar operaciones SUM en esta tabla en diferentes intervalos de tiempo y esas llamadas pueden tomar hasta 40 segundos y me encantaría escuchar sugerencias sobre cómo mejorar eso.

Tengo el siguiente índice en su lugar en esta tabla:

CREATE INDEX idx_traffic_partner_only ON traffic (dt_created) WHERE campaign_id IS NULL AND uuid_self <> uuid_partner;

Aquí hay un ejemplo de declaración SELECT:

SELECT SUM("clicks") AS clicks, SUM("impressions") AS impressions
FROM "traffic"
WHERE "uuid_self" != "uuid_partner"
AND "campaign_id" is NULL
AND "dt_created" >= 'Sun, 29 Mar 2015 00:00:00 +0000'
AND "dt_created" <= 'Mon, 27 Apr 2015 23:59:59 +0000' 

Y este es el EXPLICAR ANÁLISIS:

Aggregate  (cost=21625.91..21625.92 rows=1 width=16) (actual time=41804.754..41804.754 rows=1 loops=1)
  ->  Index Scan using idx_traffic_partner_only on traffic  (cost=0.09..20085.11 rows=308159 width=16) (actual time=1.409..41617.976 rows=302392 loops=1)
      Index Cond: ((dt_created >= '2015-03-29'::date) AND (dt_created <= '2015-04-27'::date))
Total runtime: 41804.893 ms

http://explain.depesz.com/s/gGA

Esta pregunta es muy similar a otra en SE, pero esa utilizaba un índice en dos intervalos de marcas de tiempo de columna y el planificador de índice para esa consulta tenía estimaciones que estaban muy lejos. La sugerencia principal fue crear un índice ordenado de varias columnas, pero para los índices de una sola columna eso no tiene mucho efecto. Las otras sugerencias fueron usar los índices CLUSTER / pg_repack y GIST, pero aún no los he probado, ya que me gustaría ver si hay una mejor solución usando índices regulares.

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

Como referencia, probé los siguientes índices, que no fueron utilizados por el DB:

INDEX idx_traffic_2 ON traffic (campaign_id, uuid_self, uuid_partner, dt_created);
INDEX idx_traffic_3 ON traffic (dt_created);
INDEX idx_traffic_4 ON traffic (uuid_self);
INDEX idx_traffic_5 ON traffic (uuid_partner);

EDITAR : Corrió EXPLICAR (ANALIZAR, VERBOSO, COSTOS, BUFFERS) y estos fueron los resultados:

Aggregate  (cost=20538.62..20538.62 rows=1 width=8) (actual time=526.778..526.778 rows=1 loops=1)
  Output: sum(clicks), sum(impressions)
  Buffers: shared hit=47783 read=29803 dirtied=4
  I/O Timings: read=184.936
  ->  Index Scan using idx_traffic_partner_only on public.traffic  (cost=0.09..20224.74 rows=313881 width=8) (actual time=0.049..431.501 rows=302405 loops=1)
      Output: id, uuid_self, uuid_partner, impressions, clicks, dt_created... (other fields redacted)
      Index Cond: ((traffic.dt_created >= '2015-03-29'::date) AND (traffic.dt_created <= '2015-04-27'::date))
      Buffers: shared hit=47783 read=29803 dirtied=4
      I/O Timings: read=184.936
Total runtime: 526.881 ms

http://explain.depesz.com/s/7Gu6

Definición de tabla:

CREATE TABLE traffic (
    id              serial,
    uuid_self       uuid not null,
    uuid_partner    uuid not null,
    impressions     integer NOT NULL DEFAULT 1,
    clicks          integer NOT NULL DEFAULT 0,
    campaign_id     integer,
    dt_created      DATE DEFAULT CURRENT_DATE NOT NULL,
    dt_updated      TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
)

id es la clave principal y uuid_self, uuid_partner y campaign_id son claves foráneas. El campo dt_updated se actualiza con una función postgres.

Evan Appleby
fuente
explain (buffers, analyze, verbose) ...podría arrojar más luz.
Craig Ringer el
Aquí falta una información esencial: la definición exacta de la tabla traffic. Además: ¿por qué el segundo EXPLAINmuestra una caída de 42 segundos a 0,5 segundos? ¿Fue la primera ejecución con caché frío?
Erwin Brandstetter
Acabo de agregar la definición de tabla a la pregunta. Sí, los 42 a 0.5 segundos probablemente se debieron a un caché frío, pero dado que hay tantas actualizaciones, esto probablemente sea una ocurrencia bastante común. Acabo de ejecutar EXPLAIN ANALYZE nuevamente y esta vez tardó 56 segundos. Lo ejecuté una vez más y bajó a .4s.
Evan Appleby
¿Es seguro asumir que hay una restricción de PK id? ¿Alguna otra restricción? Veo dos columnas que pueden ser NULL. ¿Cuál es el porcentaje de valores NULL en cada uno? ¿Qué obtienes por esto? SELECT count(*) AS ct, count(campaign_id)/ count(*) AS camp_pct, count(dt_updated)/count(*) AS upd_pct FROM traffic;
Erwin Brandstetter
Sí, ID tiene una restricción PK y uuid_self, uuid_partner y campaign_id tienen restricciones FK. Campaign_id es 99% + NULL y dt_updated es 0% NULL.
Evan Appleby

Respuestas:

3

Dos cosas que son muy extrañas aquí:

  1. La consulta selecciona 300k filas de una tabla con 1M + filas. Para el 30% (o algo más del 5%, depende del tamaño de la fila y otros factores), generalmente no vale la pena usar un índice. Deberíamos ver una exploración secuencial .

    La excepción serían los escaneos de solo índice, que no veo aquí. El índice de varias columnas que @Craig sugirió sería la mejor opción si obtiene escaneos de solo índice. Con muchas actualizaciones como las que mencionó, es posible que esto no funcione, en cuyo caso estará mejor sin las columnas adicionales, y solo el índice que ya tiene. Es posible que pueda hacer que funcione para usted con configuraciones de vacío automático más agresivas para la mesa. Puede ajustar los parámetros para tablas individuales.

  2. Si bien Postgres utilizará el índice, ciertamente esperaría ver un escaneo de índice de mapa de bits para tantas filas, no un escaneo de índice simple, que generalmente es la mejor opción para un bajo porcentaje de filas. Tan pronto como Postgres espere múltiples visitas por página de datos (a juzgar por sus estadísticas en la tabla), normalmente cambiará a un escaneo de índice de mapa de bits.

A juzgar por eso, sospecharía que su configuración de costos es inadecuada (y posiblemente también las estadísticas de la tabla). Es posible que haya establecido random_page_costy / o demasiado bajo , en relación con . Sigue los enlaces y lee el manual.cpu_index_tuple_cost seq_page_cost

También encajaría con la observación de que el caché frío es un factor importante, como lo resolvimos en los comentarios. ¿Está accediendo a (partes de) tablas que nadie ha tocado en mucho tiempo o está ejecutando en un sistema de prueba donde el caché no está poblado (todavía)?
De lo contrario, simplemente no tiene suficiente RAM disponible para almacenar en caché la mayoría de los datos relevantes en su base de datos. En consecuencia, el acceso aleatorio es mucho más costoso que el acceso secuencial cuando los datos residen en la memoria caché. Dependiendo de la situación real, puede que tenga que ajustar para obtener mejores planes de consulta.

Se debe mencionar otro factor para la respuesta lenta en la primera lectura solamente: bits de pista . Lea los detalles en Postgres Wiki y esta pregunta relacionada:

O la tabla está extremadamente hinchada , en cuyo caso una exploración de índice tendría sentido y me referiría nuevamente aCLUSTER / pg_repacken mi respuesta anterior que usted citó. (O simplementeVACUUM FULL)e investiga tuVACUUMconfiguración. Es importante con esomany inserts and updates every day.

Dependiendo de los UPDATEpatrones, también considere un valor FILLFACTORinferior a 100. Si actualiza principalmente solo las filas recién agregadas, establezca el valor más bajo FILLFACTER después de compactar su tabla, de modo que solo las páginas nuevas tengan margen de maniobra para las actualizaciones.

Esquema

campaign_ides 99% + NULL y dt_updatedes 0% NULL.

Ajuste la secuencia de columnas ligeramente, para guardar 8 bytes por fila (en el 99% de los casos donde campaign_ides NULL):

CREATE TABLE traffic (
    uuid_self       uuid not null REFERENCES ... ,
    uuid_partner    uuid not null REFERENCES ... ,
    id              serial PRIMARY KEY,
    impressions     integer NOT NULL DEFAULT 1,
    clicks          integer NOT NULL DEFAULT 0,
    campaign_id     integer,
    dt_created      DATE DEFAULT CURRENT_DATE NOT NULL,
    dt_updated      TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
);

Explicación detallada y enlaces a más:

Para medir:

Erwin Brandstetter
fuente
Gracias por la sugerencia. Actualmente confío en la aspiradora automática integrada configurada a través de Heroku y la mesa de tráfico se aspira casi todos los días. Analizaré más sobre cómo cambiar las estadísticas de la tabla y el Factor de relleno y usar pg_repack e informar.
Evan Appleby
2

Me parece que está consultando muchos datos en un índice grande, por lo que es lento. Nada notablemente malo allí.

Si está en PostgreSQL 9.3 o 9.4, puede intentar ver si puede obtener un escaneo de solo índice convirtiéndolo en una especie de índice de cobertura.

CREATE INDEX idx_traffic_partner_only 
ON traffic (dt_created, clicks, impressions)
WHERE campaign_id IS NULL 
  AND uuid_self <> uuid_partner;

PostgreSQL no tiene verdaderos índices de cobertura o soporte para términos de índice que son solo valores, no parte del árbol b, por lo que esto es más lento y más costoso de lo que podría ser con esas características. Todavía podría ser una victoria sobre un escaneo de índice simple si el vacío se ejecuta con la frecuencia suficiente para mantener actualizado el mapa de visibilidad.


Idealmente, PostgreSQL admitiría campos de datos auxiliares en un índice como en MS-SQL Server ( esta sintaxis NO FUNCIONARÁ en PostgreSQL ):

-- This will not work in PostgreSQL (at least 9.5)
-- it's an example of what I wish did work. Don't
-- comment to say it doesn't work.
--
CREATE INDEX idx_traffic_partner_only 
ON traffic (dt_created)
INCLUDING (clicks, impressions) -- auxillary data columns
WHERE campaign_id IS NULL 
  AND uuid_self <> uuid_partner;
Craig Ringer
fuente
Gracias por la sugerencia. Probé el índice de cobertura y el DB lo ignoró y todavía usé el otro índice. ¿Sugeriría eliminar el otro índice y solo usar el índice de cobertura (o, alternativamente, solo usar múltiples índices de cobertura para cada situación que lo requiera)? También agregué EXPLICAR (ANALIZAR, VERBOSO, COSTOS, BUFFERS) en la pregunta original.
Evan Appleby
Impar. Tal vez el planificador no sea lo suficientemente inteligente como para elegir un escaneo de solo índice si ve más de un agregado, pero hubiera pensado que podría. Intenta jugar con los parámetros de costo ( random_page_costetc.). Además, para fines de prueba, solo vea si set enable_indexscan = offy set enable_seqscan = offluego volver a ejecutar fuerza un escaneo de solo índice, y si es así, cuáles son sus estimaciones de costos del análisis explicado.
Craig Ringer el