Überlappende Einträge markieren

Hallo,

ich knobele schon eine ganze Weile an einer Sache herum. Ich habe zwar schon eine Quick’n’dirty-Version laufen, aber ich hätte es gerne in “schön” - dass heißt möglichst in einem SQL-Statement.

Doch nun zur Sache:
ich erstelle eine Erfassung von Abwesenheiten von Mitarbeitern (Krankheit, Urlaub, …) und habe (im Wesentlichen - die “echten” sind natürlich komplexer, aber das tut hier nichts zur Sache) folgende Tabellen erstellt:

CREATE TABLE mitarbeiter(
    id          INTEGER PRIMARY KEY,
    vorname     TEXT,
    name        TEXT
);
INSERT INTO mitarbeiter VALUES (1, 'Martin', 'Meier');
INSERT INTO mitarbeiter VALUES (2, 'Nicole', 'Neumann');

CREATE TABLE abwesenheit_grund(
    id          INTEGER PRIMARY KEY,
    bezeichnung TEXT
);
INSERT INTO abwesenheit_grund VALUES (1, 'Krankheit');
INSERT INTO abwesenheit_grund VALUES (2, 'Urlaub');

CREATE TABLE abwesenheit(
    id          SERIAL PRIMARY KEY,
    mitarbeiter INTEGER REFERENCES mitarbeiter,
    grund       INTEGER REFERENCES abwesenheit_grund,
    beginn      TIMESTAMP,
    ende        TIMESTAMP,
    fehler      BOOLEAN DEFAULT false
);
INSERT INTO abwesenheit (mitarbeiter, grund, beginn, ende)
VALUES (1, 2, '2008-04-21 8:00', '2008-04-25 18:00');
INSERT INTO abwesenheit (mitarbeiter, grund, beginn, ende)
VALUES (2, 2, '2008-04-14 8:00', '2008-05-02 18:00');
INSERT INTO abwesenheit (mitarbeiter, grund, beginn, ende)
VALUES (1, 1, '2008-04-23 12:00', '2008-04-23 17:00');

Bei den Datensätzen sollte es natürlich keine Überlappungen geben, d.h. wenn für den Mitarbeiter 1 Urlaub vom 21.04 bis zum 25.04 eingegeben wurde und nun ein Krankheitstag am 23.04 - also während des Urlaubs - dann sollen die beiden Datensätze zur Korrektur herausgesucht werden, was ja auch einfach ist:

SELECT  ueberlapp.id
FROM    abwesenheit AS test,
        abwesenheit AS ueberlapp
WHERE   test.id = 3 AND
        ueberlapp.mitarbeiter = test.mitarbeiter AND
        ueberlapp.beginn < test.ende AND
        ueberlapp.ende   > test.beginn;

ich teste hier, ob es zum Eintrag mit der ID 3 einen überlappenden Eintrag gibt. Ergebnis lautet auch - wie erwartet:

 id
----
  1
  3
(2 Zeilen)

Leider liefert mir diese Abfrage aber auch ein Ergebnis zurück, wenn es keinen überlappenden Eintrag gibt:

SELECT  ueberlapp.id
FROM    abwesenheit AS test,
        abwesenheit AS ueberlapp
WHERE   test.id = 2 AND
        ueberlapp.mitarbeiter = test.mitarbeiter AND
        ueberlapp.beginn < test.ende AND
        ueberlapp.ende   > test.beginn;

liefert mir

 id
----
  2
(1 Zeile)

was ich aber nicht haben möchte, da es ja keine Überlappung gibt.
Aufmerksame Zeitgenossen werden jetzt natürlich zurecht einwenden, dass ich den Datensatz, den ich prüfe auch ausschließen könnte:

SELECT  ueberlapp.id
FROM    abwesenheit AS test,
        abwesenheit AS ueberlapp
WHERE   test.id = 2 AND
        test.id <> ueberlapp.id AND
        ueberlapp.mitarbeiter = test.mitarbeiter AND
        ueberlapp.beginn < test.ende AND
        ueberlapp.ende   > test.beginn;

liefert mir auch

 id
----
(0 Zeilen)

ich möchte aber gerne alle Datensätze aufgerufen haben, die sich überlappen.
Der Hintergrund ist folgender: nach dem Einfügen eines Datensatzes möchte ich gerne so etwas machen:

INSERT INTO abwesenheit (mitarbeiter, grund, beginn, ende)
VALUES(2, 1, '2008-04-10 8:00', '2008-04-10 17:00');
UPDATE  abwesenheit
SET     fehler = true
WHERE   id IN (
    SELECT  ueberlapp.id
    FROM    abwesenheit AS test,
            abwesenheit AS ueberlapp
    WHERE   test.id = currval('abwesenheit_id_seq') AND
            ueberlapp.mitarbeiter = test.mitarbeiter AND
            ueberlapp.beginn < test.ende AND
            ueberlapp.ende   > test.beginn
);

womit dann im Anschluss an das INSERT bei allen überlappenden Einträgen - und eben auch bei dem neuen Datensatz - das Feld abwesenheit.fehler auf ‘true’ gesetzt wird und nichts geschieht, wenn es keine überlappenden Einträge gibt.

Wenn es keine triviale Lösung ohne Funktion gibt, werde ich wohl doch plpgsql bemühen müssen. :frowning: Ich gebe zu, dass meine Frage etwas akademisch ist, aber ich finde es einfach “schöner” wenn es mit reinem SQL ginge.

Für Anregungen und/oder Hinweise bin ich äußerst dankbar…

Ich würde auf die Fehler-Spalte verzichten und statt dessen eine SRF schreiben, die all jene Records liefert, die sich überschneiden. Daraus könnte man dann dem Benutzer eine Übersicht der zu korrigierenden Datensätze basteln.

Andreas

Letztendlich werde ich es wohl auch so machen. Ich fand es nur reizvoll, so etwas mit Plain-SQL zu lösen - das wäre dann eine etwas allgemeinere Lösung…

Geht sicher auch mit SQL. Schnelltest:

test=# select * from abwesenheit;
 id | mitarbeiter | grund |         beginn         |          ende          | fehler
----+-------------+-------+------------------------+------------------------+--------
  1 |           1 |     2 | 2008-04-21 08:00:00+02 | 2008-04-25 18:00:00+02 |
  2 |           2 |     2 | 2008-04-14 08:00:00+02 | 2008-05-02 18:00:00+02 |
  3 |           1 |     1 | 2008-04-23 12:00:00+02 | 2008-04-23 17:00:00+02 |
  4 |           1 |     1 | 2008-04-26 12:00:00+02 | 2008-04-27 17:00:00+02 |
(4 rows)

Also, Mitarbeiter 1, id 1 und 3 überschneiden sich.

test=*# select * from (select a.mitarbeiter, a.id, b.id, a.beginn as ba, a.ende as ea, b.beginn as bb, b.ende as eb from abwesenheit a left join abwesenheit b on (a.mitarbeiter=b.mitarbeiter) and a.beginn<b.beginn) foo where bb is not null and (ba,ea) overlaps (bb,eb);
 mitarbeiter | id | id |           ba           |           ea           |           bb           |           eb
-------------+----+----+------------------------+------------------------+------------------------+------------------------
           1 |  1 |  3 | 2008-04-21 08:00:00+02 | 2008-04-25 18:00:00+02 | 2008-04-23 12:00:00+02 | 2008-04-23 17:00:00+02
(1 row)

Geht sicher eleganter, und ich befürchte, daß das bei großen Tabellen dann, ähm, nicht optimal sein könnte.

Andreas