Cara tercepat untuk menghitung berapa banyak rentang tanggal mencakup setiap tanggal dari seri

12

Saya punya tabel (dalam PostgreSQL 9.4) yang terlihat seperti ini:

CREATE TABLE dates_ranges (kind int, start_date date, end_date date);
INSERT INTO dates_ranges VALUES 
    (1, '2018-01-01', '2018-01-31'),
    (1, '2018-01-01', '2018-01-05'),
    (1, '2018-01-03', '2018-01-06'),
    (2, '2018-01-01', '2018-01-01'),
    (2, '2018-01-01', '2018-01-02'),
    (3, '2018-01-02', '2018-01-08'),
    (3, '2018-01-05', '2018-01-10');

Sekarang saya ingin menghitung untuk tanggal yang diberikan dan untuk setiap jenis, ke berapa banyak baris dari dates_rangessetiap tanggal jatuh. Nol bisa dihilangkan.

Hasil yang diinginkan:

+-------+------------+----+
|  kind | as_of_date |  n |
+-------+------------+----+
|     1 | 2018-01-01 |  2 |
|     1 | 2018-01-02 |  2 |
|     1 | 2018-01-03 |  3 |
|     2 | 2018-01-01 |  2 |
|     2 | 2018-01-02 |  1 |
|     3 | 2018-01-02 |  1 |
|     3 | 2018-01-03 |  1 |
+-------+------------+----+

Saya telah datang dengan dua solusi, satu dengan LEFT JOINdanGROUP BY

SELECT
kind, as_of_date, COUNT(*) n
FROM
    (SELECT d::date AS as_of_date FROM generate_series('2018-01-01'::timestamp, '2018-01-03'::timestamp, '1 day') d) dates
LEFT JOIN
    dates_ranges ON dates.as_of_date BETWEEN start_date AND end_date
GROUP BY 1,2 ORDER BY 1,2

dan satu dengan LATERAL, yang sedikit lebih cepat:

SELECT
    kind, as_of_date, n
FROM
    (SELECT d::date AS as_of_date FROM generate_series('2018-01-01'::timestamp, '2018-01-03'::timestamp, '1 day') d) dates,
LATERAL
    (SELECT kind, COUNT(*) AS n FROM dates_ranges WHERE dates.as_of_date BETWEEN start_date AND end_date GROUP BY kind) ss
ORDER BY kind, as_of_date

Saya bertanya-tanya apakah ini cara yang lebih baik untuk menulis pertanyaan ini? Dan bagaimana cara memasukkan pasangan jenis tanggal dengan 0 hitungan?

Pada kenyataannya ada beberapa jenis yang berbeda, periode hingga lima tahun (1800 tanggal), dan ~ 30rb baris dalam dates_rangestabel (tetapi bisa tumbuh secara signifikan).

Tidak ada indeks. Lebih tepatnya dalam kasus saya ini adalah hasil dari subquery, tapi saya ingin membatasi pertanyaan menjadi satu masalah, jadi lebih umum.

BartekCh
sumber
Apa yang Anda lakukan jika rentang dalam tabel tidak tumpang tindih atau menyentuh. Misalnya jika Anda memiliki rentang di mana (jenis, mulai, akhir) = (1,2018-01-01,2018-01-15)dan (1,2018-01-20,2018-01-25)apakah Anda ingin memperhitungkannya ketika menentukan berapa banyak tanggal yang tumpang tindih yang Anda miliki?
Evan Carroll
Saya juga bingung mengapa meja Anda kecil? Mengapa tidak 2018-01-31atau 2018-01-30atau 2018-01-29di dalamnya ketika kisaran pertama memiliki semua dari mereka?
Evan Carroll
@ EvanCarroll tanggal di generate_seriesadalah parameter eksternal - mereka tidak harus mencakup semua rentang dalam dates_rangestabel. Adapun pertanyaan pertama saya kira saya tidak memahaminya - baris dates_rangesindependen, saya tidak ingin menentukan tumpang tindih.
BartekCh

Jawaban:

4

Kueri berikut juga berfungsi jika "nol yang hilang" tidak apa-apa:

select *
from (
  select
    kind,
    generate_series(start_date, end_date, interval '1 day')::date as d,
    count(*)
  from dates_ranges
  group by 1, 2
) x
where d between date '2018-01-01' and date '2018-01-03'
order by 1, 2;

