SOMMA su righe distinte con più join


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

Dati :

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

Quindi abbiamo:

  • 3 articoli in CZ in 1 in PL
  • 370 guadagnati in CZ e 25 in PL
  • 350 costo in CZ e 20 in PL
  • 11 extra guadagnati in CZ e 5 extra guadagnati in PL

Ora voglio ottenere risposte per le seguenti domande:

  1. Quanti articoli abbiamo avuto il mese scorso in ogni paese?
  2. Qual è stato l'importo totale guadagnato (somma dei pagamenti. Importi) in ogni paese?
  3. Qual è stato il costo totale (somma di articoli.prezzo) in ogni paese?
  4. Qual è stato il totale delle entrate extra (somma degli extra) in ogni paese?

Con la seguente query ( 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;

I risultati sono sbagliati:

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

I costi e gli extra appresi per CZ non sono validi - 450 invece di 350 e 16 invece di 11. Anche i costi e i guadagni per PL non sono validi - sono raddoppiati.

Capisco che in caso LEFT OUTER JOINci siano 2 righe per l'articolo con items.id = 1 (e così via per altre corrispondenze), ma non so come creare una query corretta.

Domande :

  1. Come evitare risultati errati nell'aggregazione nelle query su più tabelle?
  2. Qual è il modo migliore per calcolare la somma su valori distinti (items.id in quel caso)?

Versione PostgreSQL : 9.6.1


Vedi l'opzione 3 nella mia risposta qui: dba.stackexchange.com/questions/17012/help-with-this-query/… Puoi anche fare l'opzione 4 riscrivendo OUTER APPLYe usando LATERALinvece i join.
ypercubeᵀᴹ

L'opzione 3 funzionerà ma in tal caso richiederà Seq Scansui pagamenti, il che significa che la statistica verrà ricalcolata su tutti gli articoli. Non ho menzionato questo nella domanda, ma voglio filtrare gli elementi anche al momento della creazione, quindi avrò bisogno solo di un sottoinsieme specifico dei dati aggregati. Aggiornerò la domanda
Stranger6667

È possibile aggiungere WHEREclausole o join nelle sottoquery. Ma controlla anche l'opzione 4, usando LATERAL.
ypercubeᵀᴹ

Vuoi dire ISCRIVITI paymentse itemsin subquery e aggiungerlo WHERE ? Dovrò confrontare tutte le opzioni :)
Stranger6667

Se si desidera limitare il sottoinsieme in base a items.created_at, sì.
ypercubeᵀᴹ

Risposte:


9

Dal momento che possono esserci più paymentse più extrasper item, si verifica un "join incrociato proxy" tra queste due tabelle. Righe aggregate per item_id prima di unirsi a iteme dovrebbe essere tutto corretto:

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;

Considera l'esempio del "mercato del pesce":

Per essere precisi, SUM(i.price)sarebbe errato dopo aver aderito a una singola n-table, che moltiplica ogni prezzo per il numero di righe correlate. Farlo due volte non fa che peggiorare la situazione - e anche potenzialmente computazionalmente costoso.

Oh, e dal momento che non moltiplichiamo le righe itemsora, possiamo semplicemente usare il più economico count(*)invece di count(DISTINCT i.id). ( idessere NOT NULL PRIMARY KEY.)

SQL Fiddle.

Ma se voglio filtrare per items.created?

Affrontare il tuo commento.

Dipende. Possiamo applicare lo stesso filtro a payments.createde extras.created?

Se sì, aggiungi anche i filtri anche nelle sottoquery. (Non sembra probabile in questo caso.)

In caso contrario, ma stiamo ancora selezionando la maggior parte degli elementi , la query sopra sarebbe ancora più efficiente. Alcune aggregazioni nelle sottoquery vengono eliminate nei join, ma è comunque più economico delle query più complesse.

In caso negativo, e stiamo selezionando una piccola parte di articoli, suggerisco sottoquery o LATERALjoin correlati . Esempi:


Grazie per la risposta! Ma se voglio filtrare in base a items.createdqual è il modo più efficiente per farlo? Dovrei aggiungere il supplemento JOINsu itemsdi subquery ( pe enel tuo esempio) di effettuare tale filtrazione come @ ypercubeᵀᴹ menzionato?
Stranger6667,

@ Stranger6667: dipende. Ed è una domanda diversa, davvero. Ho aggiunto una risposta sopra.
Erwin Brandstetter,

LATERAL JOINper me va bene! Grazie per la chiara spiegazione :)
Stranger6667
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.