PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Indexation de base de données dans PostgreSQL

L'indexation de base de données est l'utilisation de structures de données spéciales qui visent à améliorer les performances, en obtenant un accès direct aux pages de données. Un index de base de données fonctionne comme la section index d'un livre imprimé :en regardant dans la section index, il est beaucoup plus rapide d'identifier la ou les pages qui contiennent le terme qui nous intéresse. On peut facilement localiser les pages et y accéder directement . C'est au lieu de parcourir les pages du livre de manière séquentielle, jusqu'à ce que nous trouvions le terme que nous recherchons.

Les index sont un outil essentiel entre les mains d'un DBA. L'utilisation d'index peut fournir d'importants gains de performances pour une variété de domaines de données. PostgreSQL est connu pour sa grande extensibilité et la riche collection d'addons principaux et tiers, et l'indexation ne fait pas exception à cette règle. Les index PostgreSQL couvrent un large éventail de cas, des index b-tree les plus simples sur les types scalaires aux index géospatiaux GiST en passant par les index fts ou json ou tableau GIN.

Les index, cependant, aussi merveilleux qu'ils paraissent (et sont en réalité !) ne sont pas gratuits. Il y a une certaine pénalité qui accompagne les écritures sur une table indexée. Ainsi, le DBA, avant d'examiner ses options pour créer un index spécifique, doit d'abord s'assurer que ledit index a du sens en premier lieu, ce qui signifie que les gains de sa création l'emporteront sur la perte de performances en écriture.

Terminologie de l'index de base PostgreSQL

Avant de décrire les types d'index dans PostgreSQL et leur utilisation, examinons quelques termes que tout DBA rencontrera tôt ou tard lors de la lecture de la documentation.

  • Méthode d'accès à l'index (également appelé Méthode d'accès ):Le type d'index (B-tree, GiST, GIN, etc)
  • Tapez : le type de données de la colonne indexée
  • Opérateur : une fonction entre deux types de données
  • Famille d'opérateur : opérateur de type de données croisées, en regroupant les opérateurs de types ayant un comportement similaire
  • Classe d'opérateur (également mentionné comme stratégie d'indexation ) :définit les opérateurs à utiliser par l'index pour une colonne

Dans le catalogue système de PostgreSQL, les méthodes d'accès sont stockées dans pg_am, les classes d'opérateurs dans pg_opclass, les familles d'opérateurs dans pg_opfamily. Les dépendances de ce qui précède sont illustrées dans le diagramme ci-dessous :

Types d'index dans PostgreSQL

PostgreSQL fournit les types d'index suivants :

  • Arbre B : l'index par défaut, applicable pour les types pouvant être triés
  • Hachage : gère uniquement l'égalité
  • GIST : adapté aux types de données non scalaires (par exemple, formes géométriques, pieds, tableaux)
  • SP-GiST : espace partitionné GIST, une évolution de GiST pour la gestion des structures non équilibrées (quadtrees, k-d trees, radix trees)
  • GIN : adapté aux types complexes (par exemple jsonb, fts, arrays )
  • BRIN : un type d'index relativement nouveau qui prend en charge les données pouvant être triées en stockant les valeurs min/max dans chaque bloc

Low, nous allons essayer de nous salir les mains avec des exemples concrets. Tous les exemples donnés sont réalisés avec PostgreSQL 10.0 (avec les clients psql 10 et 9) sur FreeBSD 11.1.

Index B-tree

Supposons que nous ayons le tableau suivant :

create table part (
id serial primary key, 
partno varchar(20) NOT NULL UNIQUE, 
partname varchar(80) NOT NULL, 
partdescr text,
machine_id int NOT NULL
);
testdb=# \d part
                                  Table "public.part"
   Column       |         Type          |                     Modifiers                     
------------+-----------------------+---------------------------------------------------
 id         | integer                 | not null default nextval('part_id_seq'::regclass)
 partno     | character varying(20)| not null
 partname       | character varying(80)| not null
 partdescr      | text                    |
 machine_id     | integer                 | not null
Indexes:
    "part_pkey" PRIMARY KEY, btree (id)
    "part_partno_key" UNIQUE CONSTRAINT, btree (partno)

