web-dev-qa-db-fra.com

Quel index utiliser avec beaucoup de valeurs en double?

Faisons quelques hypothèses:

J'ai une table qui ressemble à ceci:

 a | b
---+---
 a | -1
 a | 17
  ...
 a | 21
 c | 17
 c | -3
  ...
 c | 22

Faits sur mon set:

  • La taille de la table entière est ~ 10dix Lignes.

  • J'ai ~ 100k lignes avec la valeur a dans la colonne a, similaire pour les autres valeurs (par exemple c).

  • Cela signifie ~ 100k valeurs distinctes dans la colonne 'a'.

  • La plupart de mes requêtes liront toutes ou la plupart des valeurs pour une valeur donnée dans un, par exemple select sum(b) from t where a = 'c'.

  • La table est écrite de telle manière que les valeurs consécutives soient physiquement proches (soit elles sont écrites dans l'ordre, soit nous supposons CLUSTER a été utilisé sur cette table et colonne a ).

  • Le tableau est rarement, voire jamais mis à jour, nous ne nous préoccupons que de la vitesse de lecture.

  • Le tableau est relativement étroit (disons ~ 25 octets par tuple, + 23 octets de surcharge).

Maintenant, la question est, quel type d'index dois-je utiliser? Ma compréhension est:

  • BTree Mon problème ici est que l'index BTree sera énorme car pour autant que je sache, il stockera des valeurs en double (il le doit, car il peut ' t supposer que la table est triée physiquement). Si le BTree est énorme, je finis par devoir lire à la fois l'index et les parties du tableau vers lesquelles l'index pointe. (Nous pouvons utiliser fillfactor = 100 Pour diminuer un peu la taille de l'index.)

  • [~ # ~] brin [~ # ~] Ma compréhension est que je peux avoir un petit index ici au détriment de la lecture de pages inutiles. Utiliser un petit pages_per_range Signifie que l'index est plus grand (ce qui est un problème avec BRIN car j'ai besoin de lire tout l'index), avoir un grand pages_per_range Signifie que je vais lire beaucoup de pages inutiles. Existe-t-il une formule magique pour trouver une bonne valeur de pages_per_range Qui prend en compte ces compromis?

  • GIN/Gist Je ne sais pas si ceux-ci sont pertinents ici car ils sont principalement utilisés pour la recherche en texte intégral, mais j'entends également qu'ils sont bons pour traiter avec des clés en double. Un index GIN ou Gist serait-il utile ici?

Une autre question est la suivante: Postgres utilisera-t-il le fait qu'une table est CLUSTERed (en supposant qu'aucune mise à jour) dans le planificateur de requêtes (par exemple en recherchant en binaire les pages de début/fin pertinentes)? Quelque peu lié, puis-je simplement stocker toutes mes colonnes dans un BTree et supprimer complètement la table (ou obtenir quelque chose d'équivalent, je pense que ce sont des index clusterisés dans SQL Server)? Y a-t-il un indice hybride BTree/BRIN qui pourrait aider ici?

Je préfère éviter d'utiliser des tableaux pour stocker mes valeurs car ma requête sera moins lisible de cette façon (je comprends que cela réduirait le coût des 23 octets par surcharge de tuple en réduisant le nombre de tuples).

14
foo

BTree

Mon problème ici est que l'index BTree sera énorme, car il stockera des valeurs en double (il l'a aussi, car il ne peut pas supposer que la table est triée physiquement). Si le BTree est énorme, je finis par devoir lire à la fois l'index et les parties du tableau que l'index pointe aussi ...

Pas nécessairement - Avoir un index btree qui est 'couvrant' sera le temps de lecture le plus rapide, et si c'est tout ce que vous voulez (c'est-à-dire si vous pouvez vous permettre le stockage supplémentaire), alors c'est votre meilleur pari .

BRIN

Je crois comprendre que je peux avoir un petit index ici au détriment de la lecture de pages inutiles. À l'aide d'un petit pages_per_range signifie que l'index est plus grand (ce qui est un problème avec BRIN car j'ai besoin de lire tout l'index), ayant un gros pages_per_range signifie que je vais lire beaucoup de pages inutiles.

Si vous ne pouvez pas vous permettre les frais de stockage d'un index btree couvrant, BRIN est idéal pour vous, car vous avez déjà un clustering en place (ceci est crucial pour que BRIN soit utile). Index BRIN sont minuscules , donc toutes les pages sont susceptibles d'être en mémoire si vous choisissez une valeur appropriée de pages_per_range.

Existe-t-il une formule magique pour trouver une bonne valeur de pages_per_range qui prend en compte ces compromis?

Pas de formule magique, mais commencez par pages_per_rangen peu moins que la taille moyenne (en pages) occupée par la valeur moyenne a. Vous essayez probablement de minimiser: (nombre de pages BRIN analysées) + (nombre de pages de segment analysées) pour une requête standard. Chercher Heap Blocks: lossy=n dans le plan d'exécution pour pages_per_range=1 et comparer avec d'autres valeurs pour pages_per_range - c'est-à-dire voir combien de blocs de tas inutiles sont analysés.

