Quel index utiliser avec beaucoup de valeurs en double?

14

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 ~ 10 10 lignes.

  • J'ai ~ 100k lignes avec une valeur adans la colonne a, similaire pour d'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'.

  • Le tableau est écrit de telle manière que les valeurs consécutives soient physiquement proches (soit il est écrit dans l'ordre, soit nous supposons qu'il a CLUSTERété utilisé sur ce tableau et cette 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 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 vers lesquelles l'index pointe. (Nous pouvons utiliser fillfactor = 100pour diminuer un peu la taille de l'index.)

  • BRIN Je crois comprendre que je peux avoir un petit index ici au détriment de la lecture de pages inutiles. Utiliser un petit pages_per_rangesignifie 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 gros pages_per_rangesignifie que je vais lire beaucoup de pages inutiles. Existe-t-il une formule magique pour trouver une bonne valeur pages_per_rangequi tient compte de ces compromis?

  • 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 GINou un GiSTindex aiderait-il ici?

Une autre question est la suivante: Postgres utilisera-t-il le fait qu'un tableau est CLUSTERédité (en supposant qu'il n'y ait pas de mises à jour) dans le planificateur de requêtes (par exemple en recherchant 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).

foo
la source
"principalement utilisé pour la recherche en texte intégral" GiST est largement utilisé par PostGIS.
jpmc26

Réponses:

15

BTree

Mon problème ici est que l'index BTree sera énorme, car il stockera des valeurs en double (il en 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 «couvre» 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. Utiliser un petit pages_per_rangesignifie 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 gros pages_per_rangesignifie 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 (cela est crucial pour que BRIN soit utile). Les index BRIN sont minuscules , donc toutes les pages sont susceptibles d'être en mémoire si vous choisissez une valeur appropriée pour 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 avec pages_per_range un peu moins que la taille moyenne (en pages) occupée par la avaleur moyenne . Vous essayez probablement de minimiser: (nombre de pages BRIN analysées) + (nombre de pages de segment analysées) pour une requête standard. Recherchez Heap Blocks: lossy=ndans le plan d'exécution pages_per_range=1et comparez avec d'autres valeurs pour pages_per_range- c.-à-d. Voyez 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. Est-ce qu'un index GIN/ GiSTaiderait ici?

GIN peut être utile, mais probablement pas GiST - cependant, si le clustering naturel est vraiment bon, alors 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 les vôtres:

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 DEMANDE |
| : ------------------------------------------------- -------------------------------------------------- ------------------------------------------- |
| 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 DEMANDE |
| : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
| 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 en 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 DEMANDE |
| : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
| 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 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 DEMANDE |
| : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
| 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 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 DEMANDE |
| : ------------------------------------------------- -------------------------------------------------- ------------------------------ |
| 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 sur 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

Jack dit d'essayer topanswers.xyz
la source
Un indice de couverture sauterait donc complètement la lecture de la table au détriment de l'espace disque? On dirait un bon compromis. Je pense que nous voulons dire la même chose pour l'index BRIN par `` lire l'index entier '' (corrigez-moi si je me trompe), je voulais dire balayer tout l'index BRIN qui, je pense, est ce qui se passe dans dbfiddle.uk/… , non?
foo
@foo à propos du "(il l'a aussi, car il ne peut pas supposer que la table est triée physiquement)." L'ordre physique (cluster ou non) de la table n'a pas d'importance. L'index a les valeurs dans le bon ordre. Mais les index B-tree de Postgres doivent stocker toutes les valeurs (et oui, plusieurs fois). Voilà comment ils sont conçus. Le stockage de chaque valeur distincte une seule fois serait une fonctionnalité / amélioration intéressante. Vous pouvez le suggérer aux développeurs de Postgres (et même aider à l'implémenter.) Jack devrait commenter, je pense que l'implémentation d'Oracle de b-trees fait cela.
ypercubeᵀᴹ
1
@foo - vous avez tout à fait raison, une analyse d'un index BRIN scanne toujours l'intégralité de l'index ( pgcon.org/2016/schedule/attachments/… , 2e dernière diapositive) - bien que cela ne soit pas indiqué sur le plan d'explication dans le violon n'est-ce pas?
Jack dit d'essayer topanswers.xyz
2
@ ypercubeᵀᴹ vous pouvez utiliser COMPRESS sur Oracle qui stocke chaque préfixe distinct une fois par bloc.
Jack dit d'essayer topanswers.xyz
@JackDouglas J'ai lu Bitmap Index Scancomme signifiant «lire l'index de brin entier» mais c'est peut-être la mauvaise lecture. Oracle COMPRESSressemble à quelque chose qui serait utile ici car cela réduirait la taille de l'arbre B, mais je suis coincé avec pg!
foo
6

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

  • INCLUDEindex . 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 les pages feuilles, 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 (a,b)index trouvera les lignes correspondantes avec une seule recherche, l'index include devra passer par les valeurs (peut-être 100K comme dans votre cas) qui correspondent a = 'a'et vérifier leb valeurs.
    En revanche, l'index est légèrement moins large que l' (a,b)index et vous n'avez pas besoin de l'ordre bpour que votre requête soit calculée SUM(b). Vous pourriez aussi 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 pourrait sonne un peu fou * dans un premier temps :

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

    Un index pour chaque avaleur. 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 des bvaleurs). Dans tous les autres aspects cependant, il (les 100K index ensemble) agira comme un index b-tree (a,b)tout en utilisant l'espace d'un (b)index.
    L'inconvénient est que vous devrez les créer et les maintenir vous-même, chaque fois qu'une nouvelle valeur de aest 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. Étant donné que le tableau est plutôt 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) .

ypercubeᵀᴹ
la source
1 est intéressant, mais comme vous le faites remarquer, la page 10 n'est pas encore publiée. 2 semble fou (ou du moins contre la «sagesse commune»), je vais avoir une lecture car, comme vous le faites remarquer, cela pourrait fonctionner avec mon flux de travail presque sans écriture. 3. ne fonctionnerait pas pour moi, j'ai utilisé SUMcomme exemple, mais dans la pratique mes requêtes ne peuvent pas être précalculées (elles ressemblent plus à select ... from t where a = '?' and ??wjere ??serait une autre condition définie par l'utilisateur.
foo
1
Eh bien, nous ne pouvons pas aider si nous ne savons pas ce que ??c'est;)
ypercubeᵀᴹ
Vous mentionnez des index filtrés. Qu'en est-il du partitionnement de la table?
jpmc26
@ jpmc26 drôle, je pensais ajouter dans la réponse que la suggestion d'index filtrés est en quelque sorte une forme de partitionnement. Le partitionnement pourrait également être utile ici, mais je ne suis pas sûr. Il en résulterait beaucoup de petits index / tables.
ypercubeᵀᴹ
2
Je m'attends à ce que les index btree de couverture partielle soient le roi des performances ici, car les données ne sont presque jamais mises à jour. Même si cela signifie des index de 100k. La taille totale de l'index est la plus petite (sauf pour un index BRIN, mais Postgres doit en outre lire et filtrer les pages de tas). La génération d'index peut être automatisée avec SQL dynamique. Exemple de DOdéclaration dans cette réponse connexe .
Erwin Brandstetter