Lorsque nous définissons cette table plutôt commune, PostgreSQL crée deux index B-tree uniques dans les coulisses :part_pkey et part_partno_key. Ainsi, chaque contrainte unique dans PostgreSQL est implémentée avec un INDEX unique. Remplissons notre tableau avec un million de lignes de données :

testdb=# with populate_qry as (select gs from generate_series(1,1000000) as gs )
insert into part (partno, partname,machine_id) SELECT 'PNo:'||gs, 'Part '||gs,0 from populate_qry;
INSERT 0 1000000

Essayons maintenant de faire quelques requêtes sur notre table. Nous disons d'abord au client psql de signaler les temps de requête en tapant \timing :

testdb=# select * from part where id=100000;
   id   |   partno   |  partname   | partdescr | machine_id
--------+------------+-------------+-----------+------------
 100000 | PNo:100000 | Part 100000 |           |          0
(1 row)

Time: 0,284 ms
testdb=# select * from part where partno='PNo:100000';
   id   |   partno   |  partname   | partdescr | machine_id
--------+------------+-------------+-----------+------------
 100000 | PNo:100000 | Part 100000 |           |          0
(1 row)

Time: 0,319 ms

Nous observons qu'il ne faut que quelques fractions de milliseconde pour obtenir nos résultats. Nous nous y attendions puisque pour les deux colonnes utilisées dans les requêtes ci-dessus, nous avons déjà défini les index appropriés. Essayons maintenant d'interroger la colonne partname, pour laquelle aucun index n'existe.

testdb=# select * from part where partname='Part 100000';
   id   |   partno   |  partname   | partdescr | machine_id
--------+------------+-------------+-----------+------------
 100000 | PNo:100000 | Part 100000 |           |          0
(1 row)

Time: 89,173 ms

Ici on voit clairement que pour la colonne non indexée, les performances baissent significativement. Créons maintenant un index sur cette colonne et répétons la requête :

testdb=# create index part_partname_idx ON part(partname);
CREATE INDEX
Time: 15734,829 ms (00:15,735)
testdb=# select * from part where partname='Part 100000';
   id   |   partno   |  partname   | partdescr | machine_id
--------+------------+-------------+-----------+------------
 100000 | PNo:100000 | Part 100000 |           |          0
(1 row)

Time: 0,525 ms

Notre nouvel index part_partname_idx est également un index B-tree (par défaut). Notons tout d'abord que la création de l'index sur la table des millions de lignes a pris un temps non négligeable, environ 16 secondes. Ensuite, nous observons que notre vitesse de requête est passée de 89 ms à 0,525 ms. Les index B-tree, en plus de vérifier l'égalité, peuvent également aider avec des requêtes impliquant d'autres opérateurs sur des types ordonnés, tels que <,<=,>=,>. Essayons avec <=et>=

testdb=# select count(*) from part where partname>='Part 9999900';
 count
-------
     9
(1 row)

Time: 0,359 ms
testdb=# select count(*) from part where partname<='Part 9999900';
 count  
--------
 999991
(1 row)

Time: 355,618 ms

La première requête est beaucoup plus rapide que la seconde, en utilisant les mots clés EXPLAIN (ou EXPLAIN ANALYZE) nous pouvons voir si l'index réel est utilisé ou non :

testdb=# explain select count(*) from part where partname>='Part 9999900';
                                       QUERY PLAN                                        
-----------------------------------------------------------------------------------------
 Aggregate  (cost=8.45..8.46 rows=1 width=8)
   ->  Index Only Scan using part_partname_idx on part  (cost=0.42..8.44 rows=1 width=0)
         Index Cond: (partname >= 'Part 9999900'::text)
(3 rows)

Time: 0,671 ms
testdb=# explain select count(*) from part where partname<='Part 9999900';
                                       QUERY PLAN                                       
----------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=14603.22..14603.23 rows=1 width=8)
   ->  Gather  (cost=14603.00..14603.21 rows=2 width=8)
         Workers Planned: 2
         ->  Partial Aggregate  (cost=13603.00..13603.01 rows=1 width=8)
               ->  Parallel Seq Scan on part  (cost=0.00..12561.33 rows=416667 width=0)
                     Filter: ((partname)::text <= 'Part 9999900'::text)
