Configurar PostgreSQL para rendimiento de lectura

39

Nuestro sistema escribe muchos datos (tipo de sistema Big Data). El rendimiento de escritura es lo suficientemente bueno para nuestras necesidades, pero el rendimiento de lectura es realmente demasiado lento.

La estructura de la clave primaria (restricción) es similar para todas nuestras tablas:

timestamp(Timestamp) ; index(smallint) ; key(integer).

Una tabla puede tener millones de filas, incluso miles de millones de filas, y una solicitud de lectura suele ser para un período específico (marca de tiempo / índice) y etiqueta. Es común tener una consulta que devuelve alrededor de 200k líneas. Actualmente, podemos leer aproximadamente 15k líneas por segundo, pero debemos ser 10 veces más rápidos. ¿Es posible? y si lo es, cómo?

Nota: PostgreSQL está empaquetado con nuestro software, por lo que el hardware es diferente de un cliente a otro.

Es una VM utilizada para pruebas. El host de la VM es Windows Server 2008 R2 x64 con 24.0 GB de RAM.

Especificaciones del servidor (máquina virtual VMWare)

Server 2008 R2 x64
2.00 GB of memory
Intel Xeon W3520 @ 2.67GHz (2 cores)

postgresql.conf optimizaciones

shared_buffers = 512MB (default: 32MB)
effective_cache_size = 1024MB (default: 128MB)
checkpoint_segment = 32 (default: 3)
checkpoint_completion_target = 0.9 (default: 0.5)
default_statistics_target = 1000 (default: 100)
work_mem = 100MB (default: 1MB)
maintainance_work_mem = 256MB (default: 16MB)

Definición de tabla

CREATE TABLE "AnalogTransition"
(
  "KeyTag" integer NOT NULL,
  "Timestamp" timestamp with time zone NOT NULL,
  "TimestampQuality" smallint,
  "TimestampIndex" smallint NOT NULL,
  "Value" numeric,
  "Quality" boolean,
  "QualityFlags" smallint,
  "UpdateTimestamp" timestamp without time zone, -- (UTC)
  CONSTRAINT "PK_AnalogTransition" PRIMARY KEY ("Timestamp" , "TimestampIndex" , "KeyTag" ),
  CONSTRAINT "FK_AnalogTransition_Tag" FOREIGN KEY ("KeyTag")
      REFERENCES "Tag" ("Key") MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
)
WITH (
  OIDS=FALSE,
  autovacuum_enabled=true
);

Consulta

La consulta tarda unos 30 segundos en ejecutarse en pgAdmin3, pero nos gustaría tener el mismo resultado en menos de 5 segundos si es posible.

SELECT 
    "AnalogTransition"."KeyTag", 
    "AnalogTransition"."Timestamp" AT TIME ZONE 'UTC', 
    "AnalogTransition"."TimestampQuality", 
    "AnalogTransition"."TimestampIndex", 
    "AnalogTransition"."Value", 
    "AnalogTransition"."Quality", 
    "AnalogTransition"."QualityFlags", 
    "AnalogTransition"."UpdateTimestamp"
FROM "AnalogTransition"
WHERE "AnalogTransition"."Timestamp" >= '2013-05-16 00:00:00.000' AND "AnalogTransition"."Timestamp" <= '2013-05-17 00:00:00.00' AND ("AnalogTransition"."KeyTag" = 56 OR "AnalogTransition"."KeyTag" = 57 OR "AnalogTransition"."KeyTag" = 58 OR "AnalogTransition"."KeyTag" = 59 OR "AnalogTransition"."KeyTag" = 60)
ORDER BY "AnalogTransition"."Timestamp" DESC, "AnalogTransition"."TimestampIndex" DESC
LIMIT 500000;

Explicar 1