tapi itu tidak lebih cepat dari lateralversi dengan dataset kecil. Mungkin skala lebih baik, karena tidak ada gabungan yang diperlukan, tetapi versi di atas mengagregasi semua baris, sehingga mungkin kehilangan di sana lagi.

Kueri berikut mencoba menghindari pekerjaan yang tidak perlu dengan menghapus seri apa pun yang tidak tumpang tindih:

select
  kind,
  generate_series(greatest(start_date, date '2018-01-01'), least(end_date, date '2018-01-03'), interval '1 day')::date as d,
  count(*)
from dates_ranges
where (start_date, end_date + interval '1 day') overlaps (date '2018-01-01', date '2018-01-03' + interval '1 day')
group by 1, 2
order by 1, 2;

- dan saya harus menggunakan overlapsoperator! Perhatikan bahwa Anda harus menambahkan interval '1 day'ke kanan karena operator yang tumpang tindih menganggap periode waktu terbuka di sebelah kanan (yang cukup logis karena tanggal sering dianggap timestamp dengan komponen waktu tengah malam).

Colin 't Hart
sumber
Bagus, saya tidak tahu generate_seriesbisa digunakan seperti itu. Setelah beberapa tes saya mengikuti pengamatan. Kueri Anda benar-benar berskala sangat baik dengan panjang rentang yang dipilih - karena pada dasarnya tidak ada perbedaan antara periode 3 tahun dan 10 tahun. Namun untuk periode yang lebih singkat (1 tahun) solusi saya lebih cepat - saya menduga alasannya adalah ada beberapa rentang yang sangat panjang di dates_ranges(seperti 2010-2100), yang memperlambat permintaan Anda. Membatasi start_datedan end_datedi dalam kueri batin seharusnya membantu. Saya perlu melakukan beberapa tes lagi.
BartekCh
6

Dan bagaimana cara memasukkan pasangan jenis tanggal dengan 0 hitungan?

Bangun kisi-kisi semua kombinasi lalu LATERAL gabung ke meja Anda, seperti ini:

SELECT k.kind, d.as_of_date, c.n
FROM  (SELECT DISTINCT kind FROM dates_ranges) k
CROSS  JOIN (
   SELECT d::date AS as_of_date
   FROM   generate_series(timestamp '2018-01-01', timestamp '2018-01-03', interval '1 day') d
   ) d
CROSS  JOIN LATERAL (
   SELECT count(*)::int AS n
   FROM   dates_ranges
   WHERE  kind = k.kind
   AND    d.as_of_date BETWEEN start_date AND end_date
   ) c
ORDER  BY k.kind, d.as_of_date;

Seharusnya juga secepat mungkin.

LEFT JOIN LATERAL ... on trueAwalnya saya punya , tapi ada agregat di subquery c, jadi kami selalu mendapatkan baris dan bisa menggunakannya CROSS JOINjuga. Tidak ada perbedaan dalam kinerja.

Jika Anda memiliki tabel yang menyimpan semua jenis yang relevan , gunakan itu alih-alih menghasilkan daftar dengan subquery k.

Para pemain integeradalah opsional. Anda dapatkan bigint.

Indeks akan membantu, terutama indeks multikolom aktif (kind, start_date, end_date). Karena Anda membangun subquery, ini mungkin atau mungkin tidak mungkin dicapai.

Menggunakan fungsi set-return seperti generate_series()dalam SELECTdaftar pada umumnya tidak disarankan dalam versi Postgres sebelum 10 (kecuali Anda tahu persis apa yang Anda lakukan). Lihat:

Jika Anda memiliki banyak kombinasi dengan sedikit atau tanpa baris, bentuk yang setara ini mungkin lebih cepat:

SELECT k.kind, d.as_of_date, count(dr.kind)::int AS n
FROM  (SELECT DISTINCT kind FROM dates_ranges) k
CROSS JOIN (
   SELECT d::date AS as_of_date
   FROM   generate_series(timestamp '2018-01-01', timestamp '2018-01-03', interval '1 day') d
   ) d
LEFT   JOIN dates_ranges dr ON dr.kind = k.kind
                           AND d.as_of_date BETWEEN dr.start_date AND dr.end_date