(6 rows)

Time: 0,461 ms

Dans le premier cas, le planificateur de requête choisit d'utiliser l'index part_partname_idx. Nous observons également que cela se traduira par une analyse d'index uniquement, ce qui signifie aucun accès aux tables de données. Dans le second cas, le planificateur détermine qu'il est inutile d'utiliser l'index car les résultats renvoyés représentent une grande partie de la table, auquel cas une analyse séquentielle est considérée comme plus rapide.

Index de hachage

L'utilisation d'index de hachage jusqu'à et y compris PgSQL 9.6 a été déconseillée pour des raisons liées au manque d'écriture WAL. À partir de PgSQL 10.0, ces problèmes ont été résolus, mais les index de hachage n'avaient toujours pas de sens à utiliser. Il y a des efforts dans PgSQL 11 pour faire des index de hachage une méthode d'index de première classe avec ses grands frères (B-tree, GiST, GIN). Donc, avec cela à l'esprit, essayons en fait un index de hachage en action.

Nous allons enrichir notre table de pièces avec une nouvelle colonne parttype et la remplir avec des valeurs de distribution égale, puis exécuter une requête qui teste le type de pièce égal à "Steering" :

testdb=# alter table part add parttype varchar(100) CHECK (parttype in ('Engine','Suspension','Driveline','Brakes','Steering','General')) NOT NULL DEFAULT 'General';
ALTER TABLE
Time: 42690,557 ms (00:42,691)
testdb=# with catqry as  (select id,(random()*6)::int % 6 as cat from part)
update part SET parttype = CASE WHEN cat=1 THEN 'Engine' WHEN cat=2 THEN 'Suspension' WHEN cat=3 THEN 'Driveline' WHEN cat=4 THEN 'Brakes' WHEN cat=5 THEN 'Steering' ELSE 'General' END FROM catqry WHERE part.id=catqry.id;
UPDATE 1000000
Time: 46345,386 ms (00:46,345)
testdb=# select count(*) from part where id % 500 = 0 AND parttype = 'Steering';
 count
-------
   322
(1 row)

Time: 93,361 ms

Nous créons maintenant un index de hachage pour cette nouvelle colonne et réessayons la requête précédente :

testdb=# create index part_parttype_idx ON part USING hash(parttype);
CREATE INDEX
Time: 95525,395 ms (01:35,525)
testdb=# analyze ;
ANALYZE
Time: 1986,642 ms (00:01,987)
testdb=# select count(*) from part where id % 500 = 0 AND parttype = 'Steering';
 count
-------
   322
(1 row)

Time: 63,634 ms

Nous notons l'amélioration après l'utilisation de l'index de hachage. Nous allons maintenant comparer les performances d'un index de hachage sur des entiers avec l'index b-tree équivalent.

testdb=# update part set machine_id = id;
UPDATE 1000000
Time: 392548,917 ms (06:32,549)
testdb=# select * from part where id=500000;
   id   |   partno   |  partname   | partdescr | machine_id |  parttype  
--------+------------+-------------+-----------+------------+------------
 500000 | PNo:500000 | Part 500000 |           |     500000 | Suspension
(1 row)

Time: 0,316 ms
testdb=# select * from part where machine_id=500000;
   id   |   partno   |  partname   | partdescr | machine_id |  parttype  
--------+------------+-------------+-----------+------------+------------
 500000 | PNo:500000 | Part 500000 |           |     500000 | Suspension
(1 row)

Time: 97,037 ms
testdb=# create index part_machine_id_idx ON part USING hash(machine_id);
CREATE INDEX
Time: 4756,249 ms (00:04,756)
testdb=#
testdb=# select * from part where machine_id=500000;
   id   |   partno   |  partname   | partdescr | machine_id |  parttype  
--------+------------+-------------+-----------+------------+------------
 500000 | PNo:500000 | Part 500000 |           |     500000 | Suspension
(1 row)

Time: 0,297 ms

Comme nous le voyons, avec l'utilisation d'index de hachage, la vitesse des requêtes qui vérifient l'égalité est très proche de la vitesse des index B-tree. On dit que les index de hachage sont légèrement plus rapides pour l'égalité que les arbres B, en fait nous avons dû essayer chaque requête deux ou trois fois jusqu'à ce que l'index de hachage donne un meilleur résultat que l'équivalent en arbre B.

