Jumlah bergulir / jumlah / rata-rata selama interval tanggal

20

Dalam basis data transaksi yang mencakup 1.000 entitas selama 18 bulan, saya ingin menjalankan kueri untuk mengelompokkan setiap periode 30 hari yang mungkin entity_iddengan SUM dari jumlah transaksi mereka dan COUNT transaksi mereka dalam periode 30 hari itu, dan kembalikan data dengan cara yang kemudian bisa saya tanyakan. Setelah banyak pengujian, kode ini mencapai banyak hal yang saya inginkan:

SELECT id, trans_ref_no, amount, trans_date, entity_id,
    SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
    COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
  FROM transactiondb;

Dan saya akan menggunakan kueri terstruktur yang lebih besar seperti:

SELECT * FROM (
  SELECT id, trans_ref_no, amount, trans_date, entity_id,
      SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
      COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
    FROM transactiondb ) q
WHERE trans_count >= 4
AND trans_total >= 50000;

Kasus yang tidak dicakup oleh kueri ini adalah saat jumlah transaksi akan berlangsung beberapa bulan, namun masih dalam 30 hari satu sama lain. Apakah jenis permintaan ini memungkinkan dengan Postgres? Jika demikian, saya menerima masukan apa pun. Banyak topik lain membahas agregat " berjalan ", bukan bergulir .

Memperbarui

The CREATE TABLEScript:

CREATE TABLE transactiondb (
    id integer NOT NULL,
    trans_ref_no character varying(255),
    amount numeric(18,2),
    trans_date date,
    entity_id integer
);

Sampel data dapat ditemukan di sini . Saya menjalankan PostgreSQL 9.1.16.

Output ideal akan mencakup SUM(amount)dan COUNT()dari semua transaksi selama periode 30 hari bergulir. Lihat gambar ini, misalnya:

Contoh baris yang idealnya dimasukkan dalam "set" tetapi bukan karena set saya statis per bulan.

Sorotan tanggal hijau menunjukkan apa yang disertakan oleh kueri saya. Sorotan baris kuning menunjukkan catatan apa yang saya ingin menjadi bagian dari set.

Bacaan sebelumnya:

tufelkinder
sumber
1
Dengan every possible 30-day period by entity_idAnda berarti periode dapat memulai setiap hari, sehingga 365 periode mungkin dalam (non-lompatan) tahun? Atau apakah Anda hanya ingin mempertimbangkan hari-hari dengan transaksi aktual sebagai permulaan periode secara individual entity_id ? Either way, tolong berikan definisi tabel Anda, versi Postgres, beberapa data sampel dan hasil yang diharapkan untuk sampel.
Erwin Brandstetter
Secara teori, maksud saya setiap hari, tetapi dalam praktiknya tidak perlu mempertimbangkan hari-hari di mana tidak ada transaksi. Saya telah memposting data sampel dan definisi tabel.
tufelkinder
Jadi, Anda ingin mengakumulasi baris yang sama entity_iddalam 30 hari mulai dari setiap transaksi aktual. Mungkinkah ada beberapa transaksi untuk hal yang sama (trans_date, entity_id)atau kombinasi itu unik? Definisi tabel Anda tidak memiliki UNIQUEkendala PK atau apa pun, tetapi kendala tampaknya tidak ada ...
Erwin Brandstetter
Satu-satunya kendala adalah pada idkunci utama. Mungkin ada beberapa transaksi per entitas per hari.
tufelkinder
Tentang distribusi data: apakah ada entri (per entitas_id) untuk sebagian besar hari?
Erwin Brandstetter

Jawaban:

26

Kueri yang Anda miliki

Anda bisa menyederhanakan kueri menggunakan WINDOWklausa, tapi itu hanya memperpendek sintaks, bukan mengubah rencana kueri.

SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date)
             ORDER BY trans_date
             ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING);
  • Juga menggunakan sedikit lebih cepat count(*), karena idsudah pasti ditentukan NOT NULL?
  • Dan Anda tidak perlu ORDER BY entity_idsejak ituPARTITION BY entity_id

Anda dapat menyederhanakan lebih lanjut, meskipun:
Jangan menambahkan ORDER BYdefinisi jendela sama sekali, itu tidak relevan dengan permintaan Anda. Maka Anda tidak perlu mendefinisikan bingkai jendela khusus, baik:

SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date);

Lebih sederhana, lebih cepat, tetapi masih versi yang lebih baik dari apa yang Anda miliki , dengan bulan statis .

Permintaan yang Anda inginkan

... tidak didefinisikan dengan jelas, jadi saya akan membangun asumsi ini:

Hitung transaksi dan jumlah untuk setiap periode 30 hari dalam transaksi pertama dan terakhir dari setiap transaksi entity_id. Kecualikan periode awal dan akhir tanpa aktivitas, tetapi sertakan semua periode 30 hari yang mungkin dalam batas luar tersebut.

SELECT entity_id, trans_date
     , COALESCE(sum(daily_amount) OVER w, 0) AS trans_total
     , COALESCE(sum(daily_count)  OVER w, 0) AS trans_count
FROM  (
   SELECT entity_id
        , generate_series (min(trans_date)::timestamp
                         , GREATEST(min(trans_date), max(trans_date) - 29)::timestamp
                         , interval '1 day')::date AS trans_date
   FROM   transactiondb 
   GROUP  BY 1
   ) x
LEFT JOIN (
   SELECT entity_id, trans_date
        , sum(amount) AS daily_amount, count(*) AS daily_count
   FROM   transactiondb
   GROUP  BY 1, 2
   ) t USING (entity_id, trans_date)
WINDOW w AS (PARTITION BY entity_id ORDER BY trans_date
             ROWS BETWEEN CURRENT ROW AND 29 FOLLOWING);

Ini mencantumkan semua periode 30 hari untuk masing-masing entity_iddengan agregat Anda dan dengan trans_datemenjadi hari pertama (termasuk) periode tersebut. Untuk mendapatkan nilai untuk setiap baris, gabungkan ke tabel dasar sekali lagi ...

Kesulitan dasarnya sama dengan yang dibahas di sini:

Definisi bingkai dari suatu jendela tidak dapat bergantung pada nilai dari baris saat ini.

Sebaliknya, panggil generate_series()dengan timestampinput:

Permintaan yang sebenarnya Anda inginkan

Setelah pembaruan dan diskusi pertanyaan:
Akumulasi baris yang sama entity_iddalam jendela 30 hari mulai dari setiap transaksi aktual.

Karena data Anda didistribusikan secara jarang, seharusnya lebih efisien untuk menjalankan self-join dengan kondisi jangkauan , terlebih lagi karena Postgres 9.1 tidak memiliki LATERALsambungan, namun:

SELECT t0.id, t0.amount, t0.trans_date, t0.entity_id
     , sum(t1.amount) AS trans_total, count(*) AS trans_count
FROM   transactiondb t0
JOIN   transactiondb t1 USING (entity_id)
WHERE  t1.trans_date >= t0.trans_date
AND    t1.trans_date <  t0.trans_date + 30  -- exclude upper bound
-- AND    t0.entity_id = 114284  -- or pick a single entity ...
GROUP  BY t0.id  -- is PK!
ORDER  BY t0.trans_date, t0.id

SQL Fiddle.

Jendela bergulir hanya bisa masuk akal (berkenaan dengan kinerja) dengan data untuk sebagian besar hari.

Ini tidak mengagregasi duplikat (trans_date, entity_id)per hari, tetapi semua baris pada hari yang sama selalu dimasukkan dalam jendela 30 hari.

Untuk tabel besar, indeks penutup seperti ini bisa membantu sedikit:

CREATE INDEX transactiondb_foo_idx
ON transactiondb (entity_id, trans_date, amount);

Kolom terakhir amounthanya berguna jika Anda mendapatkan hanya pindaian indeks. Jatuhkan itu.

Tapi itu tidak akan digunakan saat Anda memilih seluruh tabel. Itu akan mendukung permintaan untuk subset kecil.

Erwin Brandstetter
sumber
Ini terlihat sangat bagus, mengujinya pada data sekarang, dan mencoba memahami semua yang sebenarnya dilakukan oleh kueri Anda
tufelkinder
@tufelkinder: Menambahkan solusi untuk pertanyaan yang diperbarui.
Erwin Brandstetter
Meninjau ulang sekarang. Saya tertarik bahwa ini berjalan dalam SQL Fiddle ... Ketika saya mencoba untuk menjalankannya langsung pada transactiondb saya, kesalahan dengancolumn "t0.amount" must appear in the GROUP BY clause...
tufelkinder
@ Tufelkinder: Saya memotong test case menjadi 100 baris. sqlfiddle membatasi ukuran data uji. Jake (penulis) mengurangi batas batas beberapa bulan yang lalu sehingga situs tersebut tidak mudah macet.
Erwin Brandstetter
1
Maaf atas keterlambatannya, diperlukan untuk mengujinya pada database lengkap. Jawaban Anda sangat mendalam dan mendidik, seperti biasa. Terima kasih!
tufelkinder