GIN/Gist

Je ne suis pas sûr que ceux-ci soient pertinents ici car ils sont principalement utilisés pour la recherche en texte intégral, mais j'entends également qu'ils sont bons pour traiter les clés en double. Un index GIN/Gist serait-il utile ici?

Le GIN peut être utile, mais probablement pas Gist - cependant, si le regroupement naturel est vraiment bon, alors le BRIN sera probablement un meilleur pari.

Voici un exemple de comparaison entre les différents types d'index pour des données factices un peu comme la vôtre:

table et index:

create table foo(a,b,c) as
select *, lpad('',20)
from (select chr(g) a from generate_series(97,122) g) a
     cross join (select generate_series(1,100000) b) b
order by a;
create index foo_btree_covering on foo(a,b);
create index foo_btree on foo(a);
create index foo_gin on foo using gin(a);
create index foo_brin_2 on foo using brin(a) with (pages_per_range=2);
create index foo_brin_4 on foo using brin(a) with (pages_per_range=4);
vacuum analyze;

tailles de relation:

select relname "name", pg_size_pretty(siz) "size", siz/8192 pages, (select count(*) from foo)*8192/siz "rows/page"
from( select relname, pg_relation_size(C.oid) siz
      from pg_class c join pg_namespace n on n.oid = c.relnamespace
      where nspname = current_schema ) z;
 nom | taille | pages | lignes/page 
: -------------- : ------ | ----: | --------: 
 foo | 149 Mo | 19118 | 135 
 Foo_btree_covering | 56 Mo | 7132 | 364 
 Foo_btree | 56 Mo | 7132 | 364 
 Foo_gin | 2928 kB | 366 | 7103 
 Foo_brin_2 | 264 kB | 33 | 78787 
 Foo_brin_4 | 136 kB | 17 | 152941 

couvrant btree:

explain analyze select sum(b) from foo where a='a';
 | PLAN DE REQUÊTE | 
 | : ------------------------------------------------- -------------------------------------------------- ------------------------------------------- | 
 | Agrégat (coût = 3282,57..3282,58 lignes = 1 largeur = 8) (temps réel = 45,942..45,942 lignes = 1 boucles = 1) | 
 | -> Indexation uniquement en utilisant foo_btree_covering sur foo (coût = 0,43..3017,80 lignes = 105907 largeur = 4) (temps réel = 0,038..27,286 lignes = 100000 boucles = 1) | 
 | Index Cond: (a = 'a' :: texte) | 
 | Récupérations de tas: 0 | 
 | Temps de planification: 0,099 ms | 
 | Temps d'exécution: 45,968 ms | 

btree ordinaire:

drop index foo_btree_covering;
explain analyze select sum(b) from foo where a='a';
 | PLAN DE REQUÊTE | 
 | : ------------------------------------------------- -------------------------------------------------- ----------------------------- | 
 | Agrégat (coût = 4064,57..4064,58 lignes = 1 largeur = 8) (temps réel = 54.242..54.242 lignes = 1 boucles = 1) | 
 | -> Index Scan utilisant foo_btree sur foo (coût = 0,43..3799,80 lignes = 105907 largeur = 4) (temps réel = 0,037..33,084 lignes = 100000 boucles = 1) | 
 | Index Cond: (a = 'a' :: texte) | 
 | Temps de planification: 0,135 ms | 
 | Temps d'exécution: 54,280 ms | 

BRIN pages_per_range = 4:

drop index foo_btree;
explain analyze select sum(b) from foo where a='a';
 | PLAN DE REQUÊTE | 
 | : ------------------------------------------------- -------------------------------------------------- ----------------------------- | 
 | Agrégat (coût = 21595,38..21595,39 lignes = 1 largeur = 8) (temps réel = 52,455..52,455 lignes = 1 boucles = 1) | 
 | -> Bitmap Heap Scan on foo (coût = 888,78..21330,61 lignes = 105907 largeur = 4) (temps réel = 2,738..31,967 lignes = 100000 boucles = 1) | 
 | Vérifiez à nouveau Cond: (a = 'a' :: text) | 
 | Lignes supprimées par la nouvelle vérification de l'index: 96 | 
 | Blocs de tas: lossy = 736 | 
 | -> Bitmap Index Scan sur foo_brin_4 (coût = 0,00..862,30 lignes = 105907 largeur = 0) (temps réel = 2,720..2,720 lignes = 7360 boucles = 1) | 
 | Index Cond: (a = 'a' :: texte) | 
 | Temps de planification: 0,101 ms | 
 | Temps d'exécution: 52,501 ms | 

BRIN pages_per_range = 2:

