Fast jeder Datenbank Entwickler kennt Inner Joins, Outer Joins, Full Joins, Equi Joins, Self Joins und vermutlich die eine oder andere Variante mehr. Ich habe im vorigen Projekt noch eine gefunden, die ich bisher noch nicht kannte. Ich nenne sie Intervall Join oder Logistischer Join. Da jede Zeile als Intervall interpretiert wird und mir diese Art des Joins bei logistischen Problemen besonders nützlich erscheint.

Prinzipiell geht es bei dem Join darum zwei Folgen von Intervallen so 'nebeneinander zu halten', dass die jeweils überlappenden Teilintervalle der beiden Folgen als Ergebniss herauskommen. Ich werde das ganze an einem Beispiel erklären. Wie üblich bei mir ist alles im Zweifelsfall beliebig Oracle spezifisch.

Szenario


Es gibt zwei Tabellen, die eine enthält Nachschublieferungen von, die andere Bedarf an einem bestimmten Produkt:

drop table supply;

drop table demand;create table supply (id number,datum date, menge number);

create table demand (id number,datum date, menge number);

insert into supply values ( 101, sysdate, 10);

insert into supply values (102, sysdate +1, 30);

insert into supply values (103, sysdate +3, 5);

insert into supply values (104, sysdate +4, 3);

insert into supply values (105, sysdate +5, 20);

insert into demand values ( 201, sysdate, 3);

insert into demand values (202, sysdate +1, 15);

insert into demand values (203, sysdate +3, 12);

insert into demand values (204, sysdate +4, 20);

insert into demand values (205, sysdate +5, 23);

insert into demand values (206, sysdate +6, 11);



Im Klartext: heute werden 10 Einheiten erwartet, morgen 30 und übermorgen 5. Entsprechen werden heute 3 Einheiten benötigt, morgen 15 und übermorgen 12.

Fragestellung


Ich möchte nun wissen, welcher Bedarf durch welche Nachschublieferungen versorgt wird. Dies kann interessant sein, da Nachschub der erst noch erwartet wird mit einer gewissen Unsicherheit versehen ist, während Nachschub in der Vergangenheit schon da und damit sicher ist. Auch könnte es ja sein, dass der Nachschub, der einen Bedarf versorgt erst nach dem Bedarf eintrifft, was dann wohl auf ein Problem hindeutet. Wenn ich derjenige bin, der den pünktlichen Nachschub zu gewährleisten hat, sind derartige Fragestellungen sehr interessant.Die folgende Tabelle skizziert den Zusammenhang zwischen den Supply und Demand Datensätzen (die Zeit verläuft von unten nach Oben).














Supply Demand Versorgung
206, 11 (*,206), 11
205, 23 (*, 205), 5
105, 20 (105, 205), 18
204, 20 (105, 204), 2
104, 3 (104, 204), 3
103, 5 (103, 204), 5
102, 30 (102, 204), 10
203, 12 (102, 203), 12
202, 15 (102, 202), 8
101, 10 (101, 202), 7
201, 3 (101, 201 ), 3

In der ersten Spalte ist jeweils die ID und die Menge des Nachschubs angegeben, entsprechend in der zweiten Spalte ID und Menge des Bedarfs. In der dritten Spalte sind die Nachschub- und Bedarfsdatensätze einander zugeordnet. Man erkennt zum Beispiel, dass der Bedarf 202 aus einer Restmenge (7)  von Nachschub 101 und einer Teilmenge (8) von 102 gedeckt wird. Wichtig ist, das die Zeitpunkt der Lieferung, beziehungweise des Bedarfs für die Zuordnung keine Rolle spielt, aber mann könnte nun in den Versorgungsdatensätzen prüfen, bei welchen Datensätzen das Datum der Lieferung nach dem des Bedarfes liegt und somit Probleme erkennen.

Lösung


Ursprünglich wurde das Problem durch PL/SQL Code gelöst. Das hatte die folgenden Nachteile:

  • Es war nur schwer zu lesen
  • Es war lang
  • Es war langsam
  • Es war fehleranfällig beim Änderungen
  • Es wurden auch nach längerer Laufzeit immer wieder Fehler gefunden


An dieser Stelle setzte mein 'Das muss doch auch anders gehen' Ehrgeiz ein und ich entwickelte die folgende Lösung:

SELECT s.id, s.datum, s.menge, d.id, d.datum, d.menge,
-- s.werk,
LEAST(s.sum_cum, d.sum_cum) - GREATEST(s.sum_cum- s.menge, d.sum_cum- d.menge) menge
FROM
( SELECT id, datum, menge,
SUM(menge) OVER (
-- PARTITION BY werk
ORDER BY datum) sum_cum
FROM supply
)s,
( SELECT id, datum, menge,
SUM(menge) OVER (
-- PARTITION BY werk
ORDER BY datum) sum_cum
FROM demand
) d
WHERE s.sum_cum - s.menge < d.sum_cum
AND s.sum_cum > d.sum_cum - d.menge
-- AND s.werk = d.werk

Im Vergleich zum ursprünglichen PL/SQL Code ist dies:

* ähnlich schwer zu lesen
* kurz
* recht schnell
* kaum Fehleranfällig
* extrem robust bei Änderungen
* Wenn das Statement einmal lief lieferte es zuverlässig auch in allen Sonderfällen die richtige Lösung

Typischer Weise muss man nicht nur eine Art Produkt versorgen, sondern viele verschiedene in unterschiedlichen Werken, dies ist im Beispiel durch die auskommentierten Code Zeilen skizziert. Wie man sieht kann man auch dies einfach bewerkstelleigen.

Im dem SQL Konstrukt werden zunächst laufende Summen gebildet (sum_com) die jeweiligen Intervalle, die in der obigen Tabelle dargestellt sind, gehen jeweils von sum_cum-menge bis sum_cum. Von allen Intervallen Paaren, werden dann diejenigen ausgewählt, die eine nicht leere Schnittmenge haben. Dies geschieht in der Where Klausel. Schließlich verbleibt es das Schnittmengenintervall zu bestimmen, dies ist die Differenz in der Spaltenliste. Wenn man sich an die eklige Syntax von Analytischen Funktionen gewöhnt hat alles gar nicht mehr so schwer.

Was die Performance angeht war das Ergebnis auch mit erheblichen Datenmengen sehr befriedigend. Oracle greift auf die beiden Tabellen nur einmal zu und erledigt den Rest im Hauptspeicher. Eventuell ist eine performantere PL/SQL Implementation denkbar, da Oracle kaum erkennen wird, dass sich bei diesem Join ein Merge Join eignen könnte, den man jedoch manuell implementieren könnte, aber ich bezweifel, dass es viele Scenarien gibt, in denen sich der erhöhte Entwicklungsaufwand lohnt.

Wie man am Titel erkennt ist dieser Artikel ein Teil einer losen Serie. Die anderen Teile findet ihr hier:

Die unendliche Tabelle

Verwendungen eines Indexes bei LIKE Abfragen

Talks

Wan't to meet me in person to tell me how stupid I am? You can find me at the following events: