Je suis Charlie

Autres trucs

Accueil

Seulement les RFC

Seulement les fiches de lecture

Ève

PostgreSQL et les quantiles, via les « window functions »

Première rédaction de cet article le 9 novembre 2012


Le SGBD PostgreSQL dispose de tas de fonctions utiles. Une relativement peu connue est le système des « window functions » qui agissent sur un découpage d'un ensemble de tuples, s'appliquant à chaque part découpée. Cela permet notamment de travailler sur les quantiles.

Bon, avant de détailler, voyons un cas réel et concret. On a une base de données du personnel et une table stocke les salaires :

CREATE TABLE Salaries 
  (id SERIAL UNIQUE NOT NULL,
   name TEXT UNIQUE NOT NULL,
   salary INTEGER NOT NULL);

INSERT INTO Salaries (name, salary) VALUES ('John', 3000);
INSERT INTO Salaries (name, salary) VALUES ('Sandy', 2000);
...

On charge cette table (le fichier complet est disponible). Il est alors facile de trouver des choses comme le salaire moyen :

essais=> SELECT round(avg(salary)) FROM Salaries;
 round 
-------
  3417
(1 row)

ou de trier selon le salaire, ici du plus payé vers le moins payé :

essais=> SELECT name,salary FROM Salaries ORDER BY salary DESC;
  name   | salary 
---------+--------
 Patt    |   9600
 Robert  |   7000
 Steve   |   4000
...

Mais on cherche des choses plus subtiles. Par exemple, le salaire moyen plait aux dirigeants pour la communication car il est relativement élevé, tiré vers le haut par quelques gros salaires. En cas de forte inégalité, ce qui est le cas dans l'entreprise de notre exemple, il est nettement moins pertinent que le le salaire médian. Il existe plusieurs méthodes pour calculer ce dernier. Par exemple, la médiane étant par définition le salaire tel que la moitié des employés gagnent davantage, un calcul approximatif (qui ignore les arrondis) est :

essais=> SELECT salary FROM salaries ORDER BY salary DESC LIMIT 
                            (SELECT count(id) FROM salaries) / 2;
...
   2900

Un moyen plus sophistiqué est de travailler sur les quantiles. Les quantiles sont les découpages des données triées en N parties égales. Si N = 100, on parle de centiles, si N = 10 de déciles, etc. Pour la médiane, on va prendre N = 2. Mais, avant cela, un petit détour par les « window functions » de PostgreSQL.

Ces fonctions opèrent sur une partie des tuples retournés par une requête. Le découpage en parties est fait par la clause OVER. Prenons l'exemple d'un découpage en autant de parties qu'il y a de tuples triés, et de la « window function » rank() qui indique la place d'un tuple par rapport aux autres :

essais=> SELECT name,salary,rank() OVER (ORDER BY salary DESC) FROM Salaries 
                              ORDER BY name;
  name   | salary | rank 
---------+--------+------
 Alfonso |   2300 |   14
 Bill    |   2900 |    8
 Bob     |   2800 |   10
 Chris   |   2900 |    8
 Chuck   |   3800 |    4
...

Le (ORDER BY salary DESC) après OVER indique que les tuples vont être classés selon le salaire décroissant. rank() va alors s'appliquer à ce classement : Alfonso a le quatorzième salaire (en partant du haut), Bill le huitième, etc. Notez que les ex aequo sont possible (ici, Bill et Chris). row_number() ferait la même chose que rank() mais sans les ex aequo.

