web-dev-qa-db-fra.com

SUM sur des lignes distinctes avec plusieurs jointures

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 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 :

  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

10
Stranger6667

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.)

SQL Fiddle.

Mais si je veux filtrer par items.created?

Adresse de votre commentaire.

Ç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:

9