Kesulitan khusus tugas ini:Anda tidak bisa hanya memilih titik data dalam rentang waktu Anda, tetapi harus mempertimbangkan terbaru titik data sebelum rentang waktu dan paling awal titik data setelah rentang waktu tambahan. Ini bervariasi untuk setiap baris dan setiap titik data mungkin ada atau tidak ada. Memerlukan kueri yang canggih dan mempersulit penggunaan indeks.
Anda dapat menggunakan jenis rentang dan operator (Postgres 9.2+ ) untuk menyederhanakan perhitungan:
WITH input(a,b) AS (SELECT '2013-01-01'::date -- your time frame here
, '2013-01-15'::date) -- inclusive borders
SELECT store_id, product_id
, sum(upper(days) - lower(days)) AS days_in_range
, round(sum(value * (upper(days) - lower(days)))::numeric
/ (SELECT b-a+1 FROM input), 2) AS your_result
, round(sum(value * (upper(days) - lower(days)))::numeric
/ sum(upper(days) - lower(days)), 2) AS my_result
FROM (
SELECT store_id, product_id, value, s.day_range * x.day_range AS days
FROM (
SELECT store_id, product_id, value
, daterange (day, lead(day, 1, now()::date)
OVER (PARTITION BY store_id, product_id ORDER BY day)) AS day_range
FROM stock
) s
JOIN (
SELECT daterange(a, b+1) AS day_range
FROM input
) x ON s.day_range && x.day_range
) sub
GROUP BY 1,2
ORDER BY 1,2;
Catatan, saya menggunakan nama kolom day
bukannya date
. Saya tidak pernah menggunakan nama tipe dasar sebagai nama kolom.
Di subkueri sub
Saya mengambil hari dari baris berikutnya untuk setiap item dengan fungsi jendela lead()
, menggunakan opsi bawaan untuk memberikan "hari ini" sebagai default di mana tidak ada baris berikutnya.
Dengan ini saya membentuk daterange
dan cocokkan dengan input dengan operator tumpang tindih &&
, menghitung rentang tanggal yang dihasilkan dengan operator persimpangan *
.
Semua rentang di sini dengan eksklusif batas atas. Itu sebabnya saya menambahkan satu hari ke rentang input. Dengan cara ini kita cukup mengurangi lower(range)
dari upper(range)
untuk mendapatkan jumlah hari.
Saya berasumsi bahwa "kemarin" adalah hari terakhir dengan data yang dapat diandalkan. "Hari ini" masih bisa berubah dalam aplikasi kehidupan nyata. Akibatnya, saya menggunakan "hari ini" (now()::date
) sebagai batas atas eksklusif untuk rentang terbuka.
Saya memberikan dua hasil:
-
your_result
setuju dengan hasil yang ditampilkan.
Anda membagi dengan jumlah hari dalam rentang tanggal tanpa syarat. Misalnya, jika item hanya terdaftar untuk hari terakhir, Anda mendapatkan "rata-rata" yang sangat rendah (menyesatkan!). -
my_result
menghitung angka yang sama atau lebih tinggi.
Saya bagi dengan sebenarnya jumlah hari item terdaftar. Misalnya, jika item hanya terdaftar untuk hari terakhir, saya mengembalikan nilai yang terdaftar sebagai rata-rata.
Untuk memahami perbedaannya, saya menambahkan jumlah hari item tersebut terdaftar:days_in_range
Indeks dan kinerja
Untuk jenis data ini, baris lama biasanya tidak berubah. Ini akan menjadi kasus yang sangat baik untuk tampilan yang terwujud :
CREATE MATERIALIZED VIEW mv_stock AS
SELECT store_id, product_id, value
, daterange (day, lead(day, 1, now()::date) OVER (PARTITION BY store_id, product_id
ORDER BY day)) AS day_range
FROM stock;
Kemudian Anda dapat menambahkan Indeks Intisari yang mendukung operator terkait &&
:
CREATE INDEX mv_stock_range_idx ON mv_stock USING gist (day_range);
Kasus uji besar
Saya menjalankan tes yang lebih realistis dengan 200 ribu baris. Kueri yang menggunakan MV sekitar 6 kali lebih cepat, yang pada gilirannya ~ 10x lebih cepat dari kueri @Joop. Kinerja sangat tergantung pada distribusi data. MV paling membantu dengan tabel besar dan frekuensi entri yang tinggi. Juga, jika tabel memiliki kolom yang tidak relevan dengan kueri ini, MV bisa lebih kecil. Pertanyaan tentang biaya vs. keuntungan.
Saya telah menempatkan semua solusi yang diposting sejauh ini (dan diadaptasi) ke dalam biola besar untuk dimainkan:
SQL Fiddle dengan kasus uji besar.
SQL Fiddle hanya dengan 40k baris
- untuk menghindari batas waktu di sqlfiddle.com