Próbuję połączyć wiele zakresów dat (moje obciążenie wynosi około 500, w większości przypadków 10), które mogą, ale nie muszą, pokrywać się z największymi możliwymi ciągłymi zakresami dat. Na przykład:
Dane:
CREATE TABLE test (
id SERIAL PRIMARY KEY NOT NULL,
range DATERANGE
);
INSERT INTO test (range) VALUES
(DATERANGE('2015-01-01', '2015-01-05')),
(DATERANGE('2015-01-01', '2015-01-03')),
(DATERANGE('2015-01-03', '2015-01-06')),
(DATERANGE('2015-01-07', '2015-01-09')),
(DATERANGE('2015-01-08', '2015-01-09')),
(DATERANGE('2015-01-12', NULL)),
(DATERANGE('2015-01-10', '2015-01-12')),
(DATERANGE('2015-01-10', '2015-01-12'));
Tabela wygląda następująco:
id | range
----+-------------------------
1 | [2015-01-01,2015-01-05)
2 | [2015-01-01,2015-01-03)
3 | [2015-01-03,2015-01-06)
4 | [2015-01-07,2015-01-09)
5 | [2015-01-08,2015-01-09)
6 | [2015-01-12,)
7 | [2015-01-10,2015-01-12)
8 | [2015-01-10,2015-01-12)
(8 rows)
Pożądane wyniki:
combined
--------------------------
[2015-01-01, 2015-01-06)
[2015-01-07, 2015-01-09)
[2015-01-10, )
Reprezentacja wizualna:
1 | =====
2 | ===
3 | ===
4 | ==
5 | =
6 | =============>
7 | ==
8 | ==
--+---------------------------
| ====== == ===============>
postgresql
aggregate
range-types
Villiers Strauss
źródło
źródło
Odpowiedzi:
Założenia / wyjaśnienia
Nie ma potrzeby rozróżniania
infinity
i otwierania górnej granicy (upper(range) IS NULL
). (Możesz to zrobić w obie strony, ale w ten sposób jest to prostsze.)infinity
w typach zakresów PostgreSQLPonieważ
date
jest to dyskretny typ, wszystkie zakresy mają domyślne[)
granice. Według dokumentacji:W przypadku innych typów (jak
tsrange
!) Egzekwowałbym to samo, jeśli to możliwe:Rozwiązanie z czystym SQL
Z CTE dla jasności:
Lub to samo z podkwerendami, szybsze, ale mniej łatwe do odczytania:
Lub z jednym poziomem podkwerend mniejszym, ale z odwróconą kolejnością sortowania:
ORDER BY range DESC NULLS LAST
(zNULLS LAST
), aby uzyskać idealnie odwróconą kolejność sortowania. Powinno to być tańsze (łatwiejsze w produkcji, idealnie pasuje do kolejności sortowania sugerowanego indeksu) i dokładne dla przypadków narożnychrank IS NULL
.Wyjaśnić
a
: Przy zamawianiu przezrange
, oblicz maksymalną liczbę górną granicę (enddate
) za pomocą funkcji okna.Zastąp granice NULL (niezwiązane) +/-
infinity
tylko dla uproszczenia (bez specjalnych przypadków NULL).b
: W tym samym porządku sortowania, jeśli poprzednienddate
jest wcześniejszy niżstartdate
mamy przerwę i rozpoczynamy nowy zakres (step
).Pamiętaj, że górna granica jest zawsze wykluczona.
c
: Utwórz grupy (grp
), licząc kroki za pomocą innej funkcji okna.W zewnętrznej
SELECT
kompilacji waha się od dolnej do górnej granicy w każdej grupie. Voilá.Ściśle związana odpowiedź na SO z dodatkowymi wyjaśnieniami:
Rozwiązanie proceduralne z plpgsql
Działa dla dowolnej nazwy tabeli / kolumny, ale tylko dla typu
daterange
.Rozwiązania proceduralne z pętlami są zwykle wolniejsze, ale w tym szczególnym przypadku oczekuję, że funkcja będzie znacznie szybsza, ponieważ wymaga tylko jednego sekwencyjnego skanowania :
Połączenie:
Logika jest podobna do rozwiązań SQL, ale możemy zrobić to za jednym przejściem.
SQL Fiddle.
Związane z:
Zwykłe ćwiczenie do obsługi danych wejściowych użytkownika w dynamicznym SQL:
Indeks
Dla każdego z tych rozwiązań zwykły (domyślny) indeks btree
range
byłby kluczowy dla wydajności w dużych tabelach:Indeks btree ma ograniczone zastosowanie dla typów zakresów , ale możemy uzyskać wstępnie posortowane dane, a może nawet skanowanie tylko indeksu.
źródło
EXPLAIN ( ANALYZE, TIMING OFF)
najlepszymi z pięciu.max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate
? Czy to nie może być po prostuCOALESCE(upper(range), 'infinity') as enddate
? AFAIKmax() + over (order by range)
powróci właśnieupper(range)
tutaj.Wymyśliłem to:
Nadal wymaga trochę szlifowania, ale pomysł jest następujący:
+
) zawiedzie, zwróć już zbudowany zakres i ponownie zainicjujźródło
generate_series()
w każdym rzędzie jest dość drogie , zwłaszcza jeśli mogą istnieć otwarte zakresy ...Kilka lat temu przetestowałem różne rozwiązania (między innymi podobne do tych z @ErwinBrandstetter) do łączenia nakładających się okresów w systemie Teradata i znalazłem następujące najbardziej wydajne (przy użyciu funkcji analitycznych, nowsza wersja Teradata ma wbudowane funkcje dla to zadanie).
maxEnddate
maxEnddate
przy użyciuLEAD
i jesteś prawie gotowy. Tylko dla ostatniego wierszaLEAD
zwraca aNULL
, aby rozwiązać ten problem, oblicz maksymalną datę zakończenia wszystkich wierszy partycji w kroku 2 iCOALESCE
to.Dlaczego było szybciej? W zależności od rzeczywistych danych krok # 2 może znacznie zmniejszyć liczbę wierszy, więc następny krok musi działać tylko na małym podzbiorze, a dodatkowo usuwa agregację.
skrzypce
Ponieważ było to najszybsze na Teradata, nie wiem, czy to samo dla PostgreSQL, byłoby miło uzyskać jakieś rzeczywiste liczby wydajności.
źródło
Dla zabawy spróbowałem. Przekonałem się, że jest to najszybsza i najczystsza metoda, aby to zrobić. Najpierw definiujemy funkcję, która łączy się, jeśli zachodzi na siebie nakładka lub jeśli dwa wejścia są sąsiednie, jeśli nie ma nakładania się lub sąsiedztwa, po prostu zwracamy pierwszą datę zmiany. Wskazówka
+
jest związkiem zasięgu w kontekście zakresów.Następnie używamy tego w ten sposób,
źródło
('2015-01-01', '2015-01-03'), ('2015-01-03', '2015-01-05'), ('2015-01-05', '2015-01-06')
.