Ich habe eine Tabelle (in PostgreSQL 9.4), die so aussieht:
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');
Jetzt möchte ich für die angegebenen Daten und für jede Art berechnen, in wie viele Zeilen von dates_ranges
jedem Datum fallen. Nullen könnten möglicherweise weggelassen werden.
Erwünschtes Ergebnis:
+-------+------------+----+
| 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 |
+-------+------------+----+
Ich habe zwei Lösungen gefunden, eine mit LEFT JOIN
undGROUP 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
und eins mit LATERAL
, was etwas schneller ist:
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
Ich frage mich, ob es eine bessere Möglichkeit ist, diese Abfrage zu schreiben. Und wie kann man Datumspaare mit 0 zählen?
In Wirklichkeit gibt es einige verschiedene Arten, einen Zeitraum von bis zu fünf Jahren (1800 Daten) und ~ 30.000 Zeilen in der dates_ranges
Tabelle (aber es könnte erheblich wachsen).
Es gibt keine Indizes. In meinem Fall ist es ein Ergebnis einer Unterabfrage, aber ich wollte die Frage auf ein Problem beschränken, damit es allgemeiner ist.
quelle
(1,2018-01-01,2018-01-15)
und(1,2018-01-20,2018-01-25)
möchten Sie dies berücksichtigen, wenn Sie bestimmen, wie viele überlappende Daten Sie haben?2018-01-31
oder2018-01-30
oder2018-01-29
drin, wenn der erste Bereich alle hat?generate_series
sind externe Parameter - sie decken nicht unbedingt alle Bereiche in derdates_ranges
Tabelle ab. Was die erste Fragedates_ranges
betrifft, verstehe ich sie vermutlich nicht - Zeilen in sind unabhängig, ich möchte keine Überlappung feststellen.Antworten:
Die folgende Abfrage funktioniert auch, wenn "fehlende Nullen" in Ordnung sind:
aber es ist nicht schneller als die
lateral
Version mit dem kleinen Datensatz. Es kann zwar besser skaliert werden, da kein Join erforderlich ist, die obige Version jedoch über alle Zeilen aggregiert wird, sodass es dort möglicherweise wieder verloren geht.Die folgende Abfrage versucht, unnötige Arbeit zu vermeiden, indem alle Serien entfernt werden, die sich ohnehin nicht überlappen:
- und ich muss den
overlaps
Operator benutzen ! Beachten Sie, dass Sieinterval '1 day'
rechts hinzufügen müssen , da der Überlappungsoperator die Zeiträume rechts als offen betrachtet (was ziemlich logisch ist, da ein Datum häufig als Zeitstempel mit einer Zeitkomponente von Mitternacht betrachtet wird).quelle
generate_series
man so etwas verwenden kann. Nach einigen Tests habe ich folgende Beobachtungen. Ihre Anfrage lässt sich in der Tat sehr gut mit der ausgewählten Bereichslänge skalieren - es gibt praktisch keinen Unterschied zwischen 3 und 10 Jahren. Für kürzere Zeiträume (1 Jahr) sind meine Lösungen jedoch schneller - ich vermute, der Grund dafür ist, dass es einige wirklich große Bereiche gibtdates_ranges
(wie 2010-2100), die Ihre Anfrage verlangsamen. Das Einschränkenstart_date
undend_date
innerhalb der inneren Abfrage sollte jedoch helfen. Ich muss noch ein paar Tests machen.Erstellen Sie ein Raster aller Kombinationen und
LATERAL
verbinden Sie es dann wie folgt mit Ihrer Tabelle:Sollte auch so schnell wie möglich sein.
Ich hatte
LEFT JOIN LATERAL ... on true
zuerst, aber es gibt ein Aggregat in der Unterabfragec
, so dass wir immer eine Zeile bekommen und auch verwenden könnenCROSS JOIN
. Kein Leistungsunterschied.Wenn Sie eine Tabelle mit allen relevanten Arten haben , verwenden Sie diese, anstatt die Liste mit Unterabfragen zu generieren
k
.Die Besetzung
integer
ist optional. Sonst bekommst dubigint
.Indizes würden helfen, insbesondere ein mehrspaltiger Index auf
(kind, start_date, end_date)
. Da Sie auf einer Unterabfrage aufbauen, kann dies möglicherweise nicht erreicht werden.Die Verwendung von Set-Return-Funktionen wie
generate_series()
in derSELECT
Liste ist in Postgres-Versionen vor 10 im Allgemeinen nicht ratsam (es sei denn, Sie wissen genau, was Sie tun). Sehen:Wenn Sie viele Kombinationen mit wenigen oder keinen Zeilen haben, ist diese äquivalente Form möglicherweise schneller:
quelle
SELECT
Liste betrifft - ich habe gelesen, dass dies nicht ratsam ist, aber es sieht so aus, als ob es gut funktioniert, wenn es nur eine solche Funktion gibt. Wenn ich sicher bin, dass es nur einen geben wird, könnte dann etwas schief gehen?SELECT
Liste funktioniert wie erwartet. Fügen Sie möglicherweise einen Kommentar hinzu, um vor dem Hinzufügen eines weiteren zu warnen. Oder verschieben Sie es in dieFROM
Liste, um mit älteren Versionen von Postgres zu beginnen. Warum Risikokomplikationen? (Das ist auch Standard-SQL und verwirrt nicht Leute, die von anderen RDBMS kommen.)Verwenden Sie den
daterange
TypPostgreSQL hat eine
daterange
. Die Verwendung ist ziemlich einfach. Beginnend mit Ihren Beispieldaten verwenden wir den Typ in der Tabelle.Um es abzufragen, kehren wir die Prozedur um und generieren eine Datumsreihe. Hier ist jedoch der Haken, den die Abfrage selbst mithilfe des
@>
Operators includement ( ) verwenden kann, um mithilfe eines Index zu überprüfen, ob die Datumsangaben im Bereich liegen .Beachten Sie, dass wir verwenden
timestamp without time zone
(um DST-Gefahren zu stoppen)Welches sind die aufgeschlüsselten Tagesüberschneidungen im Index?
Als Nebenbonus können Sie mit dem Daterange-Typ das Einfügen von Bereichen, die sich mit anderen überlappen, mit einem stoppen
EXCLUDE CONSTRAINT
quelle
JOIN
zu viel, denke ich.count(DISTINCT kind)
1
Datum2018-01-01
ist innerhalb der ersten zwei Zeilen vondates_ranges
, aber Ihre Anfrage gibt8
.count(DISTINCT kind)
haben Sie dort dasDISTINCT
Schlüsselwort hinzugefügt ?DISTINCT
funktioniert es mit dem Schlüsselwort immer noch nicht wie erwartet. Es werden unterschiedliche Arten für jedes Datum gezählt, aber ich möchte alle Zeilen jeder Art für jedes Datum zählen.