SUM di atas baris berbeda dengan banyak gabungan

10

Skema :

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

Data :

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

Jadi kita punya:

  • 3 item dalam CZ dalam 1 dalam PL
  • 370 diperoleh di CZ dan 25 di PL
  • 350 biaya dalam CZ dan 20 dalam PL
  • 11 ekstra diperoleh di CZ dan 5 ekstra di PL

Sekarang saya ingin mendapatkan jawaban untuk pertanyaan-pertanyaan berikut:

  1. Berapa banyak barang yang kami miliki bulan lalu di setiap negara?
  2. Berapa jumlah total yang diterima (jumlah pembayaran. Jumlah) di setiap negara?
  3. Berapa total biaya (jumlah barang.harga) di setiap negara?
  4. Berapa total penghasilan tambahan (jumlah extras.amount) di setiap negara?

Dengan kueri berikut ( 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;

Hasilnya salah:

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

Biaya dan extra_earned untuk CZ tidak valid - 450 bukannya 350 dan 16 bukannya 11. Biaya dan diperoleh untuk PL juga tidak valid - mereka digandakan.

Saya mengerti, bahwa dalam kasus LEFT OUTER JOINakan ada 2 baris untuk item dengan items.id = 1 (dan seterusnya untuk pertandingan lainnya), tetapi saya tidak tahu bagaimana membangun kueri yang tepat.

Pertanyaan :

  1. Bagaimana menghindari hasil yang salah dalam agregasi dalam kueri di beberapa tabel?
  2. Apa cara terbaik untuk menghitung penjumlahan dari nilai yang berbeda (items.id dalam kasus itu)?

Versi PostgreSQL : 9.6.1

Stranger6667
sumber
Lihat opsi 3 dalam jawaban saya di sini: dba.stackexchange.com/questions/17012/help-with-this-query/… Anda juga bisa melakukan opsi 4 dengan menulis ulang OUTER APPLYdan menggunakan LATERALgabungan.
ypercubeᵀᴹ
Opsi 3 akan berfungsi tetapi dalam hal itu akan membutuhkan Seq Scanpembayaran, yang berarti bahwa statistik akan dihitung ulang pada semua item. Saya tidak menyebutkan ini dalam pertanyaan, tetapi saya juga ingin memfilter item berdasarkan waktu pembuatan, jadi saya hanya perlu subkumpulan data agregat tertentu. Saya akan memperbarui pertanyaan
Stranger6667
Anda bisa menambahkan WHEREklausa atau bergabung di subqueries. Tetapi periksa opsi 4 juga, menggunakan LATERAL.
ypercubeᵀᴹ
Apakah Anda bermaksud BERGABUNG paymentsdan itemsdalam subquery dan menambahkannya WHERE ? Saya harus membandingkan semua opsi :)
Stranger6667
Jika Anda ingin membatasi subset berdasarkan items.created_at, ya.
ypercubeᵀᴹ

Jawaban:

9

Karena bisa ada beberapa paymentsdan beberapa extrasper item, Anda mengalami "gabungan lintas proxy" antara dua tabel tersebut. Baris agregat per item_id sebelum bergabung ke itemdan semuanya harus benar:

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;

Perhatikan contoh "pasar ikan":

Tepatnya, SUM(i.price)akan salah setelah bergabung ke n-tabel tunggal, yang mengalikan setiap harga dengan jumlah baris terkait. Melakukannya dua kali hanya memperburuknya - dan juga berpotensi mahal secara komputasi.

Oh, dan karena kita tidak menggandakan baris itemssekarang, kita bisa menggunakan yang lebih murah count(*)daripada count(DISTINCT i.id). ( idsedang NOT NULL PRIMARY KEY.)

SQL Fiddle.

Tetapi jika saya ingin memfilter items.created?

Mengatasi komentar Anda.

Tergantung. Bisakah kita menerapkan filter yang sama ke payments.createddan extras.created?

Jika ya, tambahkan filter di subqueries juga. (Sepertinya tidak mungkin dalam kasus ini.)

Jika tidak, tetapi kami masih memilih sebagian besar item , kueri di atas masih tetap paling efisien. Beberapa agregasi dalam subkueri dihilangkan dalam gabungan, tapi itu masih lebih murah daripada kueri yang lebih kompleks.

Jika tidak, dan kami memilih sebagian kecil item, saya sarankan subqueries yang berkorelasi atau LATERALbergabung. Contoh:

Erwin Brandstetter
sumber
Terima kasih atas jawabannya! Tetapi jika saya ingin memfilter dengan cara items.createdapa yang paling efisien untuk melakukan ini? Apakah saya harus menambahkan ekstra JOINpada itemske subqueries ( pdan edalam contoh Anda) untuk melakukan filtrasi seperti @ ypercubeᵀᴹ disebutkan?
Stranger6667
@ Stranger6667: Tergantung. Dan itu pertanyaan yang berbeda, sungguh. Saya menambahkan jawaban di atas.
Erwin Brandstetter
LATERAL JOINbekerja untukku! Terima kasih atas penjelasan bersihnya :)
Stranger6667