Un seul index, plusieurs colonnes
Un index composite — qu'on appelle aussi index multi-colonnes — est un index unique construit à partir de deux colonnes ou plus. Pour le créer, il suffit d'énumérer les colonnes dans l'ordre voulu :
L'index idx_orders_customer_status range ses entrées triées d'abord par customer_id, puis par status à l'intérieur de chaque client. Tout part de cet ordre — le reste du fonctionnement des index composites en découle directement.
Le bon modèle mental : un annuaire trié
Imaginez un vieil annuaire papier. Les entrées sont classées par nom de famille, puis par prénom à l'intérieur de chaque nom. C'est exactement la tête qu'a un index sur (last_name, first_name).
Certaines recherches sont rapides, d'autres beaucoup moins :
- « Trouver tous les Patel » — facile, tous les Patel sont regroupés.
- « Trouver Priya Patel » — facile, on saute à Patel, puis on parcourt jusqu'à Priya.
- « Trouver tous les Priya » — lent, il faut parcourir toutes les pages. Les Priya sont dispersés sur tous les noms de famille.
Un index composite SQLite fonctionne pareil. La première colonne sert de clé de tri principale ; la deuxième colonne ne trie que les entrées qui partagent la même valeur sur la première colonne.
La règle du préfixe gauche en SQLite
SQLite ne peut exploiter un index multi-colonnes que si la clause WHERE contraint un préfixe gauche de ses colonnes. Pour un index sur (a, b, c) :
- Filtre sur
a— l'index est utilisé. - Filtre sur
aetb— l'index est utilisé. - Filtre sur
a,betc— l'index est utilisé. - Filtre sur
bseul, surcseul, ou surbetc— l'index n'est pas utilisé.
Tu peux le vérifier directement avec EXPLAIN QUERY PLAN :
Le premier plan affiche SEARCH events USING INDEX idx_events_user_kind_time. Le second retombe sur un SCAN events — filtrer uniquement sur kind saute la colonne de tête user_id, donc l'index ne sert à rien pour cette requête.
L'ordre des colonnes est un choix de conception
Puisque le préfixe gauche est déterminant, l'ordre dans lequel vous listez les colonnes dans CREATE INDEX relève d'une vraie décision, pas d'un détail cosmétique. Deux règles de base :
- Placez en premier la colonne sur laquelle vous filtrez le plus souvent. C'est elle qui ouvre l'index au plus grand nombre de requêtes.
- Mettez les colonnes d'égalité avant les colonnes de plage. SQLite peut plonger dans l'index via un
=, puis parcourir une plage contiguë avec<,>ouBETWEEN— mais seulement sur la dernière colonne utilisée.
Le plan affiche SEARCH sales USING INDEX idx_sales_region_time (region=? AND sold_at>?). SQLite saute directement à region = 'EU', puis avance dans la plage de dates. Inversez l'ordre des colonnes en (sold_at, region) et la même requête devra parcourir toutes les lignes de la plage de dates en revérifiant region à chaque fois.
Index composite ou plusieurs index mono-colonnes ?
Une question revient souvent : vaut-il mieux créer un seul index sur (a, b), ou bien deux index distincts sur a et sur b ?
Pour le filtre combiné, l'index composite gagne haut la main : SQLite saute directement aux entrées (project_id, state) qui correspondent. Avec deux index mono-colonne, SQLite va généralement en choisir un, l'utiliser pour réduire le nombre de lignes, puis revérifier l'autre colonne sur chaque ligne restante. Il arrive qu'il fasse une intersection des deux, mais quand les colonnes sont interrogées ensemble, l'index multi-colonnes reste la solution la plus propre.
Si project_id et state sont aussi interrogés séparément, mieux vaut peut-être avoir les deux : l'index composite pour le filtre combiné, et un index mono-colonne sur state pour les requêtes qui ne portent que sur cette colonne.
Les index couvrants
Quand un index contient toutes les colonnes dont une requête a besoin — aussi bien celles du filtre que celles renvoyées dans le SELECT — SQLite peut répondre sans jamais toucher à la table. C'est ce qu'on appelle un index couvrant, et il n'y a pas plus rapide.
Le plan affiche USING COVERING INDEX idx_invoices_cover. La requête récupère issued_at et total directement depuis l'index — notes et id ne sont pas nécessaires, donc la table elle-même n'est jamais ouverte. Ajouter une colonne à un index composite uniquement pour couvrir une requête critique est un compromis intéressant quand cette requête tourne en permanence.
Contraintes UNIQUE composites
Les index multi-colonnes servent aussi à imposer l'unicité sur une combinaison de colonnes. Pratique quand aucune colonne prise isolément n'est unique, mais que leur combinaison doit l'être :
Le troisième INSERT déclenche UNIQUE constraint failed: enrollments.student_id, enrollments.course_id. La paire existe déjà dans l'index, donc SQLite refuse le doublon.
Pièges à connaître
- Un
ORentre colonnes non-principales casse l'index.WHERE a = 1 OR b = 2sur un index(a, b)ne pourra généralement pas exploiter l'index — SQLite doit traiter chaque branche séparément. - Une fonction appliquée à une colonne indexée désactive l'index.
WHERE lower(email) = 'x'n'utilisera pas un index posé suremail. La parade : indexer directement l'expression, ou normaliser la donnée à l'insertion. - Un index, ça coûte. Chaque index est mis à jour à chaque
INSERT,UPDATE(sur les colonnes indexées) etDELETE. Sur une table avec beaucoup d'écritures, trois index composites peuvent vite plomber les perfs en écriture. - Lancez
ANALYZEaprès avoir créé vos index. Le planificateur de SQLite s'appuie sur les statistiques collectées parANALYZEpour départager les index candidats. Sans ces stats, il retombe sur des heuristiques qui ne sont pas toujours pertinentes.
Une démarche concrète pour optimiser une requête sqlite
Pour ajuster une requête lente, la boucle ressemble en général à ceci :
- Lancez
EXPLAIN QUERY PLANsur la requête pour voir ce que fait SQLite aujourd'hui. - S'il y a un scan, regardez la clause
WHERE: quelle est la colonne d'égalité ? Quelle est la colonne de plage ? Que sélectionne-t-on ? - Construisez un index composite avec l'égalité en premier, la plage ensuite, et ajoutez les colonnes sélectionnées si un index couvrant peut aider.
- Exécutez
ANALYZE. - Relancez
EXPLAIN QUERY PLAN. Vérifiez que le plan a changé et que l'index est bien utilisé. - Chronométrez la requête avant et après, sur des données représentatives.
Sautez l'étape 6 à vos risques et périls. Un index qui paraît correct dans le plan peut très bien être plus lent en pratique, si la table est petite ou si le planificateur choisit un autre chemin.
La suite : les index partiels
Un index composite couvre toutes les lignes de la table. Mais souvent, seule une petite portion des lignes nous intéresse vraiment — les tickets ouverts, les jobs non traités, les enregistrements non supprimés. Un index partiel permet d'indexer uniquement ces lignes-là, avec une clause WHERE intégrée à l'index lui-même. C'est ce qu'on voit sur la page suivante.
Questions fréquentes
C'est quoi un index composite en SQLite ?
Un index composite, c'est un seul index qui porte sur deux colonnes ou plus. On le crée avec CREATE INDEX idx_name ON table(col_a, col_b). SQLite range les entrées d'abord par col_a, puis par col_b à l'intérieur de chaque valeur de col_a — un peu comme un annuaire trié par nom de famille, puis par prénom.
Est-ce que l'ordre des colonnes compte vraiment dans un index composite SQLite ?
Énormément. SQLite ne peut utiliser un index composite que si la clause WHERE filtre sur un préfixe gauche des colonnes indexées. Un index sur (a, b, c) sert pour les requêtes qui filtrent sur a, sur a et b, ou sur les trois — mais il ne sert à rien si vous filtrez uniquement sur b ou sur c.
Quand vaut-il mieux un index composite plutôt que plusieurs index mono-colonne ?
Optez pour un index composite quand vos requêtes filtrent ou trient régulièrement sur la même combinaison de colonnes. Les index mono-colonne sont plus adaptés si chaque colonne est interrogée de façon indépendante. Lancez EXPLAIN QUERY PLAN pour voir quel index SQLite choisit réellement — c'est le seul retour fiable.
Qu'est-ce qu'un index couvrant en SQLite ?
Un index couvrant contient toutes les colonnes nécessaires à la requête, ce qui permet à SQLite de répondre directement depuis l'index sans toucher à la table. EXPLAIN QUERY PLAN affiche USING COVERING INDEX dans ce cas. Ajouter des colonnes supplémentaires à un index composite pour couvrir une requête critique est une optimisation courante.