Fonctionnalités analytiques

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

Le 20ème siècle est si loin...

      Sur les systèmes modernes, balayer une table volumineuse ne prend pas un temps infini. Cependant, s'il est possible de l'éviter, il est toujours dommage de lire plusieurs fois les mêmes données dans une requête afin de les joindre.
      Nous allons considérer la table LANCERS contenant les performances des 54 géants du clan (idg est l'identifiant du géant, dtl la date du lancers et perf le résultat du lancer de cailloux en centimètres)
      Oumpfor demande à son scribe Morgiono la moyenne glissante à 7 jours des performances maximales et minimales journalières pour chaque géant.
      Margiono tente de l’écrire en partant d’un vieux bouquin SQL puis d’un livre plus récent :

select version(); version ----------------------------------------------------------------------------------------------------------------------------------- PostgreSQL 11.4 (Ubuntu 11.4-1.pgdg18.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0, 64-bit create table lancers(dtl timestamp, idg smallint, perf integer); CREATE TABLE with recursive serie(i) as (select 576000 UNION ALL select i - 1 from serie where i > 1) insert into lancers(dtl, idg, perf) select current_timestamp - (i || ' minutes')::interval, trunc(random() * 54 + 1), case when random() <= 0.0001 then 50000 + trunc(random() * 50000 + 1) else trunc(random() * 50000 + 1) end from serie ; INSERT 0 576000 create index lancers_i1 on lancers(idg, dtl); CREATE INDEX create index lancers_br1 on lancers using brin(dtl); CREATE INDEX explain select W.idg geant, W.jour, avg(W.perf_max) moyenne_mobile_perf_max_7, avg(W.perf_min) moyenne_mobile_perf_min_7 from ( select A.idg, A.jour, B.perf_max, B.perf_min from ( select idg, date_trunc('day', dtl) jour from lancers group by idg, date_trunc('day', dtl) ) AS A JOIN ( select idg, date_trunc('day', dtl) jour, max(perf) perf_max, min(perf) perf_min from lancers group by idg, date_trunc('day', dtl) ) AS B ON (A.idg = B.idg AND B.jour between A.jour - interval '6 days' and A.jour) ) AS W group by idg, jour order by 1, 2 asc; QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- GroupAggregate (cost=150540.69..38889920.69 rows=40000 width=42) Group Key: lancers.idg, (date_trunc('day'::text, lancers.dtl)) -> Merge Join (cost=150540.69..37507020.69 rows=184320000 width=18) Merge Cond: (lancers.idg = lancers_1.idg) Join Filter: (((date_trunc('day'::text, lancers_1.dtl)) <= (date_trunc('day'::text, lancers.dtl))) AND ((date_trunc('day'::text, lancers_1.dtl)) >= ((date_trunc('day'::text, lancers.dtl)) - '6 days'::interval))) -> Group (cost=75270.34..81030.34 rows=576000 width=10) Group Key: lancers.idg, (date_trunc('day'::text, lancers.dtl)) -> Sort (cost=75270.34..76710.34 rows=576000 width=10) Sort Key: lancers.idg, (date_trunc('day'::text, lancers.dtl)) -> Seq Scan on lancers (cost=0.00..10314.00 rows=576000 width=10) -> Materialize (cost=75270.34..95430.34 rows=576000 width=18) -> GroupAggregate (cost=75270.34..88230.34 rows=576000 width=18) Group Key: lancers_1.idg, (date_trunc('day'::text, lancers_1.dtl)) -> Sort (cost=75270.34..76710.34 rows=576000 width=14) Sort Key: lancers_1.idg, (date_trunc('day'::text, lancers_1.dtl)) -> Seq Scan on lancers lancers_1 (cost=0.00..10314.00 rows=576000 width=14) select W.idg geant, W.jour, avg(W.perf_max) moyenne_mobile_perf_max_7, avg(W.perf_min) moyenne_mobile_perf_min_7 from ( select A.idg, A.jour, B.perf_max, B.perf_min from ( select idg, date_trunc('day', dtl) jour from lancers group by idg, date_trunc('day', dtl) ) AS A JOIN ( select idg, date_trunc('day', dtl) jour, max(perf) perf_max, min(perf) perf_min from lancers group by idg, date_trunc('day', dtl) ) AS B ON (A.idg = B.idg AND B.jour between A.jour - interval '6 days' and A.jour) ) AS W group by idg, jour order by 1, 2 asc; ... Durée : 1676,266 ms (00:01,676) Durée : 1579,950 ms (00:01,580) Durée : 1603,530 ms (00:01,604) explain select idg geant, date_trunc('day', dtl) jour, avg(max(perf)) over(partition by idg order by date_trunc('day', dtl) rows 6 preceding) moyenne_mobile_perf_max_7, avg(min(perf)) over(partition by idg order by date_trunc('day', dtl) rows 6 preceding) moyenne_mobile_perf_min_7 from lancers group by idg, date_trunc('day', dtl) order by 1,2 asc; QUERY PLAN ----------------------------------------------------------------------------------- WindowAgg (cost=75270.34..102630.34 rows=576000 width=74) -> GroupAggregate (cost=75270.34..89670.34 rows=576000 width=18) Group Key: idg, (date_trunc('day'::text, dtl)) -> Sort (cost=75270.34..76710.34 rows=576000 width=14) Sort Key: idg, (date_trunc('day'::text, dtl)) -> Seq Scan on lancers (cost=0.00..10314.00 rows=576000 width=14) select idg geant, date_trunc('day', dtl) jour, avg(max(perf)) over(partition by idg order by date_trunc('day', dtl) rows 6 preceding) moyenne_mobile_perf_max_7, avg(min(perf)) over(partition by idg order by date_trunc('day', dtl) rows 6 preceding) moyenne_mobile_perf_min_7 from lancers group by idg, date_trunc('day', dtl) order by 1,2 asc; ... Temps : 401,613 ms Temps : 396,167 ms Temps : 375,216 ms

      La première requête globalement SQL-1992 utilise une auto-jointure et donne lieu à 2 balayages complets de la table LANCERS. La deuxième requête tire parti des possibilités analytiques offertes par la norme SQL:2003. Il est possible grâce aux clauses de fenêtrage de travailler de différentes façons (partionnement, tri) le jeu de résultats en mode "multiligne" APRÈS sa constitution et d'appliquer des fonctions analytiques. La deuxième requête n’effectue qu’un seul balayage de LANCERS et surtout ne donne pas lieu à une auto-jointure. La première requête a pris environ 1s5 alors que la deuxième n’a pris que 0s4.

      Si vous êtes (comme moi) un vieil habitué d’Oracle, vous ne connaissez peut-être pas la norme SQL:1992 et même la première requête et son "JOIN" peut vous sembler peu familière. Si vous utilisez des (+) quand vous voulez réaliser une jointure externe ou encore des start with...connect by quand vous voulez réaliser une requête hiérarchique alors vous êtes hors standard SQL. Je ne vous jette pas la pierre, changer les habitudes et donc prendre des risques n’est pas facile lorsqu’on doit coder de nouvelles fonctionnalités avec de la pression au niveau des délais.
      Je ne jette pas non plus la pierre à Oracle. Ce SGBD respecte les nouvelles normes mais conserve les syntaxes propriétaires pour compatiblité ascendante. Leurs requêtes hiérarchiques étaient par exemple disponibles depuis Oracle 2 au début des années 1980. Elles ont rendu bien des services mais écrire en 2016 du pseudo SQL des années 1980 c’est dommage. Certaines versions propriétaires de PostgreSQL, comme celle d’EDB, vous permettraient de garder ces (mauvaises) habitudes mais quitte à changer de SGBD autant les abandonner.
      Esnuite pourquoi se limiter à 1992 ? Comme le rappelle "Use the index Luke", vous n’avez plus une station de travail sous Windows 3.1 mais sous Debian 10 ou Ubuntu 18.04 alors pourquoi se cantonner à SQL:1992 au niveau de votre SGBD ? Le passage à PostgreSQL peut être une bonne occasion de changer vos habitudes et de découvrir la richesse des dernières évolutions de la norme SQL.
      PostgreSQL est particulièrement respectueux de la norme, la documentation indique clairement pour chaque syntaxe si vous utilisez quelque chose de "standard" ou une "extension de langage". En adoptant les nouveautés, vous rendrez votre code plus lisible et vous aurez peut-être en plus d’excellentes surprises au niveau des performances, comme dans le petit exemple présenté.

Mise à jour : 27/07/2019