"Limit  (cost=0.00..125668.31 rows=500000 width=33) (actual time=2.193..3241.319 rows=500000 loops=1)"
"  Buffers: shared hit=190147"
"  ->  Index Scan Backward using "PK_AnalogTransition" on "AnalogTransition"  (cost=0.00..389244.53 rows=1548698 width=33) (actual time=2.187..1893.283 rows=500000 loops=1)"
"        Index Cond: (("Timestamp" >= '2013-05-16 01:00:00-04'::timestamp with time zone) AND ("Timestamp" <= '2013-05-16 15:00:00-04'::timestamp with time zone))"
"        Filter: (("KeyTag" = 56) OR ("KeyTag" = 57) OR ("KeyTag" = 58) OR ("KeyTag" = 59) OR ("KeyTag" = 60))"
"        Buffers: shared hit=190147"
"Total runtime: 3863.028 ms"

Explicar 2

En mi última prueba, ¡tardé 7 minutos en seleccionar mis datos! Vea abajo:

"Limit  (cost=0.00..313554.08 rows=250001 width=35) (actual time=0.040..410721.033 rows=250001 loops=1)"
"  ->  Index Scan using "PK_AnalogTransition" on "AnalogTransition"  (cost=0.00..971400.46 rows=774511 width=35) (actual time=0.037..410088.960 rows=250001 loops=1)"
"        Index Cond: (("Timestamp" >= '2013-05-22 20:00:00-04'::timestamp with time zone) AND ("Timestamp" <= '2013-05-24 20:00:00-04'::timestamp with time zone) AND ("KeyTag" = 16))"
"Total runtime: 411044.175 ms"
JPelletier
fuente

Respuestas:

52

Alineación de datos y tamaño de almacenamiento

En realidad, la sobrecarga por tupla es de 24 bytes para el encabezado de la tupla más 4 bytes para el puntero del elemento.
Más detalles en el cálculo en esta respuesta relacionada:

Conceptos básicos de alineación de datos y relleno en esta respuesta relacionada en SO:

Tenemos tres columnas para la clave primaria:

PRIMARY KEY ("Timestamp" , "TimestampIndex" , "KeyTag")

"Timestamp"      timestamp (8 bytes)
"TimestampIndex" smallint  (2 bytes)
"KeyTag"         integer   (4 bytes)

Resultados en:

 Puntero de elemento de 4 bytes en el encabezado de página (sin contar para múltiplos de 8 bytes)
---
23 bytes para el encabezado de tupla
 Relleno de 1 byte para alineación de datos (o mapa de bits NULL)
 8 bytes "Marca de tiempo"
 2 bytes "TimestampIndex"
 Relleno de 2 bytes para alineación de datos
 4 bytes "KeyTag" 
 0 relleno al múltiplo más cercano de 8 bytes
-----
44 bytes por tupla

Más sobre la medición del tamaño del objeto en esta respuesta relacionada:

Orden de columnas en un índice de varias columnas

Lea estas dos preguntas y respuestas para comprender:

La forma en que tiene su índice (clave principal), puede recuperar filas sin un paso de clasificación, eso es atractivo, especialmente con LIMIT. Pero recuperar las filas parece extremadamente costoso.

En general, en un índice de varias columnas, las columnas de "igualdad" deben ir primero y las columnas de "rango" al final:

Por lo tanto, intente un índice adicional con el orden inverso de la columna :

CREATE INDEX analogransition_mult_idx1
   ON "AnalogTransition" ("KeyTag", "TimestampIndex", "Timestamp");

Depende de la distribución de datos. Pero con millions of row, even billion of rowsesto podría ser sustancialmente más rápido.

El tamaño de la tupla es 8 bytes más grande, debido a la alineación y el relleno de los datos. Si está utilizando esto como índice simple, puede intentar soltar la tercera columna "Timestamp". Puede ser un poco más rápido o no (ya que podría ayudar con la clasificación).

Es posible que desee mantener ambos índices. Dependiendo de una serie de factores, su índice original puede ser preferible, en particular con un pequeño LIMIT.

autovacuum y estadísticas de tabla

Las estadísticas de su tabla deben estar actualizadas. Estoy seguro de que tiene el vacío automático en ejecución.