GROUP  BY 1, 2
ORDER  BY 1, 2;
Erwin Brandstetter
sumber
Adapun fungsi set-return dalam SELECTdaftar - Saya pernah membaca bahwa itu tidak disarankan, namun sepertinya berfungsi dengan baik, jika hanya ada satu fungsi seperti itu. Jika saya yakin hanya akan ada satu, dapatkah terjadi kesalahan?
BartekCh
@ BartekCh: Satu SRF dalam SELECTdaftar berfungsi seperti yang diharapkan. Mungkin menambahkan komentar untuk memperingatkan agar tidak menambahkan yang lain. Atau pindahkan ke FROMdaftar untuk memulai dengan Postgres versi lama. Mengapa perlu risiko komplikasi? (Itu juga SQL standar dan tidak akan membingungkan orang yang datang dari RDBMS lain.)
Erwin Brandstetter
1

Menggunakan daterangetipe

PostgreSQL memiliki daterange. Menggunakannya cukup sederhana. Dimulai dengan data sampel Anda, kami bergerak untuk menggunakan tipe di atas meja.

BEGIN;
  ALTER TABLE dates_ranges ADD COLUMN myrange daterange;
  UPDATE dates_ranges
    SET myrange = daterange(start_date, end_date, '[]');
  ALTER TABLE dates_ranges
    DROP COLUMN start_date,
    DROP COLUMN end_date;
COMMIT;

-- Now you can create GIST index on it...
CREATE INDEX ON dates_ranges USING gist (myrange);

TABLE dates_ranges;
 kind |         myrange         
------+-------------------------
    1 | [2018-01-01,2018-02-01)
    1 | [2018-01-01,2018-01-06)
    1 | [2018-01-03,2018-01-07)
    2 | [2018-01-01,2018-01-02)
    2 | [2018-01-01,2018-01-03)
    3 | [2018-01-02,2018-01-09)
    3 | [2018-01-05,2018-01-11)
(7 rows)

Saya ingin menghitung untuk tanggal yang diberikan dan untuk setiap jenis, ke berapa banyak baris dari tanggal_setiap tanggal jatuh.

Sekarang untuk query kita membalikkan prosedur, dan menghasilkan seri tanggal tapi di sini menangkap permintaan itu sendiri dapat menggunakan @>operator penahanan ( ) untuk memeriksa bahwa tanggal dalam jangkauan, menggunakan indeks.

Catatan kami menggunakan timestamp without time zone(untuk menghentikan bahaya DST)

SELECT d1.kind, day::date, count(d2.kind)
FROM dates_ranges AS d1
CROSS JOIN LATERAL generate_series(
  lower(myrange)::timestamp without time zone,
  upper(myrange)::timestamp without time zone,
  '1 day'
) AS gs(day)
INNER JOIN dates_ranges AS d2
  ON d2.myrange @> day::date
GROUP BY d1.kind, day;

Yang merupakan daftar tumpang tindih hari pada indeks.

Sebagai bonus tambahan, dengan tipe daterange Anda dapat menghentikan penyisipan rentang yang tumpang tindih dengan yang lain menggunakanEXCLUDE CONSTRAINT

Evan Carroll
sumber
Ada yang salah dengan kueri Anda, sepertinya ia menghitung baris beberapa kali, JOINterlalu banyak kurasa.
BartekCh
@BartekCh tidak ada Anda memiliki baris yang tumpang tindih, Anda dapat menyiasatinya dengan menghapus rentang yang tumpang tindih (disarankan) atau menggunakancount(DISTINCT kind)
Evan Carroll
tapi saya ingin baris yang tumpang tindih. Misalnya untuk 1tanggal jenis 2018-01-01adalah dalam dua baris pertama dari dates_ranges, tetapi kueri Anda memberikan 8.
BartekCh
atau menggunakancount(DISTINCT kind) apakah Anda menambahkan DISTINCTkata kunci di sana?
Evan Carroll
Sayangnya dengan DISTINCTkata kunci itu masih tidak berfungsi seperti yang diharapkan. Itu menghitung jenis yang berbeda untuk setiap tanggal, tetapi saya ingin menghitung semua baris dari setiap jenis untuk setiap tanggal.
BartekCh