SUMME über verschiedene Zeilen mit mehreren Verknüpfungen

10

Schema :

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

Daten :

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

Also haben wir:

  • 3 Artikel in CZ in 1 in PL
  • 370 in CZ und 25 in PL verdient
  • 350 Kosten in CZ und 20 in PL
  • 11 extra verdient in CZ und 5 extra verdient in PL

Jetzt möchte ich Antworten auf folgende Fragen erhalten:

  1. Wie viele Artikel hatten wir letzten Monat in jedem Land?
  2. Was war der insgesamt verdiente Betrag (Summe der Zahlungen. Beträge) in jedem Land?
  3. Was waren die Gesamtkosten (Summe der Artikel.Preis) in jedem Land?
  4. Wie hoch war der zusätzliche Gesamtverdienst (Summe der Extras) in jedem Land?

Mit der folgenden Abfrage ( 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;

Die Ergebnisse sind falsch:

 group_by | item_count |  cost  | earned | extra_earned
----------+------------+--------+--------+--------------
 CZ       |          3 | 450.00 | 370.00 |        16.00
 PL       |          1 |  40.00 |  50.00 |         5.00

Kosten und extra_earned für CZ sind ungültig - 450 statt 350 und 16 statt 11. Kosten und verdient für PL sind ebenfalls ungültig - sie werden verdoppelt.

Ich verstehe, dass es im Fall von LEFT OUTER JOIN2 Zeilen für Artikel mit items.id = 1 gibt (und so weiter für andere Übereinstimmungen), aber ich weiß nicht, wie man eine richtige Abfrage erstellt.

Fragen :

  1. Wie vermeide ich falsche Ergebnisse bei der Aggregation in Abfragen in mehreren Tabellen?
  2. Was ist der beste Weg, um die Summe über verschiedene Werte zu berechnen (in diesem Fall items.id)?

PostgreSQL-Version : 9.6.1

Fremder6667
quelle
Siehe Option 3 in meiner Antwort hier: dba.stackexchange.com/questions/17012/help-with-this-query/… Sie können auch Option 4 ausführen, indem Sie die Joins neu schreiben OUTER APPLYund stattdessen LATERALJoins verwenden.
Ypercubeᵀᴹ
Option 3 funktioniert, in diesem Fall sind jedoch Seq ScanZahlungen erforderlich , was bedeutet, dass die Statistik für alle Elemente neu berechnet wird. Ich habe dies in der Frage nicht erwähnt, aber ich möchte Elemente auch nach Erstellungszeit filtern, sodass ich nur eine bestimmte Teilmenge der aggregierten Daten benötige. Ich werde die Frage aktualisieren
Stranger6667
Sie können WHEREden Unterabfragen Klauseln oder Verknüpfungen hinzufügen. Aktivieren Sie aber auch Option 4 mit LATERAL.
Ypercubeᵀᴹ
Wollen Sie damit JOIN paymentsund itemsin Unterabfrage , und fügen Sie WHERE es? Ich muss alle Optionen vergleichen :)
Stranger6667
Wenn Sie die Teilmenge basierend auf einschränken möchten items.created_at, ja.
Ypercubeᵀᴹ

Antworten:

9

Da es mehrere paymentsund mehrere extraspro geben kann, stoßen itemSie auf einen "Proxy-Cross-Join" zwischen diesen beiden Tabellen. Aggregieren Sie die Zeilen item_id vor dem Beitritt zu itemund es sollte alles korrekt sein:

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;

Betrachten Sie das Beispiel "Fischmarkt":

Um genau zu sein, SUM(i.price)wäre es falsch , wenn man sich einer einzelnen n-Tabelle anschließt, die jeden Preis mit der Anzahl der zugehörigen Zeilen multipliziert. Wenn Sie es zweimal machen, wird es nur noch schlimmer - und möglicherweise auch rechenintensiv.

Oh, und da wir jetzt keine Zeilen multiplizieren items, können wir count(*)stattdessen einfach die billigeren verwenden count(DISTINCT i.id). ( idSein NOT NULL PRIMARY KEY.)

SQL Fiddle.

Aber wenn ich filtern will items.created?

Adressierung Ihres Kommentars.

Es hängt davon ab, ob. Können wir den gleichen Filter auf payments.createdund anwenden extras.created?

Wenn ja, fügen Sie einfach die Filter auch in die Unterabfragen ein. (Scheint in diesem Fall nicht wahrscheinlich.)

Wenn nein, aber wir wählen immer noch die meisten Elemente aus , wäre die obige Abfrage immer noch am effizientesten. Einige der Aggregationen in den Unterabfragen werden in den Joins entfernt, aber das ist immer noch billiger als komplexere Abfragen.

Wenn nein, und wir einen kleinen Teil der Elemente auswählen , schlage ich korrelierte Unterabfragen oder Verknüpfungen vor LATERAL. Beispiele:

Erwin Brandstetter
quelle
Danke für die Antwort! Aber wenn ich filtern möchte, items.createdwie geht das am effizientesten? Soll ich hinzufügen , zusätzliche JOINauf itemsUnterabfragen ( pund ein Ihrem Beispiel) , wie Filtration , wie ausführen @ ypercubeᵀᴹ erwähnt?
Stranger6667
@ Stranger6667: Es kommt darauf an. Und es ist wirklich eine andere Frage. Ich habe oben eine Antwort hinzugefügt.
Erwin Brandstetter
LATERAL JOINfunktioniert bei mir! Vielen Dank für die saubere Erklärung :)
Stranger6667