Accélérez la création de l'index partiel Postgres

8

J'essaie de créer des index partiels pour une grande table statique (1,2 To) dans Postgres 9.4.

Mes données sont complètement statiques, je peux donc insérer toutes les données, puis créer tous les index.

Dans ce tableau de 1,2 To, j'ai une colonne nommée run_idqui divise proprement les données. Nous avons obtenu d'excellentes performances en créant des index qui couvrent une gamme de run_ids. Voici un exemple:

CREATE INDEX perception_run_frame_idx_run_266_thru_270
ON run.perception
(run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

Ces index partiels nous donnent la vitesse de requête souhaitée. Malheureusement, la création de chaque index partiel prend environ 70 minutes.

Il semble que nous ayons un processeur limité ( topaffiche 100% pour le processus).
Puis-je faire quelque chose pour accélérer la création de nos index partiels?

Spécifications du système:

  • Xeon 18 cœurs
  • 192 Go de RAM
  • 12 SSD en RAID
  • Les autovacuums sont désactivés
  • maintenance_work_mem: 64 Go (Trop haut?)

Spécifications du tableau:

  • Taille: 1,26 To
  • Nombre de lignes: 10,537 milliards
  • Taille d'index typique: 3,2 Go (il existe une variance de ~ 0,5 Go)

Définition du tableau:

CREATE TABLE run.perception(
id bigint NOT NULL,
run_id bigint NOT NULL,
frame bigint NOT NULL,
by character varying(45) NOT NULL,
by_anyone bigint NOT NULL,
by_me bigint NOT NULL,
by_s_id integer,
owning_p_id bigint NOT NULL,
obj_type_set bigint,
seq integer,
subj_id bigint NOT NULL,
subj_state_frame bigint NOT NULL,
CONSTRAINT perception_pkey PRIMARY KEY (id))

(Ne lisez pas trop les noms des colonnes - je les ai quelque peu obscurcis.)

Informations de fond:

  • Nous avons une équipe distincte sur place qui consomme ces données, mais il n'y a vraiment qu'un ou deux utilisateurs. (Ces données sont toutes générées via une simulation.) Les utilisateurs ne commencent à analyser les données qu'une fois les insertions terminées et les index entièrement construits. Notre principale préoccupation est de réduire le temps nécessaire pour générer des données utilisables, et actuellement le goulot d'étranglement est le temps de création d'index.
  • La vitesse de requête a été tout à fait adéquate lors de l'utilisation de partiels. En fait, je pense que nous pourrions augmenter le nombre d'exécutions couvertes par chaque index, tout en maintenant des performances de requête suffisamment bonnes.
  • Je suppose que nous devrons partitionner la table. Nous essayons d'épuiser toutes les autres options avant d'emprunter cette voie.
burnsy
la source
Ces informations supplémentaires seraient essentielles: types de données des colonnes impliquées, requête typique, cardinalité (nombre de lignes), combien différentes run_id? Distribué équitablement? Taille de l'index résultant sur le disque? Les données sont statiques, ok. Mais êtes-vous le seul utilisateur?
Erwin Brandstetter
Mis à jour avec plus d'informations.
burnsy
1
"Les autovacuums sont désactivés " - pourquoi? C'est vraiment une mauvaise idée. Cela empêche la collecte de statistiques et
générera
@a_horse_with_no_name Nous lançons manuellement une analyse une fois toutes les données insérées
burnsy
Votre situation n'est toujours pas claire pour moi. À quoi ressemblent vos requêtes? Si votre table l'est completely static, que voulez-vous dire par là We have a separate team onsite that consumes this data? Indexez-vous simplement la plage run_id >= 266 AND run_id <= 270ou la table entière? Quelle est l'espérance de vie de chaque index / combien de requêtes l'utiliseront? Pour combien de valeurs différentes run_id? Sonne comme ~ 15 Mio. lignes par run_id, ce qui ferait environ 800 valeurs différentes pour run_id? Pourquoi sont obj_type_set, by_s_id, seqnon défini NOT NULL? Quel pourcentage approximatif des valeurs NULL pour chacun?
Erwin Brandstetter

Réponses:

8

Indice BRIN

Disponible depuis Postgres 9.5 et probablement exactement ce que vous recherchez. Création d'index beaucoup plus rapide, index beaucoup plus petit. Mais les requêtes ne sont généralement pas aussi rapides. Le manuel:

BRIN signifie Block Range Index. BRIN est conçu pour gérer de très grandes tables dans lesquelles certaines colonnes ont une certaine corrélation naturelle avec leur emplacement physique dans la table. Une plage de blocs est un groupe de pages physiquement adjacentes dans le tableau; pour chaque plage de blocs, des informations récapitulatives sont stockées par l'index.

Lisez la suite, il y a plus.
Depesz a effectué un test préliminaire.

L'optimum pour votre cas: Si vous pouvez écrire des lignes groupées sur run_id, votre index devient très faible et la création beaucoup moins cher.

CREATE INDEX foo ON run.perception USING brin (run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

Vous pourriez même simplement indexer la table entière .

Disposition de la table

Quoi que vous fassiez d'autre, vous pouvez économiser 8 octets perdus à cause du remplissage en raison des exigences d'alignement par ligne en ordonnant des colonnes comme ceci:

CREATE TABLE run.perception(
  id               bigint NOT NULL PRIMARY KEY
, run_id           bigint NOT NULL
, frame            bigint NOT NULL
, by_anyone        bigint NOT NULL
, by_me            bigint NOT NULL
, owning_p_id      bigint NOT NULL
, subj_id          bigint NOT NULL
, subj_state_frame bigint NOT NULL
, obj_type_set     bigint
, by_s_id          integer
, seq              integer
, by               varchar(45) NOT NULL -- or just use type text
);

Rend votre table 79 Go plus petite si aucune des colonnes n'a de valeurs NULL. Détails:

En outre, vous ne disposez que de trois colonnes qui peuvent être NULL. Le bitmap NULL occupe 8 octets pour 9 à 72 colonnes. Si une seule colonne entière est NULL, il y a un cas d'angle pour un paradoxe de stockage: il serait moins cher d'utiliser une valeur fictive à la place: 4 octets gaspillés mais 8 octets enregistrés en n'ayant pas besoin d'un bitmap NULL pour la ligne. Plus de détails ici:

Index partiels

Selon vos requêtes réelles, il pourrait être plus efficace d'avoir ces cinq indices partiels au lieu de celui ci-dessus:

CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 266;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 267;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 268;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 269;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 270;

Exécutez une transaction pour chacun.

La suppression en run_idtant que colonne d'index de cette manière économise 8 octets par entrée d'index - 32 au lieu de 40 octets par ligne. Chaque index est également moins cher à créer, mais en créer cinq au lieu d'un seul prend beaucoup plus de temps pour une table trop grande pour rester dans le cache (comme @ Jürgen et @Chris ont commenté). Cela peut donc vous être utile ou non.

Partitionnement

Basé sur l'héritage - la seule option jusqu'à Postgres 9.5.
(Le nouveau partitionnement déclaratif dans Postgres 11 ou, de préférence, 12 est plus intelligent.)

Le manuel:

Toutes les contraintes sur tous les enfants de la table parent sont examinées pendant l'exclusion des contraintes, donc un grand nombre de partitions est susceptible d'augmenter considérablement le temps de planification des requêtes. Ainsi, le partitionnement hérité basé sur l'héritage fonctionnera bien avec jusqu'à peut-être une centaine de partitions ; n'essayez pas d'utiliser plusieurs milliers de partitions.

Accentuation mienne. Par conséquent, en estimant 1000 valeurs différentes pour run_id, vous feriez des partitions s'étendant sur environ 10 valeurs chacune.


maintenance_work_mem

J'ai regretté que vous vous ajustiez déjà maintenance_work_memdans ma première lecture. Je vais laisser une citation et des conseils dans ma réponse pour référence. Par documentation:

maintenance_work_mem (entier)

Spécifie la quantité maximale de mémoire à utiliser par les opérations de maintenance, tels que VACUUM, CREATE INDEX, et ALTER TABLE ADD FOREIGN KEY. Il vaut par défaut 64 mégaoctets ( 64MB). Étant donné qu'une seule de ces opérations peut être exécutée à la fois par une session de base de données, et qu'une installation n'en a normalement pas beaucoup qui s'exécutent simultanément, il est sûr de définir cette valeur significativement plus grande que work_mem. Des paramètres plus importants peuvent améliorer les performances de mise sous vide et de restauration des vidages de base de données.

Notez que lors des autovacuumexécutions, autovacuum_max_workerscette mémoire peut être allouée jusqu'à plusieurs fois. Veillez donc à ne pas définir la valeur par défaut trop élevée. Il peut être utile de contrôler cela séparément setting autovacuum_work_mem.

Je le mettrais seulement aussi haut que nécessaire - ce qui dépend de la taille d'index inconnue (pour nous). Et uniquement localement pour la session d'exécution. Comme l'explique la citation, un paramètre général trop élevé peut affamer le serveur autrement, car autovacuum peut également réclamer plus de RAM. De plus, ne le définissez pas beaucoup plus haut que nécessaire, même pendant la session d'exécution, la RAM libre peut être utilisée à bon escient dans la mise en cache des données.

Cela pourrait ressembler à ceci:

BEGIN;

SET LOCAL maintenance_work_mem = 10GB;  -- depends on resulting index size

CREATE INDEX perception_run_frame_idx_run_266_thru_270 ON run.perception(run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

COMMIT;

À propos de SET LOCAL:

Les effets SET LOCALne durent que jusqu'à la fin de la transaction en cours, qu'elle soit engagée ou non.

Pour mesurer la taille des objets:

Le serveur doit généralement être configuré raisonnablement sinon, évidemment.

Erwin Brandstetter
la source
Je parie que son travail est lié aux IO car la table est beaucoup plus grande que la RAM. Lire le tableau encore plus souvent aggravera le problème, qu'il y ait suffisamment de mémoire pour trier chaque index créé en mémoire ou non.
Jürgen Strobel
Je suis avec Jurgen sur celui-ci. Je crois qu'en raison de la taille de la table, vous devez essentiellement effectuer une analyse séquentielle complète sur la table par index créé. De plus, je ne suis pas sûr que vous verrez autant d'augmentation des performances de la création des index partiels séparés (je suis sûr à 90% que vous ne verrez aucune augmentation, mais sur ce point, je pourrais être éteint.) Je crois qu'une meilleure La solution pour la création d'index impliquerait la création d'un index sur toute la plage que vous souhaitez interroger en tant qu '"index partiel unique" pour réduire le temps de génération global.
Chris
@Chris: Je suis d'accord, 5 index prendront plus de temps à créer qu'un seul (même si tous ensemble sont plus petits, la création de chaque index est moins chère et les requêtes peuvent être plus rapides). En y réfléchissant un peu plus, cela devrait être un cas d'utilisation parfait pour un index BRIN dans Postgres 9.5.
Erwin Brandstetter
3

Peut-être que c'est juste trop conçu. Avez-vous réellement essayé d'utiliser un seul index complet? Les indices partiels couvrant l'ensemble de la table ensemble ne fournissent pas beaucoup de gain, le cas échéant, pour les recherches d'index, et à partir de votre texte, je déduis que vous avez des indices pour tous les run_ids? Il peut y avoir certains avantages à indexer les analyses avec des indices partiels, mais je voudrais tout d'abord comparer la solution simple à un index.

Pour chaque création d'index, vous avez besoin d'une analyse complète liée aux E / S dans la table. Ainsi, la création de plusieurs index partiels nécessite beaucoup plus d'E / S lisant la table que pour un seul index, bien que le tri se répande sur le disque pour le seul grand index. Si vous insistez sur des indices partiels, vous pouvez essayer de construire tous (ou plusieurs) indices en même temps en parallèle (si la mémoire le permet).

Pour une estimation approximative de maintenance_work_mem nécessaire pour trier tous les run_ids, qui sont des bigints de 8 octets, en mémoire, vous auriez besoin de 10,5 * 8 Go + quelques frais généraux.

Jürgen Strobel
la source
0

Vous pouvez également créer des index sur d'autres espaces disque logiques que ceux par défaut. Ces espaces disque logiques peuvent pointer vers des disques qui ne sont pas redondants (recréez simplement les index s'ils échouent) ou qui se trouvent sur des tableaux plus rapides.

Vous pouvez également envisager de partitionner la table en utilisant les mêmes critères que vos index partiels. Cela permettrait la même vitesse que l'index lors de la requête, sans réellement créer d'index.

Kirk Roybal
la source