đȘđ Comprendre les index postgreSQL
Hello les petits Biscuits !
Bienvenue sur la 47Úme édition de Ruby Biscuit.
Vous ĂȘtes maintenant 604 abonnĂ©s đ„ł
Bonne lecture.
Vous lisez Ruby Biscuit, la newsletter Rails de Capsens. Sâabonner gratuitement
Comment fonctionnent rĂ©ellement les index dâune base de donnĂ©es postgreSQL ? Nous nous servons parfois des index avec la vague notion quâils accĂ©lĂšrent une requĂȘte. Je vais vous expliquer comment.
SQL utilise plusieurs stratégies pour trouver un objet dans une base de données. Parlons de deux stratégies particuliÚres : la recherche séquentielle et la recherche par index.
Avant que nous ne dĂ©finissions ces stratĂ©gies, il faut comprendre que les donnĂ©es dâune table en postgreSQL sont stockĂ©es dans un fichier quâon appelle heap. Chaque objet a un rang avec les donnĂ©es correspondantes aux colonnes de la table, comme dans un fichier Excel. Les objets ne sont pas stockĂ©s dans un ordre particulier.
Recherche séquentielle
Prenons la requĂȘte :
SELECT "users".* FROM "users" WHERE (id = 1)Dans une recherche sĂ©quentielle, il faudra passer par chaque ligne du heap en demandant si lâobjet stockĂ© dans un rang de la table correspond Ă lâobjet recherchĂ©. Comme vous pouvez lâimaginer, cette stratĂ©gie ne peut ĂȘtre efficace quâavec un petit nombre dâobjets, mais sera lente quand il y en aura beaucoup. Dâailleurs, la complexitĂ© dâune recherche sĂ©quentielle croĂźt de façon linĂ©aire Ă chaque nouvel objet qui est ajoutĂ© Ă la table.
Recherche par index
La recherche par index, comme son nom lâentend, fonctionne Ă lâinstar de lâindex dâun livre, oĂč une table fait la correspondance entre un mot et des pages oĂč ce mot apparaĂźt. PostgreSQL utilise pourtant un systĂšme plus complexe que celui dâun index de livre. Alors quâun livre utilise un seul index pour chercher des mots dans un texte, postgreSQL utilise plusieurs index organisĂ©s dans une arborescence pour chercher des donnĂ©es dans le heap.
Voici le schéma de ces index :
nĆud racine => nĆuds branche => nĆuds feuille
Ce systĂšme dâorganisation sâappelle un balanced tree. Il y a dâautres types dâindex, mais nous nous concentrons sur les index de type balanced tree.
Le point dâentrĂ©e dans ces index imbriquĂ©s sâappelle le nĆud racine. Vous pouvez imaginer un livre qui, plutĂŽt que dâavoir un seul index, en aurait plusieurs, un pour les lettres de A Ă M et un autre pour les lettres de N Ă Z. Dans cette comparaison, ces deux index individuels correspondraient Ă des nĆuds branche. Le nĆud racine indiquerait sur quelles pages trouver chacun de ces deux index. Dans cet exemple, je parle de deux index seulement, mais dans une base de donnĂ©es postgreSQL, il peut, bien sĂ»r, y avoir autant de nĆuds branches que nĂ©cessaire pour structurer les donnĂ©es.
Le dernier niveau sâappelle le nĆud feuille. Cela serait lâĂ©quivalent dâune page de lâun des index, oĂč des mots sont listĂ©s avec les pages oĂč lâon peut les trouver.
Maintenant que vous comprenez lâorganisation de lâindex, considĂ©rons comment la base de donnĂ©es exĂ©cutera une requĂȘte.
Reprenons cet exemple :
SELECT "users".* FROM "users" WHERE (id = 1)La table ci-dessous reprĂ©sente le nĆud racine de lâindex des identifiants de cette table. Itemoffset identifie les entrĂ©es. Ctid (current tuple ID) indique le numĂ©ro de page du nĆud branche enfant. La colonne data reprĂ©sente la fourchette des donnĂ©es dans le nĆud branche. Les data sont dans un format hexadĂ©cimal, mais je donne des entiers ici pour plus de clartĂ©.
Dans cet exemple, le premier rang correspond Ă un nĆud branche avec des identifiants de la table users dâune valeur de moins de 100. Le deuxiĂšme rang est pour un nĆud branche avec des valeurs de 100 Ă 199, et le troisiĂšme rang est pour un nĆud branche avec des valeurs supĂ©rieures ou Ă©gales Ă 200.
Quand la base de donnĂ©es lance une recherche par index, elle trouve la ligne correspondante en commençant au milieu de la liste. Elle se demande si la valeur est dans la premiĂšre ou la deuxiĂšme moitiĂ©, choisit la bonne moitiĂ© et recommence le mĂȘme processus de recherche binaire, jusquâĂ ce quâelle trouve le nĆud branche correspondant. Le nĆud branche lui-mĂȘme ressemble structurellement au nĆud racine. Des mĂ©tadonnĂ©es font la distinction entre les branches et la racine.
Une fois que la recherche est au niveau du nĆud branche, elle fait une autre recherche binaire, jusquâĂ ce quâelle trouve le nĆud feuille, lequel contient la liste des valeurs individuelles de la colonne indexĂ©e, ainsi que le current tuple ID qui permet de trouver lâobjet recherchĂ© dans le heap. Alors que dans le nĆud racine et le nĆud branche, le ctid nâindique quâune page, dans le nĆud feuille la paire de valeurs, page et itemoffset, dĂ©signe lâemplacement exact de lâobjet dans le heap.
Quand nous ajoutons un index Ă une colonne de notre base de donnĂ©es, cette derniĂšre construit les nĆuds feuille, nĆuds branche et le nĆud racine nĂ©cessaires pour trouver un objet avec une recherche par index.
Si la complexitĂ© des recherches sĂ©quentielles croĂźt de façon linĂ©aire avec lâajout de nouveaux objets Ă une table, la complexitĂ© dâune recherche par index croĂźt de façon logarithmique. Cette diffĂ©rence est due au fait quâune recherche par index navigue entre une sĂ©rie de nĆuds dont les donnĂ©es sont ordonnĂ©es et donc faciles Ă trouver, alors quâune recherche sĂ©quentielle passe ligne par ligne sur les donnĂ©es du heap. Imaginez la diffĂ©rence de complexitĂ© entre chercher un mot dans un dictionnaire dont les entrĂ©es sont organisĂ©es alphabĂ©tiquement et chercher un mot dans un dictionnaire sans ordre aucun.
MĂȘme si lâindex peut accĂ©lĂ©rer une recherche, il faut savoir quâĂ la crĂ©ation, la mise Ă jour et la suppression dâun objet, la base de donnĂ©es doit modifier les index imbriquĂ©s, afin de maintenir les donnĂ©es.
Comment est-ce que SQL choisit la bonne méthode de recherche ?
SQL sait choisir la mĂ©thode la plus efficace pour la requĂȘte demandĂ©e. Nous pouvons mĂȘme lui demander quelle stratĂ©gie il va choisir avec EXPLAIN ou avec EXPLAIN (ANALYZE). EXPLAIN tout court explique la stratĂ©gie de requĂȘte ; EXPLAIN (ANALYZE) explique et exĂ©cute la requĂȘte.
EXPLAIN SELECT "users".* FROM "users" WHERE "users"."id" = 1La base de donnĂ©es rĂ©pondra avec soit Seq Scan, soit Index Scan, ainsi que des informations concernant le « coĂ»t » dâune requĂȘte.
Ce rĂ©sultat peut ĂȘtre paradoxal. MĂȘme une colonne indexĂ©e peut faire lâobjet dâune requĂȘte sĂ©quentielle. Cela peut arriver pour plusieurs raisons. Si, par exemple, le nombre dâobjets est petit, comme cela est souvent le cas en local, une requĂȘte sĂ©quentielle reste prĂ©fĂ©rable.
EXPLAIN SELECT "users".* FROM "users" WHERE "users"."id" = 1 En local, oĂč jâai moins de 100 utilisateurs, SQL rĂ©pond :
Seq Scan on users (cost=0.00..4.45 rows=1 width=757)mais dans la base de données de production, la réponse est :
Index Scan using users_pkey on users (cost=0.29..8.30 rows=1 width=757)Une recherche sĂ©quentielle est Ă©galement prĂ©fĂ©rable si une grande partie des objets dâune table doit ĂȘtre trouvĂ©e. Par exemple :
EXPLAIN SELECT "users".* FROM "users" WHERE "users"."id" > 1retourne
Seq Scan on users (cost=0.00..1719.10 rows=19127 width=757)Comme presque tous les utilisateurs sont demandĂ©s par cette requĂȘte, utiliser lâindex pour trouver des utilisateurs individuellement nâest pas aussi efficace quâune recherche sĂ©quentielle.
PostgreSQL choisit la stratĂ©gie de recherche la moins coĂ»teuse en analysant des donnĂ©es quâil garde en mĂ©moire, telles que le nombre de lignes et la fourchette des valeurs.
Quel est le gain de temps rĂ©el de lâajout dâun index ?
Jusquâici, nous avons regardĂ© la recherche dâun utilisateur par son identifiant, qui est une colonne indexĂ©e de facto dans notre base de donnĂ©es.
SELECT "users".* FROM "users" WHERE (id = 1)Regardons maintenant la colonne first_name, qui nâest pas indexĂ©e. Avec une base de donnĂ©es contenant 100.000 utilisateurs, combien de temps faudra-t-il pour faire la requĂȘte suivante avec une recherche sĂ©quentielle ?
EXPLAIN (ANALYZE) SELECT "users".* FROM "users" WHERE "users"."first_name" = 'John'Comme nous lâavons vu, EXPLAIN (ANALYZE) expliquera la stratĂ©gie de requĂȘte et lâexĂ©cutera. Voici le rĂ©sultat :
Seq Scan on users (cost=0.00..1925.58 rows=1 width=728) (actual time=0.036..15.707 rows=1.00 loops=1)
Filter: ((first_name)::text = 'John'::text)
Rows Removed by Filter: 100036
Buffers: shared hit=1726
Planning Time: 0.190 ms
Execution Time: 15.748 ms
(6 rows)Il faut 15,748 millisecondes pour exĂ©cuter cette requĂȘte.
Quâest-ce qui se passera si nous indexons la colonne users.first_name ?
Index Scan using index_users_on_first_name on users (cost=0.42..8.44 rows=1 width=944) (actual time=0.032..0.032 rows=1.00 loops=1)
Index Cond: ((first_name)::text = 'John'::text)
Index Searches: 1
Buffers: shared hit=4
Planning Time: 0.133 ms
Execution Time: 0.070 ms
(6 rows)Nous passons de 15,748 millisecondes Ă 0,070 millisecondes. La requĂȘte est Ă peu prĂšs 224 fois plus rapide.
Les index sur plusieurs colonnes
Il est Ă©galement possible dâajouter un index sur plusieurs colonnes. Prenons lâexemple suivant oĂč nous cherchons un utilisateur par son prĂ©nom et son nom.
SELECT "users".* FROM "users" WHERE "users"."first_name" = 'John' AND "users"."last_name" = 'Doe'PlutĂŽt que first_name_index tout court, nous pouvons crĂ©er un first_name_last_name_index. Comme vous le voyez dans le schĂ©ma ci-dessous, le nĆud racine et les nĆuds branche ordonnent les donnĂ©es selon la premiĂšre colonne de lâindex, first_name dans notre exemple. Câest dans le nĆud feuille quâapparaissent les donnĂ©es de last_name.
Lâindex first_name_last_name_index nous permettra de faire notre requĂȘte, puisque la base de donnĂ©es peut chercher par prĂ©nom dans un premier temps et ensuite par nom, une fois quâelle aura trouvĂ© le bon prĂ©nom dans le nĆud feuille. Nous pourrions mĂȘme utiliser cet index pour chercher par prĂ©nom uniquement, car la base de donnĂ©es peut se limiter Ă trouver toutes les occurrences dâun prĂ©nom dans le nĆud feuille, sans sâoccuper des noms de famille.
SELECT "users".* FROM "users" WHERE "users"."first_name" = 'John'En revanche, cet index ne permet pas de chercher par nom de famille seul : comme les entrées sont ordonnées par prénom, les noms de famille identiques sont dispersés dans toutes les feuilles.
OĂč faut-il ajouter des index ?
Maintenant que vous comprenez mieux le processus de recherche par index, vous pouvez indexer les colonnes de façon plus efficace. Outre les clefs Ă©trangĂšres que nous avons besoin de rechercher, il peut ĂȘtre utile dâindexer des colonnes consacrĂ©es aux donnĂ©es dâun service externe. Si, par exemple, vous recevez et stockez un identifiant dâune API externe, lors de la crĂ©ation dâun objet chez ce dernier, et que vous avez besoin de faire des recherches par cet identifiant, il faut prĂ©fĂ©rer un index.
Toutefois, il nâest pas idĂ©al dâajouter des index partout, puisque la construction et la maintenance de lâarborescence des index avec leurs diffĂ©rents nĆuds sont coĂ»teuses. Comme nous lâavons vu, un index peut accĂ©lĂ©rer considĂ©rablement une requĂȘte, mais cette accĂ©lĂ©ration nâest utile que si lâindex est utilisĂ©.
Il faut Ă©galement comprendre que les index peuvent ĂȘtre inutiles dans certains cas. Nous avons dĂ©jĂ vu que la requĂȘte
EXPLAIN SELECT "users".* FROM "users" WHERE "users"."id" > 1sera exĂ©cutĂ©e avec une recherche sĂ©quentielle, car elle sort une grande partie des utilisateurs. Cet exemple nâest certes pas trĂšs rĂ©aliste. Imaginons plutĂŽt que nos utilisateurs ont un Ă©tat.
EXPLAIN SELECT "users".* FROM "users" WHERE "users"."state" = 'approved' MĂȘme si nous faisons frĂ©quemment des recherches sur la colonne state, lâindexer nâest pas utile si certains Ă©tats peuvent reprĂ©senter une grande partie des utilisateurs.
Enfin, une requĂȘte qui transforme les donnĂ©es telles quâelles se trouvent dans les diffĂ©rents nĆuds dâun index sera exĂ©cutĂ©e avec une recherche sĂ©quentielle. Prenons la requĂȘte
SELECT "users".* FROM "users" WHERE (Lower(first_name) = 'john')Lower(first_name) transforme la donnĂ©e brute de first_name telle quâelle est structurĂ©e dans lâindex, ce qui rend cet index inexploitable dans ce cas.
â Andrew, dĂ©veloppeur chez Capsens



