Schéma :
CREATE TABLE "items" (
"id" SERIAL NOT NULL PRIMARY KEY,
"country" VARCHAR(2) NOT NULL,
"created" TIMESTAMP WITH TIME ZONE NOT NULL,
"price" NUMERIC(11, 2) NOT NULL
);
CREATE TABLE "payments" (
"id" SERIAL NOT NULL PRIMARY KEY,
"created" TIMESTAMP WITH TIME ZONE NOT NULL,
"amount" NUMERIC(11, 2) NOT NULL,
"item_id" INTEGER NULL
);
CREATE TABLE "extras" (
"id" SERIAL NOT NULL PRIMARY KEY,
"created" TIMESTAMP WITH TIME ZONE NOT NULL,
"amount" NUMERIC(11, 2) NOT NULL,
"item_id" INTEGER NULL
);
Données :
INSERT INTO items VALUES
(1, 'CZ', '2016-11-01', 100),
(2, 'CZ', '2016-11-02', 100),
(3, 'PL', '2016-11-03', 20),
(4, 'CZ', '2016-11-04', 150)
;
INSERT INTO payments VALUES
(1, '2016-11-01', 60, 1),
(2, '2016-11-01', 60, 1),
(3, '2016-11-02', 100, 2),
(4, '2016-11-03', 25, 3),
(5, '2016-11-04', 150, 4)
;
INSERT INTO extras VALUES
(1, '2016-11-01', 5, 1),
(2, '2016-11-02', 1, 2),
(3, '2016-11-03', 2, 3),
(4, '2016-11-03', 3, 3),
(5, '2016-11-04', 5, 4)
;
Donc nous avons:
Maintenant, je veux obtenir des réponses aux questions suivantes:
Avec la requête suivante ( SQLFiddle ):
SELECT
country AS "group_by",
COUNT(DISTINCT items.id) AS "item_count",
SUM(items.price) AS "cost",
SUM(payments.amount) AS "earned",
SUM(extras.amount) AS "extra_earned"
FROM items
LEFT OUTER JOIN payments ON (items.id = payments.item_id)
LEFT OUTER JOIN extras ON (items.id = extras.item_id)
GROUP BY 1;
Les résultats sont faux:
group_by | item_count | cost | earned | extra_earned
----------+------------+--------+--------+--------------
CZ | 3 | 450.00 | 370.00 | 16.00
PL | 1 | 40.00 | 50.00 | 5.00
Le coût et extra_earned pour CZ sont invalides - 450 au lieu de 350 et 16 au lieu de 11. Le coût et gagné pour PL sont également invalides - ils sont doublés.
Je comprends qu'en cas de LEFT OUTER JOIN
il y aura 2 lignes pour l'élément avec items.id = 1 (et ainsi de suite pour les autres correspondances), mais je ne sais pas comment créer une requête appropriée.
Questions :
Version PostgreSQL : 9.6.1
Puisqu'il peut y avoir plusieurs payments
et plusieurs extras
par item
, vous rencontrez un "proxy cross join" entre ces deux tables. Agréger les lignes par item_id
avant se joindre à item
et tout devrait être correct:
SELECT i.country AS group_by
, COUNT(*) AS item_count
, SUM(i.price) AS cost
, SUM(p.sum_amount) AS earned
, SUM(e.sum_amount) AS extra_earned
FROM items i
LEFT JOIN (
SELECT item_id, SUM(amount) AS sum_amount
FROM payments
GROUP BY 1
) p ON p.item_id = i.id
LEFT JOIN (
SELECT item_id, SUM(amount) AS sum_amount
FROM extras
GROUP BY 1
) e ON e.item_id = i.id
GROUP BY 1;
Prenons l'exemple du "marché aux poissons":
Pour être précis, SUM(i.price)
serait incorrect après avoir rejoint une seule table n, qui multiplie chaque prix par le nombre de lignes liées. Le faire deux fois ne fait qu'empirer les choses - et aussi potentiellement coûteux en calculs.
Oh, et puisque nous ne multiplions pas les lignes dans items
maintenant, nous pouvons simplement utiliser la count(*)
moins chère au lieu de count(DISTINCT i.id)
. (id
étant NOT NULL PRIMARY KEY
.)
items.created
?Ça dépend. Pouvons-nous appliquer le même filtre à payments.created
Et extras.created
?
Si oui, ajoutez simplement les filtres dans les sous-requêtes également. (Cela ne semble pas probable dans ce cas.)
Si non, mais que nous sélectionnons toujours la plupart des éléments , la requête ci-dessus serait toujours plus efficace. Certaines des agrégations dans les sous-requêtes sont éliminées dans les jointures, mais cela reste moins cher que les requêtes plus complexes.
Si non, et que nous sélectionnons une petite fraction d'articles, je suggère des sous-requêtes corrélées ou LATERAL
jointures. Exemples: