Serilerden her bir tarihi kaç tarih aralığının kapsadığını saymanın en hızlı yolu


12

Aşağıdaki gibi bir tablo (PostgreSQL 9.4'te) var:

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

Şimdi verilen tarihler ve her tür için, dates_rangesher bir tarihten kaç satır düştüğünü hesaplamak istiyorum . Sıfırlar atlanabilir.

İstenen sonuç:

+-------+------------+----+
|  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 |
+-------+------------+----+

İki çözüm buldum, biri LEFT JOINveGROUP 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

ve biri ile LATERAL, biraz daha hızlı:

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

Bu sorguyu yazmak için daha iyi bir yol olduğunu merak ediyorum? Ve nasıl 0 sayısı ile tarih-tür çiftleri dahil etmek?

Gerçekte birkaç farklı tür, beş yıla kadar süre (1800 tarih) ve dates_rangestabloda ~ 30 bin satır vardır (ancak önemli ölçüde büyüyebilir).

Hiçbir dizin yok. Benim durumumda kesin olmak gerekirse, bu alt sorgulamanın bir sonucudur, ancak soruyu bir konu ile sınırlandırmak istedim, bu yüzden daha genel.


Tablodaki aralıklar üst üste binmiyorsa veya dokunmuyorsa ne yaparsınız? Örneğin, (kind, start, end) = aralığında bir aralığınız varsa (1,2018-01-01,2018-01-15)ve (1,2018-01-20,2018-01-25)kaç tane çakışan tarihinizin olduğunu belirlerken bunu hesaba katmak ister misiniz?
Evan Carroll

Masanızın neden küçük olduğu konusunda da kafam karıştı? İlk aralıkta hepsine sahip olduğunda neden 2018-01-31ya 2018-01-30da 2018-01-29içinde değil?
Evan Carroll

@EvanCarroll tarihleri generate_seriesharici parametrelerdir - dates_rangestablodaki tüm aralıkları kapsaması gerekmez . Sanırım ilk soruya gelince, anlamıyorum - satırlar dates_rangesbağımsız, üst üste binmeyi belirlemek istemiyorum.
BartekCh

Yanıtlar:


4

Aşağıdaki sorgu "eksik sıfırlar" uygunsa da çalışır:

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;

ancak lateralküçük veri kümesine sahip sürümden daha hızlı değildir . Birleştirme gerekli olmadığından daha iyi ölçeklenebilir, ancak yukarıdaki sürüm tüm satırlarda toplanır, bu yüzden orada tekrar kaybedebilir.

Aşağıdaki sorgu, üst üste binmeyen herhangi bir diziyi kaldırarak gereksiz işlerden kaçınmaya çalışır:

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;

- ve ben overlapsoperatörü kullanmalıyım ! interval '1 day'Çakışan işleç, zaman aralıklarının sağda açık olduğunu düşündüğü için sağa eklemeniz gerektiğini unutmayın (bu oldukça mantıklıdır, çünkü bir tarih genellikle gece yarısı saat bileşenine sahip bir zaman damgası olarak kabul edilir).


Güzel, generate_seriesböyle kullanılabileceğini bilmiyordum . Birkaç testten sonra aşağıdaki gözlemlerim var. Sorgunuz gerçekten seçilen aralık uzunluğuyla gerçekten iyi ölçeklendiriliyor - 3 yıl ile 10 yıl arasında neredeyse hiçbir fark yok. Ancak daha kısa süreler (1 yıl) için çözümlerim daha hızlı - bunun nedeni, dates_rangessorgunuzu yavaşlatan (2010-2100 gibi) gerçekten uzun aralıklar olması . İç sorguyu sınırlamak start_dateve içiniz end_dateyardımcı olacaktır. Birkaç test daha yapmam gerekiyor.
BartekCh

6

Ve nasıl 0 sayısı ile tarih-tür çiftleri dahil etmek?

Tüm kombinasyonlardan oluşan bir ızgara oluşturun ve ardından LATERAL tablonuza şu şekilde katılın:

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;

Ayrıca olabildiğince hızlı olmalıdır.

LEFT JOIN LATERAL ... on trueİlk başta vardı , ama alt sorgu bir agrega var c, bu yüzden her zaman bir satır almak ve de kullanabilirsiniz CROSS JOIN. Performansta fark yok.

İlgili tüm türleri içeren bir tablonuz varsa , listeyi alt sorguyla oluşturmak yerine bu tabloyu kullanın k.

Oyuncular integeristeğe bağlıdır. Else olsun bigint.

Dizinler, özellikle çok sütunlu bir dizin üzerinde yardımcı olacaktır (kind, start_date, end_date). Bir alt sorgu üzerine inşa ettiğiniz için, bunu başarmak mümkün olabilir veya olmayabilir.

Listede olduğu gibi set döndüren işlevlerin generate_series()kullanılması, 10'dan önceki Postgres sürümlerinde (ne yaptığınızı tam olarak bilmiyorsanız) SELECTgenellikle önerilmez . Görmek:

Birkaç satır içeren veya hiç satır içermeyen çok sayıda kombinasyonunuz varsa, bu eşdeğer form daha hızlı olabilir:

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;

SELECTListedeki set döndürme işlevlerine gelince - tavsiye edilmediğini okudum, ancak böyle bir işlev varsa, sadece iyi çalışıyor gibi görünüyor. Sadece bir tane olacağından eminsem, bir şeyler ters gidebilir mi?
BartekCh

@BartekCh: Listedeki tek bir SRF SELECTbeklendiği gibi çalışır. Başka bir yorum eklemeye karşı uyarmak için bir yorum ekleyin. Veya FROMPostgres'in eski sürümlerinde başlamak için listeye taşıyın . Neden risk komplikasyonları? (Bu aynı zamanda standart
SQL'dir

1

daterangeTürü kullanma

PostgreSQL'in bir daterange. Bunu kullanmak oldukça basit. Örnek verilerinizden başlayarak tablodaki türü kullanmak için hareket ediyoruz.

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)

Verilen tarihler ve her tür için, date_ranges'ten her bir tarihin kaç satırına düştüğünü hesaplamak istiyorum.

Şimdi sorgulamak için prosedürü tersine çeviriyoruz ve bir tarih serisi oluşturuyoruz , ancak burada sorgunun kendisi @>, tarihlerin aralıkta olup olmadığını bir dizin kullanarak kontrol etmek için encment ( ) operatörünü kullanabileceği yakalama .

Kullandığımız not timestamp without time zone(DST tehlikelerini durdurmak için)

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;

Endekste belirtilen gün çakışmalarıdır.

Bir yan bonus olarak, tarih aralığı türüyle , başkalarıyla çakışan aralıkların eklenmesini durdurabilirsiniz .EXCLUDE CONSTRAINT


Sorgunuzla ilgili bir sorun var, sanırım satırları birden çok kez sayıyor, JOINsanırım çok fazla.
BartekCh

@BartekCh hayır, üst üste binen satırlarınız var, üst üste binen aralıkları kaldırarak (önerilen) veyacount(DISTINCT kind)
Evan Carroll

ama üst üste binen satırlar istiyorum. Örneğin, tür 1tarih 2018-01-01ilk iki satır içinde dates_ranges, ancak sorgunuz verir 8.
BartekCh

veyacount(DISTINCT kind)DISTINCT anahtar kelimeyi oraya eklediniz mi?
Evan Carroll

Ne yazık ki DISTINCTanahtar kelime ile hala beklendiği gibi çalışmıyor. Her tarih için farklı türleri sayar, ancak her tarih için her türün tüm satırlarını saymak istiyorum.
BartekCh
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.