Un index partiel est un index construit sur un sous-ensemble d'une table ; le sous-ensemble est défini par une expression conditionnelle (appelée prédicat de l'index partiel). L'index ne contient des entrées que pour les lignes de la table qui satisfont au prédicat. Les index partiels sont une fonctionnalité spécialisée, mais ils trouvent leur utilité dans de nombreuses situations.
Une raison majeure à l'utilisation d'index partiels est d'éviter d'indexer les valeurs courantes. Puisqu'une requête qui recherche une valeur courante (qui correspond à plus de quelques pour cents de toutes les lignes) n'utilise, de toute façon, pas cet index, il ne sert à rien de garder ces lignes dans l'index. Cela réduit la taille de l'index, ce qui accélèrera les requêtes qui l'utilisent. Cela accélère aussi nombre d'opérations de mise à jour de la table, car l'index n'a pas à être mis à jour à chaque fois. L'Exemple 11.1 montre une application possible de cette idée.
Exemple 11.1. Mettre en place un index partiel pour exclure des valeurs courantes
Soit l'enregistrement d'un journal d'accès à un serveur web dans une base de données. La plupart des accès proviennent de classes d'adresses IP internes à l'organisation, mais certaines proviennent de l'extérieur (des employés connectés par modem, par exemple). Si les recherches par adresses IP concernent essentiellement les accès extérieurs, il est inutile d'indexer les classes d'adresses IP qui correspondent au sous-réseau de l'organisation.
Si la table ressemble à :
CREATE TABLE access_log ( url varchar, client_ip inet, ... );
Pour créer un index partiel qui corresponde à l'exemple, il faut utiliser une commande comme celle-ci :
CREATE INDEX access_log_client_ip_ix ON access_log (client_ip) WHERE NOT (client_ip > inet '192.168.100.0' AND client_ip < inet '192.168.100.255');
Une requête typique qui peut utiliser cet index est :
SELECT * FROM access_log WHERE url = '/index.html' AND client_ip = inet '212.78.10.32';
Ici l'adresse IP de la requête est couverte par un index partiel. La requête suivante ne peut pas utiliser l'index partiel, puisqu'elle utilise une adresse IP qui est exclue de l'index :
SELECT * FROM access_log WHERE url = '/index.html' AND client_ip = inet '192.168.100.23';
Ce type d'index partiel nécessite que les valeurs courantes soient prédéterminées, de façon à ce que ce type d'index soit mieux utilisé avec une distribution des données qui ne change pas. Les index peuvent être recréés occasionnellement pour s'adapter aux nouvelles distributions de données, mais cela ajoute de la maintenance.
Une autre utilisation possible d'index partiel revient à exclure des valeurs de l'index qui ne correspondent pas aux requêtes courantes ; ceci est montré dans l'Exemple 11.2. Cette méthode donne les mêmes avantages que la précédente, mais empêche l'accès par l'index aux valeurs « sans intérêt ». Évidemment, mettre en place des index partiels pour ce genre de scénarios nécessite beaucoup de soin et d'expérimentation.
Exemple 11.2. Mettre en place un index partiel pour exclure les valeurs inintéressantes
Soit une table qui contient des commandes facturées et des commandes non facturées, avec les commandes non facturées qui ne prennent qu'une petite fraction de l'espace dans la table, et qui sont les plus accédées. Il est possible d'améliorer les performances en créant un index limité aux lignes non facturées. La commande pour créer l'index ressemble à :
CREATE INDEX index_commandes_nonfacturees ON commandes (no_commande) WHERE facturee is not true;
La requête suivante utilise cet index :
SELECT * FROM commandes WHERE facturee is not true AND no_commande < 10000;
Néanmoins, l'index peut aussi être utilisé dans des requêtes qui
n'utilisent pas no_commande
, comme :
SELECT * FROM commandes WHERE facturee is not true AND montant > 5000.00;
Ceci n'est pas aussi efficace qu'un index partiel sur la colonne
montant
, car le système doit lire l'index en entier.
Néanmoins, s'il y a assez peu de commandes non facturées, l'utilisation
de cet index partiel pour trouver les commandes non facturées peut être
plus efficace.
La requête suivante ne peut pas utiliser cet index :
SELECT * FROM commandes WHERE no_commande = 3501;
La commande 3501 peut faire partie des commandes facturées ou non facturées.
L'Exemple 11.2 illustre aussi le fait que la colonne
indexée et la colonne utilisée dans le prédicat ne sont pas nécessairement
les mêmes. PostgreSQL supporte tous les
prédicats sur les index partiels, tant que ceux-ci ne portent que sur des
champs de la table indexée. Néanmoins, il faut se rappeler que le prédicat doit
correspondre aux conditions utilisées dans les requêtes qui sont supposées
profiter de l'index. Pour être précis, un index partiel ne peut être utilisé
pour une requête que si le système peut reconnaître que la clause
WHERE
de la requête implique mathématiquement le prédicat de
l'index.
PostgreSQL n'a pas de méthode sophistiquée de
démonstration de théorème pour reconnaître que des expressions apparemment
différentes sont mathématiquement équivalentes. (Non seulement une telle
méthode générale de démonstration serait extrêmement complexe à créer,
mais, en plus, elle serait probablement trop lente pour être d'une quelconque
utilité).
Le système peut reconnaître des implications d'inégalités simples, par
exemple « x < 1 » implique « x < 2 » ;
dans les autres cas,
la condition du prédicat doit correspondre exactement à une partie de la
clause WHERE
de la requête, sans quoi l'index ne peut pas
être considéré comme utilisable. La correspondance prend place lors de l'exécution
de la planification de la requête, pas lors de l'exécution. À ce titre, les
clauses de requêtes à paramètres ne fonctionnent pas avec un index partiel.
Par exemple, une requête préparée avec un paramètre peut indiquer
« x < ? » qui n'implique jamais
« x < 2 » pour toutes les valeurs possibles du paramètre.
Un troisième usage possible des index partiels ne nécessite pas que l'index soit utilisé dans des requêtes. L'idée ici est de créer un index d'unicité sur un sous-ensemble de la table, comme dans l'Exemple 11.3. Cela permet de mettre en place une unicité parmi le sous-ensemble des lignes de la table qui satisfont au prédicat, sans contraindre les lignes qui n'y satisfont pas.
Exemple 11.3. Mettre en place un index d'unicité partielle
Soit une table qui décrit des résultats de tests. On souhaite s'assurer qu'il n'y a qu'une seule entrée « succès » (succes) pour chaque combinaison de sujet et de résultat, alors qu'il peut y avoir un nombre quelconque d'entrées « echec ». Une façon de procéder :
CREATE TABLE tests ( sujet text, resultat text, succes boolean, ... ); CREATE UNIQUE INDEX contrainte_tests_reussis ON tests (sujet, resultat) WHERE succes;
C'est une méthode très efficace quand il y a peu de tests
réussis et beaucoup de tests en échec. Il est aussi possible de permettre
un seul NULL dans une colonne en créant un index partiel unique avec une
restriction IS NULL
.
Enfin, un index partiel peut aussi être utilisé pour surcharger les choix de plan d'exécution de requêtes du système. De plus, des jeux de données à distribution particulière peuvent inciter le système à utiliser un index alors qu'il ne le devrait pas. Dans ce cas, on peut mettre en place l'index de telle façon qu'il ne soit pas utilisé pour la requête qui pose problème. Normalement, PostgreSQL fait des choix d'usage d'index raisonnables. Par exemple, il les évite pour rechercher les valeurs communes, si bien que l'exemple précédent n'économise que la taille de l'index, il n'est pas nécessaire pour éviter l'utilisation de l'index. En fait, les choix de plan d'exécution incorrects doivent être traités comme des bogues, et être transmis à l'équipe de développement.
Mettre en place un index partiel indique une connaissance au moins aussi étendue que celle de l'analyseur de requêtes, en particulier, savoir quand un index peut être profitable. Une telle connaissance nécessite de l'expérience et une bonne compréhension du fonctionnement des index de PostgreSQL. Dans la plupart des cas, les index partiels ne représentent pas un gros gain par rapport aux index classiques. Dans certains cas, ils sont même contre-productifs, comme dans Exemple 11.4.
Exemple 11.4. Ne pas utiliser les index partiels comme substitut au partitionnement
Vous pourriez être tenté de créer un gros ensemble d'index partiels qui ne se recouvrent pas, par exemple :
CREATE INDEX mytable_cat_1 ON mytable (data) WHERE category = 1; CREATE INDEX mytable_cat_2 ON mytable (data) WHERE category = 2; CREATE INDEX mytable_cat_3 ON mytable (data) WHERE category = 3; ... CREATE INDEX mytable_cat_N
ON mytable (data) WHERE category =N
;
C'est une mauvaise idée ! Pratiquement à coup sûr, vous seriez mieux avec un seul index complet, déclaré ainsi :
CREATE INDEX mytable_cat_data ON mytable (category, data);
(Placez la colonne category en premier, pour les raisons décrites dans Section 11.3.) Bien qu'une recherche dans cet index plus gros pourrait avoir à descendre quelques niveaux de plus dans l'arbre que ce que ferait une recherche dans un index plus petit, cela sera certainement moins cher que ce que va coûter l'effort du planificateur pour sélectionner le bon index parmi tous les index. Le cœur du problème est que le système ne comprend pas la relation entre les index partiels et va laborieusement tester chaque index pour vois s'il est applicable à la requête courante.
Si votre table est suffisamment volumineuse pour qu'un index seul soit réellement une mauvaise idée, vous devriez plutôt regarder du côté du partitionnement (voir Section 5.11). Avec ce mécanisme, le système comprend que les tables et les index ne se croisent pas, et donc de meilleures performances sont possibles.
Plus d'informations sur les index partiels sont disponibles dans [ston89b], [olson93] et [seshadri95].