La table brno2015
peut être téléchargée et restaurée
ainsi :
curl -kL https://dali.bo/tp_brno2015 -o /tmp/brno2015.dump
createdb brno2015
pg_restore -O -d brno2015 /tmp/brno2015.dump
# une erreur sur l'existence du schéma public est normale
Le schéma brno2015
dispose d’une table pilotes ainsi que
les résultats tour par tour de la course de MotoGP de Brno (CZ) de la
saison 2015.
La table brno2015
indique pour chaque tour, pour chaque
pilote, le temps réalisé dans le tour :
Table "public.brno_2015"
Column | Type | Modifiers
-----------+----------+-----------
no_tour | integer |
no_pilote | integer |
lap_time | interval |
Une table pilotes
permet de connaître les détails d’un
pilote :
Table "public.pilotes"
Column | Type | Modifiers
-------------+---------+-----------
no | integer |
nom | text |
nationalite | text |
ecurie | text |
moto | text |
Précisions sur les données à manipuler : la course est réalisée en
plusieurs tours; certains coureurs n’ont pas terminé la course, leur
relevé de tours s’arrête donc brutalement.
Agrégation
Tout d’abord, nous positionnons le search_path
pour
chercher les objets du schéma brno2015
:
SET search_path = brno2015;
Quel est le pilote qui a le moins gros écart entre son meilleur tour
et son moins bon tour ?
Le coureur :
SELECT nom, max (lap_time) - min (lap_time) as ecart
FROM brno_2015
JOIN pilotes
ON (no_pilote = no )
GROUP BY 1
ORDER BY 2
LIMIT 1 ;
La requête donne le résultat suivant :
nom | ecart
-----------------+--------------
Jorge LORENZO | 00:00:04.661
Déterminer quel est le pilote le plus régulier (écart-type).
Nous excluons le premier tour car il s’agit d’une course avec départ
arrêté, donc ce tour est plus lent que les autres, ici d’au moins 8
secondes :
SELECT nom, stddev (extract (epoch from lap_time)) as stddev
FROM brno_2015
JOIN pilotes
ON (no_pilote = no )
WHERE no_tour > 1
GROUP BY 1
ORDER BY 2
LIMIT 1 ;
Le résultat montre le coureur qui a abandonné en premier :
nom | stddev
-----------------+-------------------
Alex DE ANGELIS | 0.130107647741847
On s’aperçoit qu’Alex De Angelis n’a pas terminé la course. Il semble
donc plus intéressant de ne prendre en compte que les pilotes qui ont
terminé la course et toujours en excluant le premier tour (il y a 22
tours sur cette course, on peut le positionner soit en dur dans la
requête, soit avec un sous-select permettant de déterminer le nombre
maximum de tours) :
SELECT nom, stddev (extract (epoch from lap_time)) as stddev
FROM brno_2015
JOIN pilotes
ON (no_pilote = no )
WHERE no_tour > 1
AND no_pilote in (SELECT no_pilote FROM brno_2015 WHERE no_tour= 22 )
GROUP BY 1
ORDER BY 2
LIMIT 1 ;
Le pilote 19 a donc été le plus régulier :
nom | stddev
-----------------+-------------------
Alvaro BAUTISTA | 0.222825823492654
Window Functions
Si ce n’est pas déjà fait, nous positionnons le search_path pour
chercher les objets du schéma brno2015
:
SET search_path = brno2015;
Afficher la place sur le podium pour chaque coureur.
Les coureurs qui ne franchissent pas la ligne d’arrivée sont dans le
classement malgré tout. Il faut donc tenir compte de cela dans
l’affichage des résultats.
SELECT rank () OVER (ORDER BY max_lap desc , total_time asc ) AS rang,
nom, ecurie, total_time
FROM (SELECT no_pilote,
sum (lap_time) over (PARTITION BY no_pilote) as total_time,
max (no_tour) over (PARTITION BY no_pilote) as max_lap
FROM brno_2015
) AS race_data
JOIN pilotes
ON (race_data.no_pilote = pilotes.no )
GROUP BY nom, ecurie, max_lap, total_time
ORDER BY max_lap desc , total_time asc ;
La requête affiche le résultat suivant :
rang | nom | ecurie | total_time
------+------------------+-----------------------------+--------------
1 | Jorge LORENZO | Movistar Yamaha MotoGP | 00:42:53.042
2 | Marc MARQUEZ | Repsol Honda Team | 00:42:57.504
3 | Valentino ROSSI | Movistar Yamaha MotoGP | 00:43:03.439
4 | Andrea IANNONE | Ducati Team | 00:43:06.113
5 | Dani PEDROSA | Repsol Honda Team | 00:43:08.692
6 | Andrea DOVIZIOSO | Ducati Team | 00:43:08.767
7 | Bradley SMITH | Monster Yamaha Tech 3 | 00:43:14.863
8 | Pol ESPARGARO | Monster Yamaha Tech 3 | 00:43:16.282
9 | Aleix ESPARGARO | Team SUZUKI ECSTAR | 00:43:36.826
10 | Danilo PETRUCCI | Octo Pramac Racing | 00:43:38.303
11 | Yonny HERNANDEZ | Octo Pramac Racing | 00:43:43.015
12 | Scott REDDING | EG 0,0 Marc VDS | 00:43:43.216
13 | Alvaro BAUTISTA | Aprilia Racing Team Gresini | 00:43:47.479
14 | Stefan BRADL | Aprilia Racing Team Gresini | 00:43:47.666
15 | Loris BAZ | Forward Racing | 00:43:53.358
16 | Hector BARBERA | Avintia Racing | 00:43:54.637
17 | Nicky HAYDEN | Aspar MotoGP Team | 00:43:55.43
18 | Mike DI MEGLIO | Avintia Racing | 00:43:58.986
19 | Jack MILLER | CWM LCR Honda | 00:44:04.449
20 | Claudio CORTI | Forward Racing | 00:44:43.075
21 | Karel ABRAHAM | AB Motoracing | 00:44:55.697
22 | Maverick VIÑALES | Team SUZUKI ECSTAR | 00:29:31.557
23 | Cal CRUTCHLOW | CWM LCR Honda | 00:27:38.315
24 | Eugene LAVERTY | Aspar MotoGP Team | 00:08:04.096
25 | Alex DE ANGELIS | E-Motion IodaRacing Team | 00:06:05.782
( 25 rows)
À partir de la requête précédente, afficher également la différence
du temps de chaque coureur par rapport à celui de la première place.
La requête n’est pas beaucoup modifiée, seule la fonction
first_value()
est utilisée pour déterminer le temps du
vainqueur, temps qui sera ensuite retranché au temps du coureur
courant.
SELECT rank () OVER (ORDER BY max_lap desc , total_time asc ) AS rang,
nom, ecurie, total_time,
total_time - first_value (total_time)
OVER (ORDER BY max_lap desc , total_time asc ) AS difference
FROM (SELECT no_pilote,
sum (lap_time) over (PARTITION BY no_pilote) as total_time,
max (no_tour) over (PARTITION BY no_pilote) as max_lap
FROM brno_2015
) AS race_data
JOIN pilotes
ON (race_data.no_pilote = pilotes.no )
GROUP BY nom, ecurie, max_lap, total_time
ORDER BY max_lap desc , total_time asc ;
La requête affiche le résultat suivant :
r | nom | ecurie | total_time | difference
--+-----------------+----------------------+-------------+---------------
1 | Jorge LORENZO | Movistar Yamaha [ ... ] | 00:42:53.042 | 00:00:00
2 | Marc MARQUEZ | Repsol Honda Team | 00:42:57.504 | 00:00:04.462
3 | Valentino ROSSI | Movistar Yamaha [ ... ] | 00:43:03.439 | 00:00:10.397
4 | Andrea IANNONE | Ducati Team | 00:43:06.113 | 00:00:13.071
5 | Dani PEDROSA | Repsol Honda Team | 00:43:08.692 | 00:00:15.65
6 | Andrea DOVIZIOSO| Ducati Team | 00:43:08.767 | 00:00:15.725
7 | Bradley SMITH | Monster Yamaha Tech 3| 00:43:14.863 | 00:00:21.821
8 | Pol ESPARGARO | Monster Yamaha Tech 3| 00:43:16.282 | 00:00:23.24
9 | Aleix ESPARGARO | Team SUZUKI ECSTAR | 00:43:36.826 | 00:00:43.784
10 | Danilo PETRUCCI | Octo Pramac Racing | 00:43:38.303 | 00:00:45.261
11 | Yonny HERNANDEZ | Octo Pramac Racing | 00:43:43.015 | 00:00:49.973
12 | Scott REDDING | EG 0,0 Marc VDS | 00:43:43.216 | 00:00:50.174
13 | Alvaro BAUTISTA | Aprilia Racing [ ... ] | 00:43:47.479 | 00:00:54.437
14 | Stefan BRADL | Aprilia Racing [ ... ] | 00:43:47.666 | 00:00:54.624
15 | Loris BAZ | Forward Racing | 00:43:53.358 | 00:01:00.316
16 | Hector BARBERA | Avintia Racing | 00:43:54.637 | 00:01:01.595
17 | Nicky HAYDEN | Aspar MotoGP Team | 00:43:55.43 | 00:01:02.388
18 | Mike DI MEGLIO | Avintia Racing | 00:43:58.986 | 00:01:05.944
19 | Jack MILLER | CWM LCR Honda | 00:44:04.449 | 00:01:11.407
20 | Claudio CORTI | Forward Racing | 00:44:43.075 | 00:01:50.033
21 | Karel ABRAHAM | AB Motoracing | 00:44:55.697 | 00:02:02.655
22 | Maverick VIÑALES| Team SUZUKI ECSTAR | 00:29:31.557 | -00:13:21.485
23 | Cal CRUTCHLOW | CWM LCR Honda | 00:27:38.315 | -00:15:14.727
24 | Eugene LAVERTY | Aspar MotoGP Team | 00:08:04.096 | -00:34:48.946
25 | Alex DE ANGELIS | E-Motion Ioda[ ... ] | 00:06:05.782 | -00:36:47.26
( 25 rows)
Pour chaque tour, afficher :
le nom du pilote ;
son rang dans le tour ;
son temps depuis le début de la course ;
dans le tour, la différence de temps par rapport au premier.
Pour construire cette requête, nous avons besoin d’obtenir le temps
cumulé tour après tour pour chaque coureur. Nous commençons donc par
écrire une première requête :
SELECT * ,
SUM (lap_time)
OVER (PARTITION BY no_pilote ORDER BY no_tour) AS temps_tour_glissant
FROM brno_2015
Elle retourne le résultat suivant :
no_tour | no_pilote | lap_time | temps_tour_glissant
---------+-----------+--------------+---------------------
1 | 4 | 00:02:02.209 | 00:02:02.209
2 | 4 | 00:01:57.57 | 00:03:59.779
3 | 4 | 00:01:57.021 | 00:05:56.8
4 | 4 | 00:01:56.943 | 00:07:53.743
5 | 4 | 00:01:57.012 | 00:09:50.755
6 | 4 | 00:01:57.011 | 00:11:47.766
7 | 4 | 00:01:57.313 | 00:13:45.079
8 | 4 | 00:01:57.95 | 00:15:43.029
9 | 4 | 00:01:57.296 | 00:17:40.325
10 | 4 | 00:01:57.295 | 00:19:37.62
11 | 4 | 00:01:57.185 | 00:21:34.805
12 | 4 | 00:01:57.45 | 00:23:32.255
13 | 4 | 00:01:57.457 | 00:25:29.712
14 | 4 | 00:01:57.362 | 00:27:27.074
15 | 4 | 00:01:57.482 | 00:29:24.556
16 | 4 | 00:01:57.358 | 00:31:21.914
17 | 4 | 00:01:57.617 | 00:33:19.531
18 | 4 | 00:01:57.594 | 00:35:17.125
19 | 4 | 00:01:57.412 | 00:37:14.537
20 | 4 | 00:01:57.786 | 00:39:12.323
21 | 4 | 00:01:58.087 | 00:41:10.41
22 | 4 | 00:01:58.357 | 00:43:08.767
( … )
Cette requête de base est ensuite utilisée dans une CTE qui sera
utilisée par la requête répondant à la question de départ. La colonne
temps_tour_glissant
est utilisée pour calculer le rang du
pilote dans la course, est affiché et le temps cumulé du meilleur pilote
est récupéré avec la fonction first_value
:
WITH temps_glissant AS (
SELECT no_tour, no_pilote, lap_time,
sum (lap_time)
OVER (PARTITION BY no_pilote
ORDER BY no_tour
) as temps_tour_glissant
FROM brno_2015
ORDER BY no_pilote, no_tour
)
SELECT no_tour, nom,
rank () OVER (PARTITION BY no_tour
ORDER BY temps_tour_glissant ASC
) as place_course,
temps_tour_glissant,
temps_tour_glissant - first_value (temps_tour_glissant)
OVER (PARTITION BY no_tour
ORDER BY temps_tour_glissant asc
) AS difference
FROM temps_glissant t
JOIN pilotes p ON p.no = t.no_pilote;
On pouvait également utiliser une simple sous-requête pour obtenir le
même résultat :
SELECT no_tour,
nom,
rank ()
OVER (PARTITION BY no_tour
ORDER BY temps_tour_glissant ASC
) AS place_course,
temps_tour_glissant,
temps_tour_glissant - first_value (temps_tour_glissant)
OVER (PARTITION BY no_tour
ORDER BY temps_tour_glissant asc
) AS difference
FROM (
SELECT * , SUM (lap_time)
OVER (PARTITION BY no_pilote
ORDER BY no_tour)
AS temps_tour_glissant
FROM brno_2015) course
JOIN pilotes
ON (pilotes.no = course.no_pilote)
ORDER BY no_tour;
La requête fournit le résultat suivant :
no. | nom | place_c. | temps_tour_glissant | difference
---+------------------+----------+---------------------+--------------
1 | Jorge LORENZO | 1 | 00:02:00.83 | 00:00:00
1 | Marc MARQUEZ | 2 | 00:02:01.058 | 00:00:00.228
1 | Andrea DOVIZIOSO | 3 | 00:02:02.209 | 00:00:01.379
1 | Valentino ROSSI | 4 | 00:02:02.329 | 00:00:01.499
1 | Andrea IANNONE | 5 | 00:02:02.597 | 00:00:01.767
1 | Bradley SMITH | 6 | 00:02:02.861 | 00:00:02.031
1 | Pol ESPARGARO | 7 | 00:02:03.239 | 00:00:02.409
( .. )
2 | Jorge LORENZO | 1 | 00:03:57.073 | 00:00:00
2 | Marc MARQUEZ | 2 | 00:03:57.509 | 00:00:00.436
2 | Valentino ROSSI | 3 | 00:03:59.696 | 00:00:02.623
2 | Andrea DOVIZIOSO | 4 | 00:03:59.779 | 00:00:02.706
2 | Andrea IANNONE | 5 | 00:03:59.9 | 00:00:02.827
2 | Bradley SMITH | 6 | 00:04:00.355 | 00:00:03.282
2 | Pol ESPARGARO | 7 | 00:04:00.87 | 00:00:03.797
2 | Maverick VIÑALES | 8 | 00:04:01.187 | 00:00:04.114
( … )
( 498 rows)
Pour chaque coureur, quel est son meilleur tour et quelle place
avait-il sur ce tour ?
Il est ici nécessaire de sélectionner pour chaque tour le temps du
meilleur tour. On peut alors sélectionner les tours pour lequels le
temps du tour est égal au meilleur temps :
WITH temps_glissant AS (
SELECT no_tour, no_pilote, lap_time,
sum (lap_time)
OVER (PARTITION BY no_pilote
ORDER BY no_tour
) as temps_tour_glissant
FROM brno_2015
ORDER BY no_pilote, no_tour
),
classement_tour AS (
SELECT no_tour, no_pilote, lap_time,
rank () OVER (
PARTITION BY no_tour
ORDER BY temps_tour_glissant
) as place_course,
temps_tour_glissant,
min (lap_time) OVER (PARTITION BY no_pilote) as meilleur_temps
FROM temps_glissant
)
SELECT no_tour, nom, place_course, lap_time
FROM classement_tour t
JOIN pilotes p ON p.no = t.no_pilote
WHERE lap_time = meilleur_temps;
Ce qui donne le résultat suivant :
no_tour | nom | place_course | lap_time
---------+------------------+--------------+--------------
4 | Jorge LORENZO | 1 | 00:01:56.169
4 | Marc MARQUEZ | 2 | 00:01:56.048
4 | Valentino ROSSI | 3 | 00:01:56.747
6 | Andrea IANNONE | 5 | 00:01:56.86
6 | Dani PEDROSA | 7 | 00:01:56.975
4 | Andrea DOVIZIOSO | 4 | 00:01:56.943
3 | Bradley SMITH | 6 | 00:01:57.25
17 | Pol ESPARGARO | 8 | 00:01:57.454
4 | Aleix ESPARGARO | 12 | 00:01:57.844
4 | Danilo PETRUCCI | 11 | 00:01:58.121
9 | Yonny HERNANDEZ | 14 | 00:01:58.53
2 | Scott REDDING | 14 | 00:01:57.976
3 | Alvaro BAUTISTA | 21 | 00:01:58.71
3 | Stefan BRADL | 16 | 00:01:58.38
3 | Loris BAZ | 19 | 00:01:58.679
2 | Hector BARBERA | 15 | 00:01:58.405
2 | Nicky HAYDEN | 16 | 00:01:58.338
3 | Mike DI MEGLIO | 18 | 00:01:58.943
4 | Jack MILLER | 22 | 00:01:59.007
2 | Claudio CORTI | 24 | 00:02:00.377
14 | Karel ABRAHAM | 23 | 00:02:01.716
3 | Maverick VIÑALES | 8 | 00:01:57.436
3 | Cal CRUTCHLOW | 11 | 00:01:57.652
3 | Eugene LAVERTY | 20 | 00:01:58.977
3 | Alex DE ANGELIS | 23 | 00:01:59.257
( 25 rows)
Déterminer quels sont les coureurs ayant terminé la course qui ont
gardé la même position tout au long de la course.
WITH nb_tour AS (
SELECT max (no_tour) FROM brno_2015
),
temps_glissant AS (
SELECT no_tour, no_pilote, lap_time,
sum (lap_time) OVER (
PARTITION BY no_pilote
ORDER BY no_tour
) as temps_tour_glissant,
max (no_tour) OVER (PARTITION BY no_pilote) as total_tour
FROM brno_2015
),
classement_tour AS (
SELECT no_tour, no_pilote, lap_time, total_tour,
rank () OVER (
PARTITION BY no_tour
ORDER BY temps_tour_glissant
) as place_course
FROM temps_glissant
)
SELECT no_pilote
FROM classement_tour t
JOIN nb_tour n ON n.max = t.total_tour
GROUP BY no_pilote
HAVING count (DISTINCT place_course) = 1 ;
Elle retourne le résultat suivant :
no_pilote
-----------
93
99
En quelle position a terminé le coureur qui a doublé le plus de
personnes ? Combien de personnes a-t-il doublées ?
WITH temps_glissant AS (
SELECT no_tour, no_pilote, lap_time,
sum (lap_time) OVER (
PARTITION BY no_pilote
ORDER BY no_tour
) as temps_tour_glissant
FROM brno_2015
),
classement_tour AS (
SELECT no_tour, no_pilote, lap_time,
rank () OVER (
PARTITION BY no_tour
ORDER BY temps_tour_glissant
) as place_course,
temps_tour_glissant
FROM temps_glissant
),
depassement AS (
SELECT no_pilote,
last_value (place_course) OVER (PARTITION BY no_pilote) as rang,
CASE
WHEN lag (place_course) OVER (
PARTITION BY no_pilote
ORDER BY no_tour
) - place_course < 0
THEN 0
ELSE lag (place_course) OVER (
PARTITION BY no_pilote
ORDER BY no_tour
) - place_course
END AS depasse
FROM classement_tour t
)
SELECT no_pilote, rang, sum (depasse)
FROM depassement
GROUP BY no_pilote, rang
ORDER BY sum (depasse) DESC
LIMIT 1 ;
Grouping Sets
La suite de ce TP est maintenant réalisé avec la base de formation
habituelle. Attention, ce TP nécessite l’emploi d’une version 9.5 ou
supérieure de PostgreSQL.
Tout d’abord, nous positionnons le search_path pour chercher les
objets du schéma magasin
:
SET search_path = magasin;
En une seule requête, afficher le montant total des commandes par
année et pays et le montant total des commandes uniquement par
année.
SELECT extract ('year' from date_commande) AS annee, code_pays,
SUM (quantite* prix_unitaire) AS montant_total_commande
FROM commandes c
JOIN lignes_commandes l
ON (c.numero_commande = l.numero_commande)
JOIN clients
ON (c.client_id = clients.client_id)
JOIN contacts co
ON (clients.contact_id = co.contact_id)
GROUP BY GROUPING SETS (
(extract ('year' from date_commande), code_pays),
(extract ('year' from date_commande))
);
Le résultat attendu est :
annee | code_pays | montant_total_commande
-------+-----------+------------------------
2003 | DE | 49634.24
2003 | FR | 10003.98
2003 | | 59638.22
2008 | CA | 1016082.18
2008 | CN | 801662.75
2008 | DE | 694787.87
2008 | DZ | 663045.33
2008 | FR | 5860607.27
2008 | IN | 741850.87
2008 | PE | 1167825.32
2008 | RU | 577164.50
2008 | US | 928661.06
2008 | | 12451687.15
( ... )
Ajouter également le montant total des commandes depuis le début de
l’activité.
L’opérateur de regroupement ROLL UP
amène le niveau
d’agrégation sans regroupement :
SELECT extract ('year' from date_commande) AS annee, code_pays,
SUM (quantite* prix_unitaire) AS montant_total_commande
FROM commandes c
JOIN lignes_commandes l
ON (c.numero_commande = l.numero_commande)
JOIN clients
ON (c.client_id = clients.client_id)
JOIN contacts co
ON (clients.contact_id = co.contact_id)
GROUP BY ROLLUP (extract ('year' from date_commande), code_pays);
Ajouter également le montant total des commandes par pays.
Cette fois, l’opérateur CUBE
permet d’obtenir l’ensemble
de ces informations :
SELECT extract ('year' from date_commande) AS annee, code_pays,
SUM (quantite* prix_unitaire) AS montant_total_commande
FROM commandes c
JOIN lignes_commandes l
ON (c.numero_commande = l.numero_commande)
JOIN clients
ON (c.client_id = clients.client_id)
JOIN contacts co
ON (clients.contact_id = co.contact_id)
GROUP BY CUBE (extract ('year' from date_commande), code_pays);
À partir de la requête précédente, ajouter une colonne par critère
de regroupement, de type booléen, qui est positionnée à
true
lorsque le regroupement est réalisé sur l’ensemble des
valeurs de la colonne.
Ces colonnes booléennes permettent d’indiquer à l’application comment
gérer la présentation des résultats.
SELECT grouping (extract ('year' from date_commande))::boolean AS g_annee,
grouping (code_pays)::boolean AS g_pays,
extract ('year' from date_commande) AS annee,
code_pays,
SUM (quantite* prix_unitaire) AS montant_total_commande
FROM commandes c
JOIN lignes_commandes l
ON (c.numero_commande = l.numero_commande)
JOIN clients
ON (c.client_id = clients.client_id)
JOIN contacts co
ON (clients.contact_id = co.contact_id)
GROUP BY CUBE (extract ('year' from date_commande), code_pays);