SUM sur des lignes distinctes avec plusieurs jointures

10

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:

  1. Combien d'articles nous avions le mois dernier dans chaque pays?
  2. Quel était le montant total gagné (somme des paiements, montants) dans chaque pays?
  3. Quel était le coût total (somme des articles.prix) dans chaque pays?
  4. 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 JOINil 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 :

  1. Comment éviter les mauvais résultats d'agrégation dans les requêtes sur plusieurs tables?
  2. Quelle est la meilleure façon de calculer la somme sur des valeurs distinctes (items.id dans ce cas)?

Version PostgreSQL : 9.6.1

Stranger6667
la source
Voir l'option 3 dans ma réponse ici: dba.stackexchange.com/questions/17012/help-with-this-query/… Vous pouvez également faire l'option 4 en réécrivant les OUTER APPLYet en utilisant des LATERALjointures à la place.
ypercubeᵀᴹ
L'option 3 fonctionnera mais dans ce cas, elle nécessitera des Seq Scanpaiements, 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 question
Stranger6667
Vous pouvez ajouter des WHEREclauses ou des jointures dans les sous-requêtes. Mais cochez également l'option 4 en utilisant LATERAL.
ypercubeᵀᴹ
Voulez-vous vous joindre à paymentset itemsdans la sous-requête et y ajouter WHERE? Je devrai comparer toutes les options :)
Stranger6667
Si vous souhaitez restreindre le sous-ensemble en fonction de items.created_at, oui.
ypercubeᵀᴹ

Réponses:

9

Puisqu'il peut y avoir plusieurs paymentset plusieurs extraspar item, vous rencontrez une "jointure proxy" entre ces deux tables. Agréger les lignes par item_id avant de rejoindre itemet tout doit ê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 itemsmaintenant, nous pouvons simplement utiliser le moins cher count(*)au lieu de count(DISTINCT i.id). ( idêtre NOT 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.createdet 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 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 LATERALjointures corrélées . Exemples:

Erwin Brandstetter
la source
Merci pour la réponse! Mais si je veux filtrer par items.createdquel est le moyen le plus efficace de le faire? Dois - je ajouter de plus JOINsur itemsla sous - requêtes ( pet edans votre exemple) pour effectuer cette filtration @ ypercubeᵀᴹ mentionné?
Stranger6667
@ Stranger6667: Cela dépend. Et c'est vraiment une question différente. J'ai ajouté une réponse ci-dessus.
Erwin Brandstetter
LATERAL JOINtravaille pour moi! Merci pour l'explication claire :)
Stranger6667