Configurando o PostgreSQL para desempenho de leitura

39

Nosso sistema grava muitos dados (tipo sistema de Big Data). O desempenho de gravação é bom o suficiente para nossas necessidades, mas o desempenho de leitura é realmente muito lento.

A estrutura da chave primária (restrição) é semelhante para todas as nossas tabelas:

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

Uma tabela pode ter milhões de linhas, até bilhões de linhas, e uma solicitação de leitura geralmente é para um período específico (carimbo de data / hora / índice) e marca. É comum ter uma consulta que retorna cerca de 200 mil linhas. Atualmente, podemos ler cerca de 15 mil linhas por segundo, mas precisamos ser 10 vezes mais rápidos. Isso é possível e, se sim, como?

Nota: O PostgreSQL é fornecido com o nosso software, portanto o hardware é diferente de um cliente para outro.

É uma VM usada para teste. O host da VM é o Windows Server 2008 R2 x64 com 24,0 GB de RAM.

Especificações do servidor (VMWare da máquina virtual)

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

postgresql.conf otimizações

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)

Definição da tabela

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
);

Inquerir

A consulta leva cerca de 30 segundos para ser executada no pgAdmin3, mas gostaríamos de obter o mesmo resultado em menos de 5 segundos, se possível.

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;

Explique 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"

Explique 2

No meu último teste, foram necessários 7 minutos para selecionar meus dados! Ver abaixo:

"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
fonte

Respostas:

52

Alinhamento de dados e tamanho de armazenamento

Na verdade, a sobrecarga por tupla é de 24 bytes para o cabeçalho da tupla mais 4 bytes para o ponteiro do item.
Mais detalhes no cálculo nesta resposta relacionada:

Noções básicas de alinhamento e preenchimento de dados nesta resposta relacionada ao SO:

Temos três colunas para a chave primária:

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

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

Resulta em:

 Ponteiro de item de 4 bytes no cabeçalho da página (sem contar para múltiplos de 8 bytes)
---
23 bytes para o cabeçalho da tupla
 Preenchimento de 1 byte para alinhamento de dados (ou bitmap NULL)
 8 bytes "Registro de data e hora"
 2 bytes "TimestampIndex"
 Preenchimento de 2 bytes para alinhamento de dados
 4 bytes "KeyTag" 
 0 preenchimento para o múltiplo mais próximo de 8 bytes
-----
44 bytes por tupla

Mais sobre como medir o tamanho do objeto nesta resposta relacionada:

Ordem das colunas em um índice de várias colunas

Leia estas duas perguntas e respostas para entender:

Da maneira como você tem seu índice (chave primária), é possível recuperar linhas sem uma etapa de classificação, o que é atraente, especialmente com LIMIT. Mas recuperar as linhas parece extremamente caro.

Geralmente, em um índice de várias colunas, as colunas "igualdade" devem ir primeiro e as colunas "intervalo" por último:

Portanto, tente um índice adicional com a ordem da coluna invertida :

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

Depende da distribuição de dados. Mas com millions of row, even billion of rowsisso pode ser substancialmente mais rápido.

O tamanho da tupla é 8 bytes maior, devido ao alinhamento e preenchimento dos dados. Se você estiver usando isso como índice simples, tente soltar a terceira coluna "Timestamp". Pode ser um pouco mais rápido ou não (pois pode ajudar na classificação).

Você pode querer manter os dois índices. Dependendo de vários fatores, seu índice original pode ser preferível - em particular com um pequeno LIMIT.

estatísticas de autovacuum e tabela

Suas estatísticas da tabela precisam estar atualizadas. Tenho certeza que você tem vácuo automático em execução.

Como sua tabela parece enorme e as estatísticas importantes para o plano de consulta correto, eu aumentaria substancialmente o destino das estatísticas para as colunas relevantes:

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

... ou ainda mais com bilhões de linhas. O máximo é 10000, o padrão é 100.

Faça isso para todas as colunas envolvidas WHEREou ORDER BYcláusulas. Então corra ANALYZE.

Layout da tabela

Enquanto isso, se você aplicar o que aprendeu sobre alinhamento e preenchimento de dados, esse layout de tabela otimizado deve economizar espaço em disco e ajudar um pouco o desempenho (ignorando pk & 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 otimizar o desempenho de leitura para consultas que usam um determinado índice (seja o original ou a minha alternativa sugerida), você pode reescrever a tabela na ordem física do índice. CLUSTERfaz isso, mas é bastante invasivo e requer um bloqueio exclusivo durante a operação. pg_repacké uma alternativa mais sofisticada que pode fazer o mesmo sem um bloqueio exclusivo na mesa.
Isso pode ajudar substancialmente com tabelas enormes, pois é necessário ler muito menos blocos da tabela.

RAM

Geralmente, 2 GB de RAM física não são suficientes para lidar com bilhões de linhas rapidamente. Mais memória RAM pode percorrer um longo caminho - acompanhada de configurações adaptadas: obviamente uma maior effective_cache_sizepara começar.

Erwin Brandstetter
fonte
2
Adicionei um índice simples apenas no KeyTag e parece ser bastante rápido agora. Também aplicarei suas recomendações sobre o alinhamento de dados. Muito obrigado!
precisa saber é o seguinte
9

Portanto, a partir dos planos, vejo uma coisa: você indexa ou está inchado (em seguida, junto com a tabela subjacente) ou simplesmente não é realmente bom para esse tipo de consulta (tentei abordar isso no meu último comentário acima).

Uma linha do índice contém 14 bytes de dados (e alguns para o cabeçalho). Agora, calculando a partir dos números fornecidos no plano: você obteve 500.000 linhas de 190147 páginas - isso significa, em média, menos de 3 linhas úteis por página, ou seja, cerca de 37 bytes por página de 8 kb. Essa é uma proporção muito ruim, não é? Como a primeira coluna do índice é o Timestampcampo e é usada na consulta como um intervalo, o planejador pode - e escolhe - o índice para encontrar linhas correspondentes. Mas não há nenhuma TimestampIndexmenção nas WHEREcondições, portanto, a filtragem KeyTagnão é muito eficaz, pois esses valores supostamente aparecem aleatoriamente nas páginas de índice.

Portanto, uma possibilidade é alterar a definição do índice para

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

(ou, dada a carga do seu sistema, crie este índice como um novo:

CREATE INDEX CONCURRENTLY "idx_AnalogTransition" 
    ON "AnalogTransition" ("Timestamp", "KeyTag", "TimestampIndex");
  • isso levará um tempo, com certeza, mas você ainda poderá trabalhar enquanto isso.)

A outra possibilidade de que uma grande proporção das páginas de índice seja ocupada por linhas mortas, que podem ser removidas por aspiração. Você criou a tabela com a configuração autovacuum_enabled=true- mas você já começou a aspirar automaticamente? Ou executar VACUUMmanualmente?

dezso
fonte