Tout d’abord, nous positionnons le search_path pour
chercher les objets des schémas magasin et
facturation :
SET search_path = magasin,facturation;
Index « simples »
Considérons le cas d’usage d’une recherche de commandes par date. Le
besoin fonctionnel est le suivant : renvoyer l’intégralité des commandes
passées au mois de janvier 2014.
Créer la requête affichant l’intégralité des commandes passées au
mois de janvier 2014.
Pour renvoyer l’ensemble de ces produits, la requête est très simple
:
SELECT * FROM commandes date_commande
WHERE date_commande >= '2014-01-01 '
AND date_commande < '2014-02-01 ' ;
Afficher le plan de la requête , en utilisant
EXPLAIN (ANALYZE, BUFFERS). Que constate-t-on ?
Le plan de celle-ci est le suivant :
EXPLAIN (ANALYZE , BUFFERS) SELECT * FROM commandes
WHERE date_commande >= '2014-01-01 ' AND date_commande < '2014-02-01 ' ;
QUERY PLAN
-------------------------------------------------------------------------------
Seq Scan on commandes (cost=0.00..25200.00 rows=20057 width=51) (actual time=3.436..172.184 rows=19146.00 loops=1)
Filter: ((date_commande >= '2014-01-01'::date) AND (date_commande < '2014-02-01'::date))
Rows Removed by Filter: 980854
Buffers: shared hit=10200
Planning:
Buffers: shared hit=50 read=15 dirtied=2
Planning Time: 1.939 ms
Execution Time: 190.667 ms
Réécrire la requête par ordre de date croissante. Afficher de nouveau
son plan. Que constate-t-on ?
Ajoutons la clause ORDER BY :
EXPLAIN (ANALYZE , BUFFERS) SELECT * FROM commandes
WHERE date_commande >= '2014-01-01 ' AND date_commande < '2014-02-01 '
ORDER BY date_commande;
QUERY PLAN
-------------------------------------------------------------------------------
Sort (cost=26633.25..26683.40 rows=20057 width=51) (actual time=130.149..143.515 rows=19146.00 loops=1)
Sort Key: date_commande
Sort Method: quicksort Memory: 2089kB
Buffers: shared hit=10203
-> Seq Scan on commandes (cost=0.00..25200.00 rows=20057 width=51) (actual time=1.712..111.853 rows=19146.00 loops=1)
Filter: ((date_commande >= '2014-01-01'::date) AND (date_commande < '2014-02-01'::date))
Rows Removed by Filter: 980854
Buffers: shared hit=10200
Planning:
Buffers: shared hit=14 read=6
Planning Time: 0.460 ms
Execution Time: 156.629 ms
On constate ici que lors du parcours séquentiel, 980 854 lignes ont
été lues, puis écartées car ne correspondant pas au prédicat, nous
laissant ainsi avec un total de 19 146 lignes. Les valeurs précises
peuvent changer, les données étant générées aléatoirement. De plus, le
tri a été réalisé en mémoire. On constate de plus que 10 200 blocs ont
été parcourus, ici depuis le cache, mais ils auraient pu l’être depuis
le disque.
Créer un index permettant de répondre à ces requêtes.
Création de l’index :
CREATE INDEX idx_commandes_date_commande ON commandes(date_commande);
Afficher de nouveau le plan des deux requêtes. Que
constate-t-on ?
EXPLAIN (ANALYZE , BUFFERS) SELECT * FROM commandes
WHERE date_commande >= '2014-01-01 ' AND date_commande < '2014-02-01 ' ;
QUERY PLAN
-------------------------------------------------------------------------------
Index Scan using idx_commandes_date_commande on commandes (cost=0.42..687.10 rows=20057 width=51) (actual time=0.086..14.712 rows=19146.00 loops=1)
Index Cond: ((date_commande >= '2014-01-01'::date) AND (date_commande < '2014-02-01'::date))
Index Searches: 1
Buffers: shared hit=221
Planning:
Buffers: shared hit=5
Planning Time: 0.393 ms
Execution Time: 26.332 ms
Le temps d’exécution a été réduit considérablement : la requête est 7
fois plus rapide. On constate notamment que seuls 221 blocs ont été
parcourus.
Pour la requête avec la clause ORDER BY, nous obtenons
le plan d’exécution suivant :
QUERY PLAN
-------------------------------------------------------------------------------
Index Scan using idx_commandes_date_commande on commandes (cost=0.42..687.10 rows=20057 width=51) (actual time=0.031..12.837 rows=19146.00 loops=1)
Index Cond: ((date_commande >= '2014-01-01'::date) AND (date_commande < '2014-02-01'::date))
Index Searches: 1
Buffers: shared hit=221
Planning Time: 0.125 ms
Execution Time: 23.028 ms
Celui-ci est identique ! En effet, l’index permettant un parcours
trié, l’opération de tri est ici « gratuite ».
Écrire la requête affichant commandes.numero_commande et
clients.type_client pour client_id = 3.
Afficher son plan. Que constate-t-on ?
EXPLAIN (ANALYZE , BUFFERS) SELECT numero_commande, type_client FROM commandes
INNER JOIN clients ON commandes.client_id = clients.client_id
WHERE clients.client_id = 3 ;
QUERY PLAN
-------------------------------------------------------------------------------
Nested Loop (cost=0.29..22708.42 rows=11 width=10) (actual time=1.491..78.551 rows=10.00 loops=1)
Buffers: shared hit=10206
-> Index Scan using clients_pkey on clients (cost=0.29..8.31 rows=1 width=10) (actual time=0.028..0.038 rows=1.00 loops=1)
Index Cond: (client_id = 3)
Index Searches: 1
Buffers: shared hit=6
-> Seq Scan on commandes (cost=0.00..22700.00 rows=11 width=16) (actual time=1.456..78.465 rows=10.00 loops=1)
Filter: (client_id = 3)
Rows Removed by Filter: 999990
Buffers: shared hit=10200
Planning:
Buffers: shared hit=37 read=1
Planning Time: 0.280 ms
Execution Time: 78.620 ms
Créer un index pour accélérer cette requête.
CREATE INDEX ON commandes (client_id) ;
Afficher de nouveau son plan. Que constate-t-on ?
EXPLAIN (ANALYZE , BUFFERS) SELECT * FROM commandes
INNER JOIN clients on commandes.client_id = clients.client_id
WHERE clients.client_id = 3 ;
QUERY PLAN
-------------------------------------------------------------------------------
Nested Loop (cost=4.80..55.98 rows=11 width=102) (actual time=0.085..0.144 rows=10.00 loops=1)
Buffers: shared hit=16
-> Index Scan using clients_pkey on clients (cost=0.29..8.31 rows=1 width=51) (actual time=0.018..0.021 rows=1.00 loops=1)
Index Cond: (client_id = 3)
Index Searches: 1
Buffers: shared hit=3
-> Bitmap Heap Scan on commandes (cost=4.51..47.56 rows=11 width=51) (actual time=0.056..0.095 rows=10.00 loops=1)
Recheck Cond: (client_id = 3)
Heap Blocks: exact=10
Buffers: shared hit=13
-> Bitmap Index Scan on commandes_client_id_idx (cost=0.00..4.51 rows=11 width=0) (actual time=0.034..0.035 rows=10.00 loops=1)
Index Cond: (client_id = 3)
Index Searches: 1
Buffers: shared hit=3
Planning:
Buffers: shared hit=41 read=3
Planning Time: 0.731 ms
Execution Time: 0.194 ms
On constate ici un temps d’exécution divisé par 400 : en effet, on ne
lit plus que 13 blocs pour la commande (3 pour l’index, 10 pour les
données) au lieu de 10 200.
Sélectivité
Écrire une requête renvoyant l’intégralité des clients qui sont du
type entreprise (‘E’), une autre pour l’intégralité des clients qui sont
du type particulier (‘P’).
Les requêtes :
SELECT * FROM clients WHERE type_client = 'P ' ;
SELECT * FROM clients WHERE type_client = 'E ' ;
Ajouter un index sur la colonne type_client, et rejouer
les requêtes précédentes.
Pour créer l’index :
CREATE INDEX ON clients (type_client);
Afficher leurs plans d’exécution. Que se passe-t-il ? Pourquoi ?
Les plans d’éxécution :
EXPLAIN ANALYZE SELECT * FROM clients WHERE type_client = 'P ' ;
QUERY PLAN
-------------------------------------------------------------------------------
Seq Scan on clients (cost=0.00..2298.00 rows=89700 width=51) (actual time=0.021..79.523 rows=89811.00 loops=1)
Filter: (type_client = 'P'::bpchar)
Rows Removed by Filter: 10189
Buffers: shared hit=1048
Planning:
Buffers: shared hit=63 read=21 dirtied=1
Planning Time: 1.177 ms
Execution Time: 142.988 ms
EXPLAIN ANALYZE SELECT * FROM clients WHERE type_client = 'E ' ;
QUERY PLAN
-------------------------------------------------------------------------------
Bitmap Heap Scan on clients (cost=95.97..1246.69 rows=8217 width=51) (actual time=1.514..11.304 rows=8058.00 loops=1)
Recheck Cond: (type_client = 'E'::bpchar)
Heap Blocks: exact=1025
Buffers: shared hit=1025 read=9
-> Bitmap Index Scan on clients_type_client_idx (cost=0.00..93.92 rows=8217 width=0) (actual time=1.088..1.090 rows=8058.00 loops=1)
Index Cond: (type_client = 'E'::bpchar)
Index Searches: 1
Buffers: shared read=9
Planning Time: 0.215 ms
Execution Time: 17.487 ms
L’optimiseur sait estimer, à partir des statistiques (consultables
via la vue pg_stats), qu’il y a approximativement 89 000
clients particuliers, contre 8 000 clients entreprise.
Dans le premier cas, la majorité de la table sera parcourue, et
renvoyée : il n’y a aucun intérêt à utiliser l’index.
Dans l’autre, le nombre de lignes étant plus faible, l’index est bel
et bien utilisé (via un Bitmap Scan , ici).
Index partiels
Sur la base fournie pour les TPs, les lots non livrés sont
constamment requêtés. Notamment, un système d’alerte est mis en place
afin d’assurer un suivi qualité sur les lots expédié depuis plus de 3
jours (selon la date d’expédition), mais non réceptionné (date de
réception à NULL).
Écrire la requête correspondant à ce besoin fonctionnel (il est
normal qu’elle ne retourne rien).
La requête est la suivante :
SELECT * FROM lots
WHERE date_reception IS NULL
AND date_expedition < now () - '3d ' :: interval ;
Afficher le plan d’exécution.
Le plans (ci-dessous avec ANALYZE) opère un Seq
Scan parallélisé, lit et rejette toutes les lignes, ce qui est
évidemment lourd :
QUERY PLAN
-------------------------------------------------------------------------------
Gather (cost=1000.00..17764.65 rows=1 width=43) (actual time=73.024..81.743 rows=0.00 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared read=9424
-> Parallel Seq Scan on lots (cost=0.00..16764.55 rows=1 width=43) (actual time=66.811..66.815 rows=0.00 loops=3)
Filter: ((date_reception IS NULL) AND (date_expedition < (now() - '3 days'::interval)))
Rows Removed by Filter: 335568
Buffers: shared read=9424
Planning:
Buffers: shared hit=99 read=6
Planning Time: 0.858 ms
Execution Time: 81.832 ms
Quel index partiel peut-on créer pour optimiser ?
On peut optimiser ces requêtes sur les critères de recherche à l’aide
des index partiels suivants :
CREATE INDEX ON lots (date_expedition) WHERE date_reception IS NULL ;
Afficher le nouveau plan d’exécution et vérifier l’utilisation du
nouvel index.
EXPLAIN (ANALYZE )
SELECT * FROM lots
WHERE date_reception IS NULL
AND date_expedition < now () - '3d ' :: interval ;
QUERY PLAN
-------------------------------------------------------------------------------
Index Scan using lots_date_expedition_idx on lots (cost=0.13..4.15 rows=1 width=43) (actual time=0.009..0.011 rows=0.00 loops=1)
Index Cond: (date_expedition < (now() - '3 days'::interval))
Index Searches: 1
Buffers: shared hit=1
Planning:
Buffers: shared hit=21 read=1
Planning Time: 0.499 ms
Execution Time: 0.039 ms
Il est intéressant de noter que seul le test sur la condition indexée
(date_expedition) est présent dans le plan : la condition
date_reception IS NULL est implicitement validée par
l’index partiel.
Attention, il peut être tentant d’utiliser une formulation de la
sorte pour ces requêtes :
SELECT * FROM lots
WHERE date_reception IS NULL
AND now () - date_expedition > '3d ' :: interval ;
D’un point de vue logique, c’est la même chose, mais l’optimiseur
n’est pas capable de réécrire cette requête correctement. Ici, le nouvel
index sera tout de même utilisé, le volume de lignes satisfaisant au
critère étant très faible, mais il ne sera pas utilisé pour filtrer sur
la date :
EXPLAIN (ANALYZE ) SELECT * FROM lots
WHERE date_reception IS NULL
AND now () - date_expedition > '3d ' :: interval ;
QUERY PLAN
-------------------------------------------------------------------------------
Index Scan using lots_date_expedition_idx on lots (cost=0.12..4.15 rows=1 width=43) (actual time=0.004..0.005 rows=0.00 loops=1)
Filter: ((now() - (date_expedition)::timestamp with time zone) > '3 days'::interval)
Index Searches: 1
Buffers: shared hit=1
Planning:
Buffers: shared hit=1
Planning Time: 0.133 ms
Execution Time: 0.057 ms
La ligne importante et différente ici concerne le Filter
en lieu et place du Index Cond du plan précédent. Ici tout
l’index partiel (certes tout petit) est lu intégralement et les lignes
testées une à une.
C’est une autre illustration des points vus précédemment sur les
index non utilisés.
Index fonctionnel
Ce TP utilise la base magasin .
Écrire une requête permettant de renvoyer l’ensemble des produits
(table magasin.produits) dont le volume ne dépasse pas 1
litre (les unités de longueur sont en mm, 1 litre = 1 000 000 mm³).
Concernant le volume des produits, la requête est assez simple :
SELECT * FROM produits WHERE longueur * hauteur * largeur < 1000000 ;
Quel index permet d’optimiser cette requête ? (Utiliser une fonction
est possible, mais pas obligatoire.)
L’option la plus simple est de créer l’index de cette façon, sans
avoir besoin d’une fonction :
CREATE INDEX ON produits((longueur * hauteur * largeur));
En général, il est plus propre de créer une fonction. On peut passer
la ligne entière en paramètre pour éviter de fournir 3 paramètres. Il
faut que cette fonction soit IMMUTABLE pour être
indexable :
CREATE OR REPLACE function volume (p produits)
RETURNS numeric
AS $$
SELECT p .longueur * p .hauteur * p .largeur;
$$ language SQL
PARALLEL SAFE
IMMUTABLE ;
(Elle est même PARALLEL SAFE pour la même raison qu’elle
est IMMUTABLE : elle dépend uniquement des données de la
table.)
On peut ensuite indexer le résultat de cette fonction :
CREATE INDEX ON produits (volume(produits)) ;
Il est ensuite possible d’écrire la requête de plusieurs manières, la
fonction étant ici écrite en SQL et non en PL/pgSQL ou autre langage
procédural :
SELECT * FROM produits WHERE longueur * hauteur * largeur < 1000000 ;
SELECT * FROM produits WHERE volume(produits) < 1000000 ;
En effet, l’optimiseur est capable de « regarder » à l’intérieur de
la fonction SQL pour déterminer que les clauses sont les mêmes, ce qui
n’est pas vrai pour les autres langages.
En revanche, la requête suivante, où la multiplication est faite dans
un ordre différent, n’utilise pas l’index :
SELECT * FROM produits WHERE largeur * longueur * hauteur < 1000000 ;
et c’est notamment pour cette raison qu’il est plus propre d’utiliser
la fonction.
De part l’origine « relationnel-objet » de PostgreSQL, on peut même
écrire la requête de la manière suivante :
SELECT * FROM produits WHERE produits.volume < 1000000 ;
Cas d’index non utilisés
Afficher le plan de la requête.
SELECT * FROM lignes_commandes WHERE numero_lot_expedition = '190774 ' :: numeric ;
EXPLAIN (ANALYZE,BUFFERS) SELECT * FROM lignes_commandes
WHERE numero_lot_expedition = '190774'::numeric;
QUERY PLAN
-------------------------------------------------------------------------------
Seq Scan on lignes_commandes (cost=0.00..89353.51 rows=15710 width=74) (actual time=0.608..950.713 rows=6.00 loops=1)
Filter: ((numero_lot_expedition)::numeric = '190774'::numeric)
Rows Removed by Filter: 3141961
Buffers: shared read=42224
Planning:
Buffers: shared hit=115 read=6 dirtied=1
Planning Time: 0.847 ms
Execution Time: 950.826 ms
Le moteur fait un parcours séquentiel et retire la plupart des
enregistrements pour n’en conserver que 6.
Créer un index pour améliorer son exécution.
CREATE INDEX ON lignes_commandes (numero_lot_expedition);
L’index est-il utilisé ? Quel est le problème ?
L’index n’est pas utilisé à cause de la conversion
bigint vers numeric. Il est important
d’utiliser les bons types :
EXPLAIN (ANALYZE ,BUFFERS)
SELECT * FROM lignes_commandes
WHERE numero_lot_expedition = '190774 ' ;
QUERY PLAN
-------------------------------------------------------------------------------
Index Scan using lignes_commandes_numero_lot_expedition_idx on lignes_commandes (cost=0.43..8.54 rows=6 width=74) (actual time=0.106..0.131 rows=6.00 loops=1)
Index Cond: (numero_lot_expedition = '190774'::bigint)
Index Searches: 1
Buffers: shared read=5
Planning:
Buffers: shared hit=22 read=4
Planning Time: 0.400 ms
Execution Time: 0.164 ms
Sans conversion la requête est bien plus rapide. Faites également le
test sans index, le Seq Scan sera également plus rapide, le
moteur n’ayant pas à convertir toutes les lignes parcourues.
Écrire une requête pour obtenir les commandes dont la quantité est
comprise entre 1 et 8 produits.
EXPLAIN (ANALYZE ,BUFFERS) SELECT * FROM lignes_commandes
WHERE quantite BETWEEN 1 AND 8 ;
QUERY PLAN
-------------------------------------------------------------------------------
Seq Scan on lignes_commandes (cost=0.00..89353.51 rows=2508128 width=74) (actual time=0.058..2275.371 rows=2512740.00 loops=1)
Filter: ((quantite >= 1) AND (quantite <= 8))
Rows Removed by Filter: 629227
Buffers: shared hit=284 read=41940
Planning Time: 0.087 ms
Execution Time: 3996.967 ms
Créer un index pour améliorer l’exécution de cette requête.
CREATE INDEX ON lignes_commandes(quantite);
Pourquoi celui-ci n’est-il pas utilisé ? (Conseil : regarder la vue
pg_stats)
La table pg_stats nous donne des informations de
statistiques. Par exemple, pour la répartition des valeurs pour la
colonne quantite:
SELECT * FROM pg_stats
WHERE tablename= 'lignes_commandes ' AND attname= 'quantite '
\gx
…
n_distinct | 10
most_common_vals | {2,3,0,5,4,9,7,8,1,6}
most_common_freqs | {0.103133,0.101833,0.101533,0.1013,0.100767,0.1002,
0.100033,0.0991,0.0972333,0.0948667}
…
Ces quelques lignes nous indiquent qu’il y a 10 valeurs distinctes et
qu’il y a environ 10 % d’enregistrements correspondant à chaque
valeur.
Avec le prédicat quantite BETWEEN 1 and 8, le moteur
estime récupérer environ 80 % de la table. Il est donc bien plus coûteux
de lire l’index et la table pour récupérer 80 % de la table. C’est
pourquoi le moteur fait un Seq Scan qui moins coûteux.
Faire le test avec les commandes dont la quantité est comprise entre
1 et 4 produits.
EXPLAIN (ANALYZE ,BUFFERS) SELECT * FROM lignes_commandes
WHERE quantite BETWEEN 1 AND 4 ;
QUERY PLAN
-------------------------------------------------------------------------------
Bitmap Heap Scan on lignes_commandes (cost=17270.04..78485.66 rows=1266108 width=74) (actual time=80.521..1262.322 rows=1254886.00 loops=1)
Recheck Cond: ((quantite >= 1) AND (quantite <= 4))
Heap Blocks: exact=42202
Buffers: shared hit=2 read=43259 written=7
-> Bitmap Index Scan on lignes_commandes_quantite_idx (cost=0.00..16953.51 rows=1266108 width=0) (actual time=68.698..68.700 rows=1254886.00 loops=1)
Index Cond: ((quantite >= 1) AND (quantite <= 4))
Index Searches: 1
Buffers: shared read=1059
Planning:
Buffers: shared hit=24 read=1
Planning Time: 0.372 ms
Execution Time: 2122.254 ms
Cette fois, la sélectivité est différente et le nombre
d’enregistrements moins élevé. Le moteur passe donc par un parcours
d’index.
Cet exemple montre qu’on indexe selon une requête et non selon une
table.