Revenons maintenant aux quantiles. La fonction ntile() permet de découper un ensemble de tuples en quantiles (le nombre de quantiles étant l'argument de ntile(), ici on utilise des terciles) :

essais=> SELECT  name,salary,ntile(3) OVER (ORDER BY salary) FROM Salaries;
  name   | salary | ntile 
---------+--------+-------
 Sandy   |   2000 |     1
...
 Lou     |   2600 |     2
...
 Peter   |   3400 |     3

Ici, Sandy est dans le tercile le plus bas (ntile() renvoie 1) et Peter dans celui des meilleurs salaires.

On peut utiliser les quantiles de tas de façons. Par exemple :

essais=> SELECT n,min(salary),max(salary) FROM 
              (SELECT name,salary,ntile(3) OVER (ORDER BY salary) as n FROM Salaries) AS q 
              GROUP BY n ORDER BY n;
 n | min  | max  
---+------+------
 1 | 2000 | 2500
 2 | 2600 | 3000
 3 | 3400 | 9600

Ici, on a la plage des salaires pour chaque tercile. Et la médiane que j'avais promise ?

essais=> SELECT n,min(salary),max(salary) FROM 
              (SELECT name,salary,ntile(2) OVER (ORDER BY salary) as n FROM Salaries) AS q  
              GROUP BY n ORDER BY n;
 n | min  | max  
---+------+------
 1 | 2000 | 2800
 2 | 2900 | 9600

Elle est de 2850.

Une autre façon de représenter l'inégalité est de voir quelle part des salaires représente chaque quantile. Passons à des quintiles (N = 5) :

essais=> SELECT n, sum(salary)*100/(SELECT sum(salary) FROM Salaries) AS percentage 
             FROM (SELECT name,salary,ntile(5) OVER (ORDER BY salary) as n FROM Salaries) AS q  
              GROUP BY n ORDER BY n;
 n | percentage 
---+------------
 1 |         13
 2 |         16
 3 |         18
 4 |         17
 5 |         33

Le quintile supérieur représente le tiers du total des salaires. (On note que c'est le seul des cinq quantiles à représenter plus que sa part, qui serait de 20 % en cas de totale égalité.)

Tout cela ne sert évidemment pas qu'aux salaires. Si on prend des requêtes DNS analysées par DNSmezzo, et qu'on étudie la répartition du trafic par résolveur, on observe que certains résolveurs n'envoient que très peu de requêtes (c'était peut-être un étudiant faisant un ou deux dig), alors que d'autres sont bien plus bavards. Ici, on étudie 447 658 requêtes reçues par un serveur faisant autorité, en IPv6. On observe 3 281 préfixes IPv6 de longueur /48. Quelle est la répartition des requêtes ?

dnsmezzo=> SELECT n,min(requests),max(requests),
       to_char(sum(requests)*100/(SELECT count(id) FROM DNS_packets WHERE query AND family(src_address)=6), '99.9') AS percentage FROM 
        (SELECT prefix,requests,ntile(10) OVER (ORDER BY requests) AS n FROM 
             (SELECT set_masklen(src_address::cidr, 48) AS prefix, count(id) AS requests FROM
                 DNS_packets WHERE query AND  family(src_address)=6          
                GROUP BY prefix ORDER by requests DESC) AS qinternal) AS qexternal 
   GROUP BY n ORDER BY n;

 n  | min |  max  | percentage 
----+-----+-------+------------
  1 |   1 |     1 |    .1
  2 |   1 |     1 |    .1
  3 |   1 |     2 |    .1
  4 |   2 |     3 |    .2
  5 |   3 |     4 |    .2
  6 |   4 |     7 |    .4
  7 |   7 |    14 |    .8
  8 |  14 |    32 |   1.6
  9 |  32 |    89 |   3.9
 10 |  89 | 81718 |  92.7

On voit que les cinq premiers déciles ont un trafic négligeable, et que le dixième décile, les clients les plus bavards, fait plus de 90 % des requêtes à lui seul. (En utilisant des centiles et non plus des déciles, on confirme cette grande inégalité : le centile supérieur fait 72 % des requêtes.)

Un bon tutoriel sur les « windows functions » sur PostgreSQL (et qui couvre la clause PARTITION dont je n'ai pas parlé ici), est en ligne. Pour jouer avec les quantiles, il existe aussi une extension de PostgreSQL que je n'ai pas testée, quantile. Elle est notamment censée être plus rapide.

Ces fonctions ne sont pas spécifiques à PostgreSQL, elles sont apparues dans la norme SQL en 2003 sous le nom de « fonctions analytiques ». On les trouve dans plusieurs autres SGBD, par exemple Firebird. On les trouve dans beaucoup de questions sur StackOverflow. Merci à Malek Shabou et Pierre Yager pour leur relecture.

Version PDF de cette page (mais vous pouvez aussi imprimer depuis votre navigateur, il y a une feuille de style prévue pour cela)

Source XML de cette page (cette page est distribuée sous les termes de la licence GFDL)