Mam tabelę (w PostgreSQL 9.4), która wygląda następująco:
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');
Teraz chcę obliczyć dla podanych dat i dla każdego rodzaju, ile wierszy z dates_ranges
każdej daty przypada. Zera można ewentualnie pominąć.
Pożądany rezultat:
+-------+------------+----+
| 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 |
+-------+------------+----+
Wymyśliłem dwa rozwiązania, jedno z LEFT JOIN
iGROUP 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
i jeden z LATERAL
, który jest nieco szybszy:
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
Zastanawiam się, czy jest lepszy sposób na napisanie tego zapytania? A jak uwzględnić pary dat z liczbą 0?
W rzeczywistości istnieje kilka różnych rodzajów, okres do pięciu lat (1800 dat) i ~ 30 000 wierszy w dates_ranges
tabeli (ale może znacznie wzrosnąć).
Brak indeksów. Mówiąc dokładniej, w moim przypadku jest to wynikiem podzapytania, ale chciałem ograniczyć pytanie do jednego problemu, więc jest bardziej ogólne.
postgresql
join
postgresql-9.4
functions
BartekCh
źródło
źródło
(1,2018-01-01,2018-01-15)
i(1,2018-01-20,2018-01-25)
czy chcesz wziąć to pod uwagę przy określaniu liczby pokrywających się dat?2018-01-31
lub2018-01-30
czy2018-01-29
w tym, kiedy pierwszy zakres ma wszystkie z nich?generate_series
są parametrami zewnętrznymi - niekoniecznie obejmują wszystkie zakresy wdates_ranges
tabeli. Co do pierwszego pytania, to chyba nie rozumiem - wierszedates_ranges
są niezależne, nie chcę określać nakładania się.Odpowiedzi:
Następujące zapytanie działa również, jeśli „brak zer” jest poprawny:
ale nie jest to szybsze niż
lateral
wersja z małym zestawem danych. Może jednak skalować się lepiej, ponieważ nie jest wymagane łączenie, ale powyższa wersja agreguje wszystkie wiersze, więc może ponownie stracić.Poniższe zapytanie próbuje uniknąć niepotrzebnej pracy poprzez usunięcie serii, które i tak się nie nakładają:
- i muszę użyć
overlaps
operatora! Zauważ, że musisz dodaćinterval '1 day'
po prawej, ponieważ nakładający się operator uważa, że przedziały czasowe są otwarte po prawej stronie (co jest dość logiczne, ponieważ data jest często uważana za znacznik czasu z komponentem czasu północy).źródło
generate_series
można tak używać. Po kilku testach mam następujące obserwacje. Twoje zapytanie rzeczywiście skaluje się naprawdę dobrze z wybraną długością zakresu - praktycznie nie ma różnicy między okresem 3 lat a 10 lat. Jednak w przypadku krótszych okresów (1 rok) moje rozwiązania są szybsze - zgaduję, że powodem jest to, że istnieją naprawdę duże odległościdates_ranges
(np. 2010-2100), które spowalniają Twoje zapytanie. Ograniczeniestart_date
iend_date
wewnętrzne zapytanie powinno jednak pomóc. Muszę zrobić jeszcze kilka testów.Zbuduj siatkę wszystkich kombinacji, a następnie
LATERAL
dołącz do stołu, tak jak to:Powinien też być tak szybki, jak to możliwe.
Na początku miałem
LEFT JOIN LATERAL ... on true
, ale w podzapytaniu jest agregatc
, więc zawsze otrzymujemy wiersz i możemy go również użyćCROSS JOIN
. Bez różnicy w wydajności.Jeśli masz tabelę zawierającą wszystkie odpowiednie rodzaje , użyj jej zamiast generowania listy z podzapytaniem
k
.Przesyłanie do
integer
jest opcjonalne. Jeszcze razbigint
.Pomogą w tym indeksy, zwłaszcza indeks wielokolumnowy
(kind, start_date, end_date)
. Ponieważ budujesz na podzapytaniu, może to być lub nie być możliwe do osiągnięcia.Używanie funkcji zwracających zestaw, takich jak
generate_series()
naSELECT
liście, na ogół nie jest zalecane w wersjach Postgres wcześniejszych niż 10 (chyba że wiesz dokładnie, co robisz). Widzieć:Jeśli masz wiele kombinacji z kilkoma wierszami lub bez, ten równoważny formularz może być szybszy:
źródło
SELECT
liście - przeczytałem, że nie jest to wskazane, ale wygląda na to, że działa dobrze, jeśli jest tylko jedna taka funkcja. Jeśli jestem pewien, że będzie tylko jeden, czy coś może pójść nie tak?SELECT
liście działa zgodnie z oczekiwaniami. Może dodaj komentarz, aby ostrzec przed dodaniem kolejnego. Lub przenieś go naFROM
listę, aby zacząć od starszych wersji Postgres. Po co ryzykować komplikacje? (Jest to również standardowy SQL i nie dezorientuje ludzi pochodzących z innych RDBMS.)Używanie
daterange
typuPostgreSQL ma
daterange
. Korzystanie z niego jest dość proste. Zaczynając od przykładowych danych, przechodzimy do użycia typu z tabeli.Teraz, aby wykonać zapytanie, odwracamy procedurę i generujemy serie dat, ale oto haczyk samo zapytanie może użyć
@>
operatora zawierającego ( ), aby sprawdzić, czy daty mieszczą się w zakresie, za pomocą indeksu.Uwaga, której używamy
timestamp without time zone
(aby zatrzymać zagrożenia DST)To jest wyszczególnione nakładki na dzień w indeksie.
Dodatkową korzyścią jest to, że dzięki typowi zmiany możesz zatrzymać wstawianie zakresów, które pokrywają się z innymi za pomocą
EXCLUDE CONSTRAINT
źródło
JOIN
chyba o jeden za dużo.count(DISTINCT kind)
1
data rodzaju2018-01-01
znajduje się w pierwszych dwóch wierszach oddates_ranges
, ale Twoje zapytanie podaje8
.count(DISTINCT kind)
czy dodałeśDISTINCT
tam słowo kluczowe?DISTINCT
słowem kluczowym nadal nie działa zgodnie z oczekiwaniami. Liczy różne rodzaje dla każdej daty, ale chcę policzyć wszystkie wiersze każdego rodzaju dla każdej daty.