Téléchargez le livre blanc aujourd'hui PostgreSQL Management &Automation with ClusterControlDécouvrez ce que vous devez savoir pour déployer, surveiller, gérer et faire évoluer PostgreSQLTélécharger le livre blanc

Index GiST

GiST (Generalized Search Tree) est plus qu'un simple type d'index, mais plutôt une infrastructure pour construire de nombreuses stratégies d'indexation. La distribution PostgreSQL par défaut prend en charge les types de données géométriques, tsquery et tsvector. Dans contrib, il existe des implémentations de nombreuses autres classes d'opérateurs. En lisant les docs et le répertoire contrib, le lecteur observera qu'il y a un chevauchement assez important entre les cas d'utilisation GiST et GIN :tableaux int, recherche plein texte pour nommer les cas principaux. Dans ces cas, GIN est plus rapide, et la documentation officielle l'indique explicitement. Cependant, GiST fournit une prise en charge étendue des types de données géométriques. De plus, au moment d'écrire ces lignes, GiST (et SP-GiST) est la seule méthode significative qui peut être utilisée avec des contraintes d'exclusion. Nous verrons un exemple à ce sujet. Supposons (en restant dans le domaine du génie mécanique) que nous ayons besoin de définir des variations de type de machines pour un type de machine particulier, valables pour une certaine période dans le temps; et que pour une variation particulière, aucune autre variation ne peut exister pour le même type de machine dont la période de temps chevauche (conflit) avec la période de variation particulière.

create table machine_type (
	id SERIAL PRIMARY KEY, 
	mtname varchar(50) not null, 
	mtvar varchar(20) not null, 
	start_date date not null, 
	end_date date, 
	CONSTRAINT machine_type_uk UNIQUE (mtname,mtvar)
);

Ci-dessus, nous disons à PostgreSQL que pour chaque nom de type de machine (mtname), il ne peut y avoir qu'une seule variation (mtvar). Start_date indique la date de début de la période pendant laquelle cette variation de type de machine est valide, et end_date indique la date de fin de cette période. Une date de fin nulle signifie que la variation du type de machine est actuellement valide. Maintenant, nous voulons exprimer l'exigence de non-chevauchement avec une contrainte. Pour ce faire, utilisez une contrainte d'exclusion :

testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);

La syntaxe EXCLUDE PostgreSQL nous permet de spécifier de nombreuses colonnes de types différents et avec un opérateur différent pour chacune. &&est l'opérateur de chevauchement pour les plages de dates et =est l'opérateur d'égalité commun pour varchar. Mais tant que nous appuyons sur Entrée, PostgreSQL se plaint avec un message :

ERROR:  data type character varying has no default operator class for access method "gist"
HINT:  You must specify an operator class for the index or define a default operator class for the data type.

Ce qui manque ici, c'est le support de la classe d'opérations GiST pour varchar. Si nous avons construit et installé avec succès l'extension btree_gist, nous pouvons procéder à la création de l'extension :

testdb=# create extension btree_gist ;
CREATE EXTENSION

Et puis réessayez de créer la contrainte et de la tester :

testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);
ALTER TABLE
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SH','2008-01-01','2013-01-01');
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SG','2002-01-01','2009-01-01');
ERROR:  conflicting key value violates exclusion constraint "machine_type_per"
DETAIL:  Key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2002-01-01,2009-01-01)) conflicts with existing key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2008-01-01,2013-01-01)).
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SG','2002-01-01','2008-01-01');
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SJ','2013-01-01',null);
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SJ2','2018-01-01',null);
ERROR:  conflicting key value violates exclusion constraint "machine_type_per"
DETAIL:  Key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2018-01-01,)) conflicts with existing key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2013-01-01,)).

Index SP-GiST

SP-GiST, qui signifie GiST partitionné par espace, comme GiST, est une infrastructure qui permet le développement de nombreuses stratégies différentes dans le domaine des structures de données sur disque non équilibrées. La distribution PgSQL par défaut prend en charge les points bidimensionnels, les plages (de tout type), les types de texte et inet. Comme GiST, SP-GiST peut être utilisé dans les contraintes d'exclusion, d'une manière similaire à l'exemple présenté dans le chapitre précédent.

