11.7. Index partiels

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 le 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.

L'usage principal des index partiels est d'éviter d'indexer les valeurs trop courantes. Comme une requête qui fait des recherches sur une valeur trop courante (qui correspond à plus de quelques pour-cent des lignes) n'utilisera pas cet index de toute façon, il ne sert à rien de garder ces lignes dans l'index. Cela réduit la taille de l'index, ce qui accélère les requêtes qui l'utilisent. Cela accélère aussi beaucoup d'opérations de mise à jour de la table car l'index n'a pas besoin d'être mis à jour à chaque fois. Exemple 11-1 montre une application possible de cette idée.

Exemple 11-1. Mettre en place un index partiel pour exclure les valeurs courantes

Supposons que vous enregistrez 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 à votre organisation, mais certaines viennent d'ailleurs (disons des employés connectés par modem). Si vos recherches sur des adresses IP concernent essentiellement les accès extérieures, vous n'avez probablement pas besoin d'indexer les classes d'adresses IP qui correspondent au sous-réseau de votre organisation.

Supposons que la table soit comme ceci:

CREATE TABLE access_log (
    url varchar,
    client_ip inet,
    ...
);

Pour créer un index partiel qui corresponde à notre 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';

Une requête qui ne peut pas l'utiliser est:

SELECT * FROM access_log WHERE client_ip = inet '192.168.100.23';

Observez que cet type d'index partiel nécessite que les valeurs courantes soient prédéterminées. Si la distribution des valeurs est inhérente (du fait de la nature de l'application) et statique (ne changeant pas dans le temps), ce n'est pas trop difficile, mais si les valeurs courantes sont simplement dues au hasard, cela peut demander beaucoup de travail de maintenance.

Il est aussi possible d'exclure des valeurs de l'index qui ne correspondent pas aux requêtes courantes; ceci est montré dans Exemple 11-2. Cette méthode donne les mêmes avantages que la précédente, mais empêche les valeurs << sans intérêt >> d'être accédées. Évidemment, mettre en place des index partiels pour ce genre de scénario nécessite beaucoup de soin et d'expérimentation.

Exemple 11-2. Mettre en place un index partiel pour exclure les valeurs inintéressantes

Si vous avez une table qui contient des commandes facturées et des commandes non facturées, que les commandes non facturées ne prennent qu'une petite fraction de l'espace dans la table, et que ces commandes non facturées sont les plus accédées, alors vous pouvez améliorer les performances en créant un index limité aux lignes non facturées. La commande pour créer l'index ressemblerait à ceci:

CREATE INDEX orders_unbilled_index ON orders (order_nr)
    WHERE billed is not true;

La requête suivante utilisera probablement cet index:

SELECT * FROM orders WHERE billed is not true AND order_nr < 10000;

Néanmoins, l'index peut aussi être utilisé dans des requêtes qui n'utilisent pas order_nr, comme:

SELECT * FROM orders WHERE billed is not true AND amount > 5000.00;

Ceci n'est pas aussi efficace qu'un index partiel sur la colonne amount, 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 efficace.

Notez que cette requête ne peut pas utiliser cet index:

SELECT * FROM orders WHERE order_nr = 3501;

La commande 3501 peut faire partie des commandes facturées ou bien des commandes non facturées.

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 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 >>; sinon, la condition du prédicat doit correspondre exactement à une partie de la clause WHERE de la requête, sans quoi l'index ne sera pas considéré comme utilisable.

Le 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 unique sur un sous-ensemble de la table, comme dans Exemple 11-3. Ceci 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 unique partiel

Supposons que nous ayons une table qui décrive des résultats de tests. Nous voulons nous assurer qu'il n'y a qu'une seule entrée << succès >> (success) pour chaque combinaison de sujet (subject) et de résultat (target), mais il peut y avoir un nombre quelconque d'entrées << echec >>. Voici une façon de le faire.

CREATE TABLE tests (
    subject text,
    target text,
    success boolean,
    ...
);

CREATE UNIQUE INDEX tests_success_constraint ON tests (subject, target)
    WHERE success;

C'est une méthode très efficace pour le faire quand il y a peu de tests réussis et beaucoup de tests en échec.

Enfin, un index partiel peut aussi être utilisé pour passer outre aux choix de plan d'exécution de requête du système. Il peut arriver avec certains jeux de données particuliers que le système utilise un index alors qu'il ne devrait vraiment pas le faire. Dans ce cas, on peut mettre en place l'index de telle faon 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 en fait pas nécessaire pour éviter l'usage de l'index. En fait, les choix de plan d'exécution grossièrement incorrects doivent être traités comme des bogues, et être transmis à l'équipe de développement.

Gardez à l'esprit que mettre en place un index partiel indique que vous connaissez vos données au moins aussi bien que l'analyseur de requêtes, et en particulier que vous savez 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.

Vous trouverez plus d'informations sur les index partiels en lisant The case for partial indexes, Partial indexing in POSTGRES: research project, et Generalized Partial Indexes.