Dado que su tabla parece ser enorme y las estadísticas importantes para el plan de consulta correcto, aumentaría sustancialmente el objetivo de estadísticas para las columnas relevantes:

ALTER TABLE "AnalogTransition" ALTER "Timestamp" SET STATISTICS 1000;

... o incluso más con miles de millones de filas. El máximo es 10000, el predeterminado es 100.

Haga eso para todas las columnas involucradas WHEREo ORDER BYcláusulas. Entonces corre ANALYZE.

Diseño de la mesa

Mientras lo hace, si aplica lo que ha aprendido sobre la alineación de datos y el relleno, este diseño de tabla optimizado debería ahorrar algo de espacio en disco y ayudar un poco al rendimiento (ignorando pk y fk):

CREATE TABLE "AnalogTransition"(
  "Timestamp" timestamp with time zone NOT NULL,
  "KeyTag" integer NOT NULL,
  "TimestampIndex" smallint NOT NULL,
  "TimestampQuality" smallint,
  "UpdateTimestamp" timestamp without time zone, -- (UTC)
  "QualityFlags" smallint,
  "Quality" boolean,
  "Value" numeric
);

CLUSTER / pg_repack

Para optimizar el rendimiento de lectura para consultas que utilizan un índice determinado (ya sea el original o la alternativa sugerida), puede volver a escribir la tabla en el orden físico del índice. CLUSTERhace eso, pero es bastante invasivo y requiere un bloqueo exclusivo durante la operación. pg_repackes una alternativa más sofisticada que puede hacer lo mismo sin un bloqueo exclusivo en la mesa.
Esto puede ayudar sustancialmente con tablas enormes, ya que se deben leer muchos menos bloques de la tabla.

RAM

En general, 2 GB de RAM física no son suficientes para lidiar con miles de millones de filas rápidamente. Más RAM podría recorrer un largo camino, acompañado de una configuración adaptada: obviamente, una más grande effective_cache_sizepara empezar.

Erwin Brandstetter
fuente
2
Agregué un índice simple solo en KeyTag y parece ser bastante rápido ahora. También aplicaré sus recomendaciones sobre la alineación de datos. ¡Muchas gracias!
JPelletier
9

Entonces, de los planes veo una cosa: su índice está hinchado (luego junto con la tabla subyacente) o simplemente no es realmente bueno para este tipo de consulta (intenté abordar esto en mi último comentario anterior).

Una fila del índice contiene 14 bytes de datos (y algunos para el encabezado). Ahora, calculando a partir de los números dados en el plan: obtuviste 500,000 filas de 190147 páginas, eso significa, en promedio, menos de 3 filas útiles por página, es decir, alrededor de 37 bytes por página de 8 kb. Esta es una relación muy mala, ¿no? Dado que la primera columna del índice es el Timestampcampo y se usa en la consulta como un rango, el planificador puede, y elige, el índice para encontrar filas coincidentes. Pero no se TimestampIndexmenciona en las WHEREcondiciones, por lo que el filtrado KeyTagno es muy efectivo ya que esos valores supuestamente aparecen aleatoriamente en las páginas de índice.

Entonces, una posibilidad es cambiar la definición del índice a

CONSTRAINT "PK_AnalogTransition" PRIMARY KEY ("Timestamp", "KeyTag", "TimestampIndex")

(o, dada la carga de su sistema, cree este índice como uno nuevo:

CREATE INDEX CONCURRENTLY "idx_AnalogTransition" 
    ON "AnalogTransition" ("Timestamp", "KeyTag", "TimestampIndex");
  • seguro que esto llevará un tiempo, pero aún puede trabajar mientras tanto).

La otra posibilidad es que una gran proporción de las páginas de índice esté ocupada por filas muertas, que podrían eliminarse aspirando. Creó la tabla con configuración autovacuum_enabled=true, pero ¿alguna vez ha comenzado a realizar el vacío automático? ¿O correr VACUUMmanualmente?

dezso
fuente