Indices GIN

GIN (Generalized Inverted Index) comme GiST et SP-GiST peut fournir de nombreuses stratégies d'indexation. GIN est adapté lorsque nous voulons indexer des colonnes de types composites. La distribution PostgreSQL par défaut prend en charge tout type de tableau, jsonb et la recherche en texte intégral (tsvector). Dans contrib, il existe des implémentations de nombreuses autres classes d'opérateurs. Jsonb, une fonctionnalité très appréciée de PostgreSQL (et un développement relativement récent (9.4+)) s'appuie sur GIN pour la prise en charge des index. Une autre utilisation courante de GIN est l'indexation pour la recherche en texte intégral. La recherche en texte intégral dans PgSQL mérite un article à elle seule, nous n'aborderons donc ici que la partie indexation. Commençons par préparer notre table, en donnant des valeurs non nulles à la colonne partdescr et en mettant à jour une seule ligne avec une valeur significative :

testdb=# update part set partdescr ='';
UPDATE 1000000
Time: 383407,114 ms (06:23,407)
testdb=# update part set partdescr = 'thermostat for the cooling system' where id=500000;
UPDATE 1
Time: 2,405 ms

Ensuite, nous effectuons une recherche textuelle sur la colonne nouvellement mise à jour :

testdb=# select * from part where partdescr @@ 'thermostat';
   id   |   partno   |  partname   |             partdescr             | machine_id |  parttype  
--------+------------+-------------+-----------------------------------+------------+------------
 500000 | PNo:500000 | Part 500000 | thermostat for the cooling system |     500000 | Suspension
(1 row)

Time: 2015,690 ms (00:02,016)

C'est assez lent, presque 2 secondes pour amener notre résultat. Essayons maintenant de créer un index GIN sur le type tsvector, et répétons la requête, en utilisant une syntaxe compatible avec les index :

testdb=# CREATE INDEX part_partdescr_idx ON part USING gin(to_tsvector('english',partdescr));
CREATE INDEX
Time: 1431,550 ms (00:01,432)
testdb=# select * from part where to_tsvector('english',partdescr) @@ to_tsquery('thermostat');
   id   |   partno   |  partname   |             partdescr             | machine_id |  parttype  
--------+------------+-------------+-----------------------------------+------------+------------
 500000 | PNo:500000 | Part 500000 | thermostat for the cooling system |     500000 | Suspension
(1 row)

Time: 0,952 ms

Et nous obtenons une vitesse multipliée par 2000. On peut également noter le temps relativement court qu'il a fallu pour créer l'index. Vous pouvez expérimenter l'utilisation de GiST au lieu de GIN dans l'exemple ci-dessus et mesurer les performances des lectures, des écritures et de la création d'index pour les deux méthodes d'accès.

Indices BRIN

BRIN (Block Range Index) est le plus récent ajout à l'ensemble de types d'index de PostgreSQL, depuis qu'il a été introduit dans PostgreSQL 9.5, n'ayant que quelques années comme fonctionnalité de base standard. BRIN fonctionne sur de très grandes tables en stockant des informations récapitulatives pour un ensemble de pages appelé "Block Range". Les index BRIN sont avec perte (comme GiST) et cela nécessite à la fois une logique supplémentaire dans l'exécuteur de requêtes de PostgreSQL, ainsi qu'un besoin de maintenance supplémentaire. Voyons BRIN en action :

testdb=# select count(*) from part where machine_id BETWEEN 5000 AND 10000;
 count
-------
  5001
(1 row)

Time: 100,376 ms
testdb=# create index part_machine_id_idx_brin ON part USING BRIN(machine_id);
CREATE INDEX
Time: 569,318 ms
testdb=# select count(*) from part where machine_id BETWEEN 5000 AND 10000;
 count
-------
  5001
(1 row)

Time: 5,461 ms

Ici, nous voyons en moyenne une accélération d'environ 18 fois grâce à l'utilisation de l'indice BRIN. Cependant, la vraie maison de BRIN est dans le domaine du Big Data, nous espérons donc tester cette technologie relativement nouvelle dans des scénarios réels à l'avenir.