drop index foo_brin_4;
explain analyze select sum(b) from foo where a='a';
 | PLAN DE REQUÊTE | 
 | : ------------------------------------------------- -------------------------------------------------- ----------------------------- | 
 | Agrégat (coût = 21659,38..21659,39 lignes = 1 largeur = 8) (temps réel = 53,971..53,971 lignes = 1 boucles = 1) | 
 | -> Bitmap Heap Scan on foo (coût = 952,78..21394,61 lignes = 105907 largeur = 4) (temps réel = 5,286..33,492 lignes = 100000 boucles = 1) | 
 | Vérifiez à nouveau Cond: (a = 'a' :: text) | 
 | Lignes supprimées par la nouvelle vérification de l'index: 96 | 
 | Blocs de tas: lossy = 736 | 
 | -> Bitmap Index Scan sur foo_brin_2 (coût = 0,00..926,30 lignes = 105907 largeur = 0) (temps réel = 5,275..5,275 lignes = 7360 boucles = 1) | 
 | Index Cond: (a = 'a' :: texte) | 
 | Temps de planification: 0,095 ms | 
 | Temps d'exécution: 54,016 ms | 

GIN:

drop index foo_brin_2;
explain analyze select sum(b) from foo where a='a';
 | PLAN DE REQUÊTE | 
 | : ------------------------------------------------- -------------------------------------------------- ------------------------------ | 
 | Agrégat (coût = 21687,38..21687,39 lignes = 1 largeur = 8) (temps réel = 55,331..55,331 lignes = 1 boucles = 1) | 
 | -> Bitmap Heap Scan on foo (coût = 980,78..21422,61 lignes = 105907 largeur = 4) (temps réel = 12,377..33,956 lignes = 100000 boucles = 1) | 
 | Vérifiez à nouveau Cond: (a = 'a' :: text) | 
 | Blocs de tas: exact = 736 | 
 | -> Bitmap Index Scan sur foo_gin (coût = 0,00..954,30 lignes = 105907 largeur = 0) (temps réel = 12,271..12,271 lignes = 100000 boucles = 1) | 
 | Index Cond: (a = 'a' :: texte) | 
 | Temps de planification: 0,118 ms | 
 | Temps d'exécution: 55,366 ms | 

dbfiddle --- (ici

Outre btree et brin qui semblent être les options les plus sensées, d'autres, options exotiques qui méritent d'être étudiées - elles pourraient être utiles ou non dans votre cas:

  • INCLUDE index . Ils seront - espérons-le - dans la prochaine version majeure (10) de Postgres, vers septembre 2017. Un index sur (a) INCLUDE (b) A la même structure qu'un index sur (a) Mais inclut dans la feuille pages, toutes les valeurs de b (mais non ordonnées). Ce qui signifie que vous ne pouvez pas l'utiliser par exemple pour SELECT * FROM t WHERE a = 'a' AND b = 2 ;. L'index peut être utilisé mais alors qu'un index (a,b) Trouvera les lignes correspondantes avec une seule recherche, l'index include devra passer par les valeurs (éventuellement 100K comme dans votre cas) qui correspondent à a = 'a' et vérifiez les valeurs b.
    En revanche, l'index est légèrement moins large que l'index (a,b) Et vous n'avez pas besoin de l'ordre sur b pour que votre requête calcule SUM(b). Vous pouvez également avoir par exemple (a) INCLUDE (b,c,d) Qui peut être utilisé pour des requêtes similaires aux vôtres qui se regroupent sur les 3 colonnes.

  • Index filtrés (partiels) . Une suggestion qui peut sembler un peu folle* en premier:

    CREATE INDEX flt_a  ON t (b) WHERE (a = 'a') ;
    ---
    CREATE INDEX flt_xy ON t (b) WHERE (a = 'xy') ;
    

    Un index pour chaque valeur de a. Dans votre cas, environ 100 000 index. Bien que cela semble beaucoup, considérez que chaque index sera très petit, à la fois en taille (nombre de lignes) et en largeur (car il ne stockera que les valeurs b). Dans tous les autres aspects cependant, il (les 100K index ensemble) agira comme un index b-tree sur (a,b) Tout en utilisant l'espace d'un index (b).
    L'inconvénient est que vous devrez les créer et les gérer vous-même, chaque fois qu'une nouvelle valeur de a est ajoutée dans le tableau. Étant donné que votre table est plutôt stable, sans beaucoup (ou pas) d'insertions/mises à jour, cela ne semble pas être un problème.

  • Tableaux récapitulatifs. Comme le tableau est assez stable, vous pouvez toujours créer et remplir un tableau récapitulatif avec les agrégats les plus courants dont vous aurez besoin (sum(b), sum(c), sum(d), avg(b), count(distinct b), etc). Il sera petit (seulement 100 000 lignes) et ne devra être rempli qu'une seule fois et mis à jour uniquement lorsque des lignes seront insérées/mises à jour/supprimées sur la table principale.

*: idée copiée de cette société qui gère 10 millions d'index dans leur système de production: The Heap: Running 10 Million Postgresql Indexes in Production (and counting) .

6
ypercubeᵀᴹ