Bonjour à tous.
Alors j'annonce, ça va être long. Bon, j'ai un problème que je rencontre assez souvent dans mon domaine (le décisionnel), qu'on contourne toujours un peu comme on peut et pour lequel j'aimerai bien pouvoir poser des Best Practices, voire écrire un article: agréger correctement des indicateurs dans un modèle entête - détail.
L'idéal serait que les solutions fonctionnent à partir de 9i.
Imaginons le modèle suivant :
Vendeur (id, nom, salaire)
Vente (id, id_vendeur, client, ristourne)
Vente_Detail (id, id_vente, objet, montant)
on a donc un modèle
Vendeur (1.1) - (1.n) Vente (1.1) - (1.n) Vente_Detail
Soit, pour (1) vendeur (1 à plusieurs) ventes et pour (1) vente (1 à plusieurs) lignes de détails.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9 nom salaire client ristourne objet montant BOB 1000 M.A 10 truc1 100 BOB 1000 M.A 10 truc2 200 BOB 1000 M.B 20 truc1 100 BOB 1000 M.B 20 truc2 200 TED 2000 M.C 100 truc1 100 TED 2000 M.C 100 truc2 200 TED 2000 M.D 200 truc1 100 TED 2000 M.D 200 truc2 200Les 3 indicateurs à agréger sont:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 WITH VENDEUR AS ( select 0 "id", 'BOB' "nom", 1000 "salaire" from DUAL UNION select 1 "id", 'TED' "nom", 2000 "salaire" from DUAL ) ,VENTE AS ( select 0 "id", 0 "id_vendeur", 'M.A' "client", 10 "ristourne" from DUAL UNION select 1 "id", 0 "id_vendeur", 'M.B' "client", 20 "ristourne" from DUAL UNION select 2 "id", 1 "id_vendeur", 'M.C' "client", 100 "ristourne" from DUAL UNION select 3 "id", 1 "id_vendeur", 'M.D' "client", 200 "ristourne" from DUAL ) ,VENTE_DETAIL AS ( select 0 "id", 0 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 1 "id", 0 "id_vente", 'truc2' "objet", 200 "montant" from DUAL UNION select 2 "id", 1 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 3 "id", 1 "id_vente", 'truc2' "objet", 200 "montant" from DUAL UNION select 4 "id", 2 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 5 "id", 2 "id_vente", 'truc2' "objet", 200 "montant" from DUAL UNION select 6 "id", 3 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 7 "id", 3 "id_vente", 'truc2' "objet", 200 "montant" from DUAL ) select VENDEUR."nom", VENDEUR."salaire", VENTE."client", VENTE."ristourne", VENTE_DETAIL."objet", VENTE_DETAIL."montant" from VENDEUR inner join VENTE on (VENDEUR."id" = VENTE."id_vendeur") inner join VENTE_DETAIL on (VENTE."id" = VENTE_DETAIL."id_vente")
Salaire du vendeur (on va dire qu'on travaille sur une période définie, un mois par exemple)
Ristourne accordée au client dans une vente
Montant de l'objet vendu
A partir de ces données à plat, essayons agréger les résultats en supprimant 1 à 1 les dimensions, pour aller vers le résultat le plus agrégé:
Pour le premier niveau d'agrégation supprimons le nom de l'objet vendu, pour agréger les résultats par vendeur et client (ou vente, car pour l'exemple 1 client = 1 vente):
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 Total SALAIRE Total RISTOURNE Total MONTANT 3000 330 1200
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5 nom salaire client ristourne SUM(VENTE_DETAIL."MONTANT") BOB 1000 M.A 10 300 BOB 1000 M.B 20 300 TED 2000 M.D 200 300 TED 2000 M.C 100 300Il n'y a pas de piège, un SUM classique dans un GROUP BY.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 WITH VENDEUR AS ( select 0 "id", 'BOB' "nom", 1000 "salaire" from DUAL UNION select 1 "id", 'TED' "nom", 2000 "salaire" from DUAL ) ,VENTE AS ( select 0 "id", 0 "id_vendeur", 'M.A' "client", 10 "ristourne" from DUAL UNION select 1 "id", 0 "id_vendeur", 'M.B' "client", 20 "ristourne" from DUAL UNION select 2 "id", 1 "id_vendeur", 'M.C' "client", 100 "ristourne" from DUAL UNION select 3 "id", 1 "id_vendeur", 'M.D' "client", 200 "ristourne" from DUAL ) ,VENTE_DETAIL AS ( select 0 "id", 0 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 1 "id", 0 "id_vente", 'truc2' "objet", 200 "montant" from DUAL UNION select 2 "id", 1 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 3 "id", 1 "id_vente", 'truc2' "objet", 200 "montant" from DUAL UNION select 4 "id", 2 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 5 "id", 2 "id_vente", 'truc2' "objet", 200 "montant" from DUAL UNION select 6 "id", 3 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 7 "id", 3 "id_vente", 'truc2' "objet", 200 "montant" from DUAL ) select VENDEUR."nom", VENDEUR."salaire", VENTE."client", VENTE."ristourne", sum(VENTE_DETAIL."montant") from VENDEUR inner join VENTE on (VENDEUR."id" = VENTE."id_vendeur") inner join VENTE_DETAIL on (VENTE."id" = VENTE_DETAIL."id_vente") group by VENDEUR."nom", VENDEUR."salaire", VENTE."client", VENTE."ristourne"
Pour le second niveau d'agrégation supprimons le nom du client (et donc la dimension Vente), pour agréger les résultats par vendeur :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 nom salaire SUM(VENTE."RISTOURNE") SUM(VENTE_DETAIL."MONTANT") BOB 1000 60 600 TED 2000 600 600Là on voit bien qu'il y a un souci : si les salaires et la somme des montants sont corrects, les ristournes ont été multipliées par 2 (60 au lieu de 30 et 600 au lieu de 300), la faute à la jointure entre Vente et Vente_Detail.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 WITH VENDEUR AS ( select 0 "id", 'BOB' "nom", 1000 "salaire" from DUAL UNION select 1 "id", 'TED' "nom", 2000 "salaire" from DUAL ) ,VENTE AS ( select 0 "id", 0 "id_vendeur", 'M.A' "client", 10 "ristourne" from DUAL UNION select 1 "id", 0 "id_vendeur", 'M.B' "client", 20 "ristourne" from DUAL UNION select 2 "id", 1 "id_vendeur", 'M.C' "client", 100 "ristourne" from DUAL UNION select 3 "id", 1 "id_vendeur", 'M.D' "client", 200 "ristourne" from DUAL ) ,VENTE_DETAIL AS ( select 0 "id", 0 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 1 "id", 0 "id_vente", 'truc2' "objet", 200 "montant" from DUAL UNION select 2 "id", 1 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 3 "id", 1 "id_vente", 'truc2' "objet", 200 "montant" from DUAL UNION select 4 "id", 2 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 5 "id", 2 "id_vente", 'truc2' "objet", 200 "montant" from DUAL UNION select 6 "id", 3 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 7 "id", 3 "id_vente", 'truc2' "objet", 200 "montant" from DUAL ) select VENDEUR."nom", VENDEUR."salaire", sum(VENTE."ristourne"), sum(VENTE_DETAIL."montant") from VENDEUR inner join VENTE on (VENDEUR."id" = VENTE."id_vendeur") inner join VENTE_DETAIL on (VENTE."id" = VENTE_DETAIL."id_vente") group by VENDEUR."nom", VENDEUR."salaire"
Une première solution pourrait être de pré-agréger chaque indicateur au niveau qui va bien puis de joindre les résultats.
Une autre, qui permet de conserver l'enchaînement des tables et donc qui m'arrange plus, est d'utiliser les partitions, mais cela oblige à faire une sur-requête et d'utiliser un distinct au lieu d'un GROUP BY :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 nom salaire SUM("RISTOURNE")OVER(PARTITIONBY"NOM") SUM("MONTANT")OVER(PARTITIONBY"NOM") TED 2000 300 600 BOB 1000 30 600
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51 WITH VENDEUR AS ( select 0 "id", 'BOB' "nom", 1000 "salaire" from DUAL UNION select 1 "id", 'TED' "nom", 2000 "salaire" from DUAL ) ,VENTE AS ( select 0 "id", 0 "id_vendeur", 'M.A' "client", 10 "ristourne" from DUAL UNION select 1 "id", 0 "id_vendeur", 'M.B' "client", 20 "ristourne" from DUAL UNION select 2 "id", 1 "id_vendeur", 'M.C' "client", 100 "ristourne" from DUAL UNION select 3 "id", 1 "id_vendeur", 'M.D' "client", 200 "ristourne" from DUAL ) ,VENTE_DETAIL AS ( select 0 "id", 0 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 1 "id", 0 "id_vente", 'truc2' "objet", 200 "montant" from DUAL UNION select 2 "id", 1 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 3 "id", 1 "id_vente", 'truc2' "objet", 200 "montant" from DUAL UNION select 4 "id", 2 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 5 "id", 2 "id_vente", 'truc2' "objet", 200 "montant" from DUAL UNION select 6 "id", 3 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 7 "id", 3 "id_vente", 'truc2' "objet", 200 "montant" from DUAL ) select distinct "nom", min("salaire") over (partition by "nom") "salaire", sum("ristourne") over (partition by "nom"), sum("montant") over (partition by "nom") from ( select distinct VENDEUR."nom", min(VENDEUR."salaire") over (partition by VENDEUR."nom") "salaire", min(VENTE."ristourne") over (partition by VENTE."id") "ristourne", sum(VENTE_DETAIL."montant") over (partition by VENTE."id") "montant" from VENDEUR inner join VENTE on (VENDEUR."id" = VENTE."id_vendeur") inner join VENTE_DETAIL on (VENTE."id" = VENTE_DETAIL."id_vente") )
Le même problème se posera pour le dernier niveau d'agrégation : les salaires vont être multipliés par le nombre de ventes (voire par le nombre de lignes de détail des ventes, selon la requête). Il faut donc utiliser la même méthode pour obtenir une agrégation correcte :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 SUM("SALAIRE") SUM("RISTOURNE") SUM("MONTANT") 3000 330 1200D'où ma question : comment géreriez-vous cette problématique ? Toutes les solutions sont acceptées, même une remise en question du modèle, pourquoi pas (par exemple faire remonter au niveau d'agrégation au dessus la somme du niveau d'en dessous) ou des fonctions très avancées.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58 WITH VENDEUR AS ( select 0 "id", 'BOB' "nom", 1000 "salaire" from DUAL UNION select 1 "id", 'TED' "nom", 2000 "salaire" from DUAL ) ,VENTE AS ( select 0 "id", 0 "id_vendeur", 'M.A' "client", 10 "ristourne" from DUAL UNION select 1 "id", 0 "id_vendeur", 'M.B' "client", 20 "ristourne" from DUAL UNION select 2 "id", 1 "id_vendeur", 'M.C' "client", 100 "ristourne" from DUAL UNION select 3 "id", 1 "id_vendeur", 'M.D' "client", 200 "ristourne" from DUAL ) ,VENTE_DETAIL AS ( select 0 "id", 0 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 1 "id", 0 "id_vente", 'truc2' "objet", 200 "montant" from DUAL UNION select 2 "id", 1 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 3 "id", 1 "id_vente", 'truc2' "objet", 200 "montant" from DUAL UNION select 4 "id", 2 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 5 "id", 2 "id_vente", 'truc2' "objet", 200 "montant" from DUAL UNION select 6 "id", 3 "id_vente", 'truc1' "objet", 100 "montant" from DUAL UNION select 7 "id", 3 "id_vente", 'truc2' "objet", 200 "montant" from DUAL ) select sum("salaire"), sum("ristourne"), sum("montant") from ( select distinct "nom", min("salaire") over (partition by "nom") "salaire", sum("ristourne") over (partition by "nom") "ristourne", sum("montant") over (partition by "nom") "montant" from ( select distinct VENDEUR."nom", min(VENDEUR."salaire") over (partition by VENDEUR."nom") "salaire", min(VENTE."ristourne") over (partition by VENTE."id") "ristourne", sum(VENTE_DETAIL."montant") over (partition by VENTE."id") "montant" from VENDEUR inner join VENTE on (VENDEUR."id" = VENTE."id_vendeur") inner join VENTE_DETAIL on (VENTE."id" = VENTE_DETAIL."id_vente") ) )
L'important, dans ma vision, étant d'obtenir un code SQL dont le coeur, cad l'organisation des tables et les jointures, ne changent pas selon le niveau d'agrégation, donc pas de pré-agrégation. Mais ça n'empêche pas d'évoquer la solution.
Merci
Partager