web-dev-qa-db-fra.com

Configuration de PostgreSQL pour les performances de lecture

Notre système écrit de nombreuses données (type de système Big Data). Les performances d'écriture sont suffisantes pour nos besoins, mais les performances de lecture sont vraiment trop lentes.

La structure de la clé primaire (contrainte) est similaire pour toutes nos tables:

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

Une table peut avoir des millions de lignes, voire des milliards de lignes, et une demande de lecture concerne généralement une période (horodatage/index) et une balise spécifiques. Il est courant d'avoir une requête qui renvoie environ 200 000 lignes. Actuellement, nous pouvons lire environ 15 000 lignes par seconde, mais nous devons être 10 fois plus rapides. Est-ce possible, et si oui comment?

Note: PostgreSQL est fourni avec notre logiciel, donc le matériel est différent d'un client à l'autre.

Il s'agit d'un VM utilisé pour les tests. L'hôte de la VM est Windows Server 2008 R2 x64 avec 24,0 Go de RAM.

Spécifications du serveur (machine virtuelle VMWare)

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

postgresql.conf optimisations

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)

Définition de table

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

Requete

La requête prend environ 30 secondes pour s'exécuter dans pgAdmin3, mais nous aimerions avoir le même résultat en moins de 5 secondes si possible.

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;

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

Expliquez 2

Lors de mon dernier test, il m'a fallu 7 minutes pour sélectionner mes données! Voir ci-dessous:

"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"
43
JPelletier

Alignement des données et taille de stockage

En fait, la surcharge par tuple d'index est de 8 octets pour l'en-tête Tuple plus 4 octets pour le pointeur d'élément.

En relation:

Nous avons trois colonnes pour la clé primaire:

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

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

Résulte en:

 Pointeur d'élément de 4 octets dans l'en-tête de page (sans compter le multiple de 8 octets) 
 
 8 octets pour l'en-tête de tuple d'index 
 8 octets "Horodatage" 
 2 octets "TimestampIndex" 
 2 octets de remplissage pour l'alignement des données 
 4 octets "KeyTag" 
 0 remplissage au multiple de 8 octets le plus proche 
 - ---- 
 28 octets par index Tuple; plus quelques octets de surcharge. 

Plus d'informations sur la mesure de la taille des objets dans cette réponse connexe:

Ordre des colonnes dans un index multicolonne

Lisez ces deux questions et réponses pour comprendre:

La façon dont vous avez votre index (clé primaire), vous pouvez récupérer des lignes sans étape de tri, ce qui est attrayant, surtout avec LIMIT. Mais récupérer les lignes semblent extrêmement coûteuses.

En général, dans un index multi-colonnes, les colonnes "égalité" doivent aller en premier et les colonnes "plage" en dernier:

Par conséquent, essayez un index supplémentaire avec l'ordre des colonnes inversé :

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

Cela dépend de la distribution des données. Mais avec millions of row, even billion of rows cela pourrait être beaucoup plus rapide.

La taille des tuples est supérieure de 8 octets, en raison de l'alignement et du remplissage des données. Si vous l'utilisez comme index simple, vous pouvez essayer de supprimer la troisième colonne "Timestamp". Peut être un peu plus rapide ou non (car cela pourrait aider au tri).

Vous souhaiterez peut-être conserver les deux index. En fonction d'un certain nombre de facteurs, votre index d'origine peut être préférable - en particulier avec un petit LIMIT.

statistiques de vide automatique et de table

Les statistiques de votre table doivent être à jour. Je suis sûr que vous avez autovacuum en cours d'exécution.

Étant donné que votre table semble être énorme et que les statistiques sont importantes pour le bon plan de requête, j'augmenterais considérablement la cible des statistiques pour les colonnes pertinentes:

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

... ou même plus avec des milliards de lignes. Le maximum est 10000, la valeur par défaut est 100.

Faites cela pour toutes les colonnes impliquées dans WHERE ou ORDER BY clauses. Exécutez ensuite ANALYZE.

Disposition de la table

Tout en y étant, si vous appliquez ce que vous avez appris sur l'alignement et le remplissage des données, cette disposition de table optimisée devrait économiser de l'espace disque et aider les performances un peu (en ignorant 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

Pour optimiser les performances de lecture pour les requêtes qui utilisent un certain index (que ce soit l'original ou mon alternative suggérée), vous pouvez réécrire la table dans l'ordre physique de l'index. CLUSTER fait cela, mais c'est plutôt invasif et nécessite un verrou exclusif pour la durée de l'opération.
pg_repack est une alternative plus sophistiquée qui peut faire de même sans verrou exclusif sur la table.
pg_squeeze est un outil similaire et ultérieur (je ne l'ai pas encore utilisé).

Cela peut aider considérablement avec les énormes tables, car beaucoup moins de blocs de la table doivent être lus.

RAM

Généralement, 2 Go de physique RAM n'est tout simplement pas suffisant pour traiter rapidement des milliards de lignes. Plus RAM peut faire beaucoup - accompagné d'un réglage adapté: évidemment un plus grand effective_cache_size pour commencer.

55

Donc, d'après les plans, je vois une chose: votre index est gonflé (puis avec la table sous-jacente) ou n'est tout simplement pas vraiment bon pour ce type de requête (j'ai essayé de répondre à cela dans mon dernier commentaire ci-dessus).

Une ligne de l'index contient 14 octets de données (et certains pour l'en-tête). Maintenant, calcul à partir des nombres indiqués dans le plan: vous avez obtenu 500 000 lignes sur 190147 pages - ce qui signifie, en moyenne, moins de 3 lignes utiles par page, soit environ 37 octets par page de 8 ko. C'est un très mauvais ratio, non? Étant donné que la première colonne de l'index est le champ Timestamp et qu'il est utilisé dans la requête en tant que plage, le planificateur peut - et choisit - choisir l'index pour trouver les lignes correspondantes. Mais il n'y a pas de TimestampIndex mentionné dans les conditions WHERE, donc le filtrage sur KeyTag n'est pas très efficace car ces valeurs apparaissent supposément au hasard dans les pages d'index.

Ainsi, une possibilité est de changer la définition de l'indice en

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

(ou, compte tenu de la charge de votre système, créez cet index comme un nouveau:

CREATE INDEX CONCURRENTLY "idx_AnalogTransition" 
    ON "AnalogTransition" ("Timestamp", "KeyTag", "TimestampIndex");
  • cela prendra un certain temps, mais vous pouvez toujours travailler en attendant.)

L'autre possibilité qu'une grande partie des pages d'index soit occupée par des lignes mortes, qui pourraient être supprimées en passant l'aspirateur. Vous avez créé la table avec le paramètre autovacuum_enabled=true - mais avez-vous déjà commencé à passer l'aspirateur? Ou exécutez VACUUM manuellement?

11
dezso