Filtrage avant application d’une fonction d’agrégat

(sujet préalablement traité avec la version 9.6)

      Dans cet article général, je présente le principe selon lequel le moteur des SGBD travaille requête par requête. Certes, les exécutions successives travaillant sur les mêmes données bénéficient de la mise en cache. Mais il est malgré tout préférable de ne faire qu’un seul passage sur les données.
      Ici, je reprends le contexte du clan des géants. Différentes statistiques sont régulièrement demandées au scribe concernant la population jeune :


      Pour cela, le scribe Margiono exécute 6 requêtes. Démonstration avec PostgreSQL 11 :

select version(); version ---------------------------------------------------------------------------------------------------------------------------------- PostgreSQL 11.2 (Ubuntu 11.2-2.pgdg18.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0, 64-bit create table geants( idu uuid, dtn date, genre char(1), taille smallint, masse smallint, actif boolean, devise varchar(128), pw smallint, heureux boolean, couleur varchar(8), veteran boolean, clan smallint, gabarit varchar(8), revenu integer, pm smallint, berserk boolean, tutelaire smallint, ere varchar(10), cyclope boolean); CREATE TABLE CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; psql: NOTICE: l'extension « uuid-ossp » existe déjà, poursuite du traitement CREATE EXTENSION WITH serie(i) AS (SELECT generate_series(5500000,100001,-1)) insert into geants select uuid_generate_v4(), current_date - (ceil(i/3) || ' minutes')::interval + (trunc(random() * 100 + 1) || ' days')::interval, case when random() < 0.45 then 'M' else 'F' end, 200 + (trunc(random() * 200 + 1)), 300 + (trunc(random() * 200 + 1)), case when random() < 0.7 then false else true end, upper(md5(random()::text)), (trunc(random()*100 + 1)), case when random() < 0.1 then false else true end, case when random() < 0.7 then 'GRIS' when random() < 0.8 then 'NOIR' else 'BLEU' end, case when random() < 0.9 then false else true end, (trunc(random()*1000 + 1)), case when random() < 0.1 then 'PETIT' when random() < 0.9 then 'MOYEN' else 'GRAND' end, (trunc(random()*1000000 + 1)), (trunc(random()*10 + 1)), case when random() < 0.01 then true else false end, (trunc(random()*10 + 1)), case when i < 18000000 then 'TAUREAU' when i < 40000000 then 'LICORNE' else 'DRAGON' end, case when random() < 0.001 then true else false end from serie; INSERT 0 5400000 select avg(taille) from geants where dtn >= (current_date - interval '500 days') and genre = 'M'; avg ---------------------- 300.5961816218027194 (1 ligne) Temps : 367,581 ms select avg(taille) from geants where dtn >= (current_date - interval '500 days') and genre = 'F'; avg ---------------------- 300.5453445920576464 (1 ligne) Temps : 405,593 ms select avg(masse) from geants where dtn >= (current_date - interval '500 days') and actif; avg ---------------------- 400.5705109297197845 (1 ligne) Temps : 311,944 ms select avg(masse) from geants where dtn >= (current_date - interval '500 days') and not actif; avg ---------------------- 400.4414018601866963 (1 ligne) Temps : 438,126 ms select count(*) from geants where dtn >= (current_date - interval '500 days') and revenu > 900000; count -------- 227384 (1 ligne) Temps : 298,046 ms select count(*) from geants where dtn >= (current_date - interval '500 days') and berserk and veteran; count ------- 2235 (1 ligne) Temps : 260,555 ms

      Temps cumulé : un peu plus de 2s. Ce n’est pas gigantesque mais il est à prévoir que le Grand Chef demande de plus en plus d’informations. Margiono aimerait que le temps de traitement n’explose pas. Le Grand Chef a une lourde massue et il n’est pas spécialement patient.
      Margiono décide de ne plus utiliser qu’une requête. Il pourrait le faire avec CASE mais décide d’utiliser FILTER. Cette clause a été introduite dans la norme SQL:2003. PostgreSQL la propose depuis la version 9.4. Elle permet de filtrer avant d’appliquer une fonction d’agrégat. Il va donc appliquer 6 filtres différents correspondant aux 6 cas demandés :

explain select avg(taille) filter (where genre = 'M') taille_moyenne_geants, avg(taille) filter (where genre = 'F') taille_moyenne_geantes, avg(masse) filter (where actif) masse_moyenne_actifs, avg(masse) filter (where not actif) masse_moyenne_blesses, count(*) filter (where revenu > 900000) nombre_riches, count(*) filter (where berserk and veteran) nombre_guerriers_fous from geants where dtn >= (current_date - interval '500 days'); QUERY PLAN -------------------------------------------------------------------------------------------- Finalize Aggregate (cost=150172.07..150172.08 rows=1 width=144) -> Gather (cost=150171.82..150172.03 rows=2 width=144) Workers Planned: 2 -> Partial Aggregate (cost=149171.82..149171.83 rows=1 width=144) -> Parallel Seq Scan on geants (cost=0.00..127900.18 rows=945406 width=13) Filter: (dtn >= (CURRENT_DATE - '500 days'::interval)) select avg(taille) filter (where genre = 'M') taille_moyenne_geants, avg(taille) filter (where genre = 'F') taille_moyenne_geantes, avg(masse) filter (where actif) masse_moyenne_actifs, avg(masse) filter (where not actif) masse_moyenne_blesses, count(*) filter (where revenu > 900000) nombre_riches, count(*) filter (where berserk and veteran) nombre_guerriers_fous from geants where dtn >= (current_date - interval '500 days'); taille_moyenne_geants | taille_moyenne_geantes | masse_moyenne_actifs | masse_moyenne_blesses | nombre_riches | nombre_guerriers_fous -----------------------+------------------------+----------------------+-----------------------+---------------+----------------------- 300.5961816218027194 | 300.5453445920576464 | 400.5705109297197845 | 400.4414018601866963 | 227384 | 2235 (1 ligne) Temps : 586,528 ms

      Un seul balayage de la table GEANTS est à présent nécessaire pour obtenir les informations demandées par le Grand Chef. Le temps de traitement cumulé est passée de 2s environ à 0s6. Pari réussi pour Margiono !

Mise à jour : 01/05/2019