Vollhistorie von Tabellen mit beginnt/endet Zeitstempel


Vollhistorie mit einer Tabelle

CAST beginnt/endet AS text

SELECT *
  FROM alkis.ax_grenzpunkt
 WHERE CASE WHEN '$hist_timestamp' = ''::text THEN
         endet IS NULL
       ELSE 
         beginnt::text <= '$hist_timestamp' AND ('$hist_timestamp' < endet::text OR endet IS NULL)
       END;

Nachteil: Die date-Felder beginnt und endet werden für den Vergleich in das Text-Format umgewandelt. Das kostet Zeit und macht sich bei großen Datensatzmengen deutlich bemerkbar.

CAST $hist_timestamp AS timestamp

SELECT *
  FROM alkis.ax_grenzpunkt
 WHERE CASE WHEN '$hist_timestamp' = ''::text THEN
         endet IS NULL
       ELSE 
         beginnt <= NULLIF('$hist_timestamp', '')::timestamp AND (NULLIF('$hist_timestamp', '')::timestamp < endet OR endet IS NULL)
       END;

Nachteil: Die WHERE-Klausel wird unübersichtlich. Wenn zwei Tabellen abgefragt werden, ist das WHERE gerade noch so verständlich, ab drei verknüpften Tabellen wirds völlig unübersichtlich.

CAST beginnt und endet AS tsrange

SELECT *
  FROM alkis.ax_grenzpunkt
 WHERE tsrange(beginnt, endet) @> COALESCE(NULLIF('$hist_timestamp', '')::timestamp, localtimestamp);

Vorteil: Die WHERE-Bedingung mit tsrange arbeitet deutlich schneller. Die Übersichtlichkeit bleibt auch bei mehreren verknüpften Tabellen erhalten.

Vollhistorie über mehrere Tabellen

CAST beginnt/endet AS text

SELECT *
  FROM alkis.ax_grenzpunkt AS g
  JOIN alkis.ax_punktortta AS p
       ON g.gml_id = ANY (p.istteilvon)
 WHERE CASE WHEN '$hist_timestamp' = '' THEN
         g.endet IS NULL AND p.endet IS NULL
       ELSE
         g.beginnt::text <= '$hist_timestamp' AND ('$hist_timestamp' < g.endet::text OR g.endet IS NULL) AND
         p.beginnt::text <= '$hist_timestamp' AND ('$hist_timestamp' < p.endet::text OR p.endet IS NULL)
       END;

Nachteil: Im „historischen Fall“ (dem ELSE-Zweig) werden alle Stände der ersten Tabelle und alle Fälle der zweiten Tabelle ermittelt und dann miteinander zu Objekten verknüpft - egal, ob sie zeitgleich existierten oder zeitlich nichts miteinander zu tun haben. Das generiert etliche überflüssige „Treffer“, die gar keine sind.

CAST beginnt und endet AS tsrange

SELECT *
  FROM alkis.ax_grenzpunkt AS g
  JOIN alkis.ax_punktortta AS p
       ON g.gml_id = ANY (p.istteilvon) AND
       tsrange(g.beginnt, g.endet) && tsrange(p.beginnt, p.endet)
 WHERE tsrange(g.beginnt, g.endet) * tsrange(p.beginnt, p.endet) @> COALESCE(NULLIF('$hist_timestamp', '')::timestamp, localtimestamp);

Vorteil: Der Vergleich der Lebenszeitintervalle der Objekte aus der ersten und aus der zweiten Tabelle liefert nur die Objekte, die gleichzeitig existierten (&& = a intersects b). In der WHERE-Bedingung wird nur das gemeinsame Lebenszeitintervall des verknüpften Objekts gegen $hist_timestamp geprüft (* = Intersection von a und b).

Operatoren auf range-Datentypen

Rückgabe-Datentyp ''boolean''

  • = (equal), <> (not equal), < (less than), > (greater than), <= (less than or equal) und >= (greater than or equal)
  • && (overlap) und -|- (is adjacent to)
  • <@ (is contained by) und @> (contains)
  • « (strictly left of) und » (strictly right of)
  • &< (does not extend to the right of) und &> (does not extend to the left of)

Rückgabe-Datentyp ''range''

  • * (intersection)
  • + (union)
  • - (difference)

Performance-Verbesserung von range-Operationen

Anlegen eines funktionalen Index:

CREATE INDEX idx_ax_grenzpunkt_range_gist
  ON alkis.ax_grenzpunkt
  USING gist (tsrange(beginnt, endet));

Die range-Operatoren =, &&, -|-, <@, @>, «, » und &< nutzen gist- und sp_gist-Indizes.