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:
- 3 articles en CZ en 1 en PL
- 370 gagnés en CZ et 25 en PL
- 350 en CZ et 20 en PL
- 11 supplémentaires gagnés en CZ et 5 supplémentaires gagnés en PL
Maintenant, je veux obtenir des réponses aux questions suivantes:
- Combien d'articles nous avions le mois dernier dans chaque pays?
- Quel était le montant total gagné (somme des paiements, montants) dans chaque pays?
- Quel était le coût total (somme des articles.prix) dans chaque pays?
- Quel a été le total des gains supplémentaires (somme des extras.montant) dans chaque pays?
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 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 construire une requête appropriée.
Questions :
- Comment éviter les mauvais résultats d'agrégation dans les requêtes sur plusieurs tables?
- Quelle est la meilleure façon de calculer la somme sur des valeurs distinctes (items.id dans ce cas)?
Version PostgreSQL : 9.6.1
postgresql
join
aggregate
Stranger6667
la source
la source
OUTER APPLY
et en utilisant desLATERAL
jointures à la place.Seq Scan
paiements, ce qui signifie que les statistiques seront recalculées sur tous les articles. Je ne l'ai pas mentionné dans la question, mais je veux également filtrer les éléments par heure de création, donc je n'aurai besoin que d'un sous-ensemble spécifique des données agrégées. Je mettrai à jour la questionWHERE
clauses ou des jointures dans les sous-requêtes. Mais cochez également l'option 4 en utilisantLATERAL
.payments
etitems
dans la sous-requête et y ajouterWHERE
? Je devrai comparer toutes les options :)items.created_at
, oui.Réponses:
Puisqu'il peut y avoir plusieurs
payments
et plusieursextras
paritem
, vous rencontrez une "jointure proxy" entre ces deux tables. Agréger les lignes paritem_id
avant de rejoindreitem
et tout doit être correct: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
items
maintenant, nous pouvons simplement utiliser le moins chercount(*)
au lieu decount(DISTINCT i.id)
. (id
êtreNOT NULL PRIMARY KEY
.)SQL Fiddle.
Mais si je veux filtrer
items.created
?Répondre à votre commentaire.
Ça dépend. Pouvons-nous appliquer le même filtre à
payments.created
etextras.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 la 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 nous sélectionnons une petite fraction des éléments, je suggère des sous-requêtes ou
LATERAL
jointures corrélées . Exemples:la source
items.created
quel est le moyen le plus efficace de le faire? Dois - je ajouter de plusJOIN
suritems
la sous - requêtes (p
ete
dans votre exemple) pour effectuer cette filtration @ ypercubeᵀᴹ mentionné?LATERAL JOIN
travaille pour moi! Merci pour l'explication claire :)