Configuration de PostgreSQL pour la performance en lecture

39

Notre système écrit beaucoup de données (type de système Big Data). Les performances en écriture suffisent à nos besoins, mais les performances en 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 (timestamp / 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?

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

C'est une machine virtuelle utilisée pour les tests. L'hôte de la machine virtuelle est Windows Server 2008 R2 x64 avec 24,0 Go de RAM.

Spécification du serveur (VMWare de la machine virtuelle)

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

Question

La requête prend environ 30 secondes pour s'exécuter dans pgAdmin3, mais nous aimerions avoir le même résultat sous 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;

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

Expliquer 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"
JPelletier
la source

Réponses:

52

Alignement des données et taille de stockage

En réalité, la surcharge par tuple d'index est de 8 octets pour l'en-tête de 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 la page (sans compter les multiples de 8 octets)

 8 octets pour l'en-tête du 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 plus proche multiple de 8 octets
-----
28 octets par tuple d'index; plus quelques octets de frais généraux.

Plus d'informations sur la mesure de la taille de l'objet dans cette réponse:

Ordre des colonnes dans un index multicolonne

Lisez ces deux questions et réponses pour comprendre:

De la manière dont vous avez votre index (clé primaire), vous pouvez récupérer des lignes sans étape de tri, ce qui est attrayant, en particulier avec LIMIT. Mais récupérer les lignes semble extrêmement coûteux.

Généralement, dans un index multi-colonnes, les colonnes "égalité" doivent commencer en premier et les colonnes "plage" en dernier:

Par conséquent, essayez un index supplémentaire avec un ordre de colonne 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 rowscela pourrait être considérablement plus rapide.

La taille du tuple est 8 octets plus grande, 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 voudrez peut-être conserver les deux index. En fonction d'un certain nombre de facteurs, votre indice d'origine peut être préférable, en particulier avec un petit LIMIT.

statistiques autovacuum et table

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

Comme votre tableau semble énorme et que les statistiques sont importantes pour le bon plan de requête, j'augmenterais considérablement la cible statistique 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 les clauses WHEREou ORDER BY. Alors courez ANALYZE.

Disposition de la table

En même temps, si vous appliquez ce que vous avez appris sur l'alignement et le remplissage des données, cette disposition de tableau optimisée devrait économiser de l'espace disque et améliorer un peu les performances (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 des requêtes utilisant un certain index (qu'il s'agisse de votre original ou de mon alternative suggérée), vous pouvez réécrire la table dans l'ordre physique de l'index. CLUSTERfait cela, mais il est plutôt invasif et nécessite un verrou exclusif pour la durée de l'opération.
pg_repackest une alternative plus sophistiquée qui peut faire la même chose sans verrouillage exclusif sur la table.
pg_squeezeest un outil postérieur similaire (ne l’a pas encore utilisé).

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

RAM

En général, 2 Go de RAM physique ne suffisent pas pour traiter rapidement des milliards de lignes. Plus de RAM peut aller très loin - accompagné d'un réglage adapté: évidemment un plus gros effective_cache_sizepour commencer.

Erwin Brandstetter
la source
2
J'ai ajouté un index simple sur KeyTag uniquement et il semble être assez rapide maintenant. Je vais également appliquer vos recommandations sur l'alignement des données. Merci beaucoup!
JPelletier
9

Ainsi, je vois une chose dans les plans: votre index est soit gonflé (puis à côté de la table sous-jacente) ou n’est tout simplement pas très bon pour ce type de requête (j’ai essayé d’aborder ceci dans mon dernier commentaire ci-dessus).

Une ligne de l'index contient 14 octets de données (et quelques-uns pour l'en-tête). Maintenant, calculons à partir des nombres donnés dans le plan: vous avez 500 000 lignes sur 190147 pages, ce qui signifie en moyenne moins de 3 lignes utiles par page, soit environ 37 octets par page. C'est un très mauvais ratio, n'est-ce pas? Étant donné que la première colonne de l'index est le Timestampchamp et qu'il est utilisé dans la requête en tant que plage, le planificateur peut choisir - et choisit - l'index pour rechercher les lignes correspondantes. Mais TimestampIndexles WHEREconditions ne mentionnent rien , aussi le filtrage sur KeyTagn'est-il pas très efficace, car ces valeurs sont supposées apparaître de manière aléatoire dans les pages d'index.

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

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

(ou, étant donné la charge de votre système, créez cet index en tant que nouvel index:

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

L'autre possibilité qu'une grande partie des pages d'index soit occupée par des rangées mortes, ce qui pourrait être éliminé par aspiration. Vous avez créé la table avec le réglage autovacuum_enabled=true- mais avez-vous déjà commencé l'autovacuuming? Ou courir VACUUMmanuellement?

dezso
la source