Generatoren zum Erzeugen eindeutiger Datensatz-IDs

<< SQL-Befehle für Generatoren | Firebird Generatoren-Ratgeber | Was man sonst mit Generatoren machen kann >>

Generatoren zum Erzeugen eindeutiger Datensatz-IDs

Wozu überhaupt Datensatz-IDs?

Die Beantwortung dieser Frage würde den Rahmen diese Artikels deutlich sprengen. Derjenige, der keinen Sinn darin sieht, eine eindeutige Identifikationsmöglichkeit jedes Datensatzes in jeder Tabelle zu haben, oder dem das Konzept von "bedeutungslosen" oder "Surrogat"-Schlüsseln im allgemeinen missfällt, sollte das folgende Kapitel wohl besser überspringen...

Einer für alle oder einer für jede?

OK, du willst also Datensatz-IDs. { Anm.d.Autors: Glückwunsch! :-) }

Eine grundsätzliche, weitreichende Entscheidung muss gefällt werden: Benutzt man einen einzelnen Generator für alle Tabellen, oder jeweils einen Generator pro Tabelle. Dies ist dir überlassen - man sollte aber folgendes in Betracht ziehen:

Mit dem Einer für alle-Ansatz:

  • + braucht man nur einen einzlnen Generator für alle IDs;
  • + hat man einen Integerwert, der den Datensatz nicht nur in seiner Tabelle, sondern in der gesamten Datenbank eindeutig identifiziert;
  • - hat man weniger verfügbare Generatorwerte pro Tabelle (das sollte mit 64bit-Generatoren nicht wirklich ein Problem sein);
  • - bekommt man es bald mit unhandlich großen ID-Werten zu tun, selbst in z.B. kleinen Nachschlagetabellen mit nur einer Handvoll Einträgen;
  • - hat man höchstwahrscheinlich Lücken in den IDs einer Tabelle, da die ID-Werte über alle Tabellen verteilt werden.

Mit dem Einer für jede-Ansatz:

  • - muss man für jede ID-fähige Tabelle in der Datenbank einen eigenen Generator anlegen;
  • - braucht man immer die Kombination aus ID und Tabellenname zur eindeutigen Identifizierung des Satzes in der Datenbank;
  • + hat man einen einfachen und robusten "Einfügezähler" pro Tabelle;
  • + hat man eine chronologische Sequenz pro Tabelle: Findet man eine Lücke in den IDs, stammt sie entweder von einem DELETE oder einem schiefgegangenen INSERT.

Kann man Generatorwerte wiederverwenden?

Nun ja, technisch gesehen kann man das. Aber NEIN, man sollte es nicht. Niemals. NIE-NIE-NIEMALS. Nicht nur würde dies die schöne chronologische Reihenfolge der IDs zerstören (man kann das "Alter" eines Datansatzes nicht mehr an Hand der ID abschätzen), je mehr man darüber nachdenkt um so mehr Kopfschmerzen bereitet es. Abgesehen davon ist es ein völliger Widerspruch zum Konzept eindeutiger Datensatz-IDs.

Solange man also keine wirklich guten Gründe hat, Generatorwerte zu "recyclen", und einen wohlüberlegten Mechanismus besitzt, um dies in Mehrbenutzer/Multi-Transaktionsumgebungen sicher zu machen, FINGER WEG!

zurück zum Seitenanfang

Generatoren für IDs oder Auto-Increment-Felder

Einem neu eingefügten Datensatz eine ID (im Sinne einer eindeutigen "Seriennummer") zu geben ist einfach zu bewerkstelligen unter Verwendung eines Generators und eines BEFORE INSERT-Triggers, wie wir im Folgenden sehen werden. Wir starten mit einer Tabelle TTEST mit einer Spalte ID, deklariert als Integer. Unser Generator heisst GIDTEST.

Before Insert Trigger, Version 1

 CREATE TRIGGER trgTTEST_BI_V1 for TTEST
 active before insert position 0
 as
 begin
   new.id = gen_id( gidTest, 1 );
 end

Probleme mit Trigger Version 1:

Dieser erledigt die Arbeit - aber er "verschwendet" auch jedes mal einen Generatorwert, wenn im INSERT-Befehl bereits ein generierter Wert für die ID übergeben wurde. Es wäre also effektiver, nur dann einen neuen Wert zu generieren, wenn nicht bereits einer im INSERT-Befehl enthalten war:

Before Insert Trigger, Version 2

 CREATE TRIGGER trgTTEST_BI_V2 for TTEST
 active before insert position 0
 as
 begin
   if (ne w.idisnullthen
   begin
     new.id = gen_id( gidTest, 1 );
   end
 end

Probleme mit Trigger Version 2:

Manche Zugriffskomponenten haben die "dumme Angewohnheit", alle Spaltenwerte in einem INSERT-Befehl vorzubelegen. Die Felder, die man nicht explizit setzt, bekommen Vorgabewerte - üblicherweise 0 für Integer-Spalten. In diesem Falle würde der obige Trigger nicht funktionieren: Er würde sehen, dass die ID-Spalte nicht den Zustand NULL, sondern den Wert 0 hat, und würde deshalb keine neue ID generieren. Man könnte den Satz dennoch speichern - aber nur einen... der zweite würde fehlschlagen. Es ist ohnehin eine gute Idee, die 0 als normalen ID-Wert zu "verbannen", allein schon um Verwechslungen zwischen NULL und 0 zu vermeiden. Man könnte z.B. einen speziellen Datensatz mit einer ID von 0 zur Speicherung der eigenen Vorgabewerte jeder Spalte in der Tabelle verwenden.

zurück zum Seitenanfang

Before Insert Trigger, Version 3

 CREATE TRIGGER trgTTEST_BI_V3 for TTEST
 active before insert position 0
 as
 begin
   if ((new.id is null) or (new.id = 0)) then
   begin
     new.id = gen_id( gidTest, 1 );
   end
 end

Nun, da wir einen robust funktionierenden ID-Trigger haben, werden die folgenden Absätze erläutern, warum man den meistens gar nicht braucht:

Das Grundproblem mit IDs, die in BEFORE INSERT-Triggern zugewiesen werden, ist, dass sie die IDs serverseitig erzeugen, nachdem man den INSERT-Befehl zum Server geschickt hat. Das heißt schlicht und ergreifend, dass es keinen sicheren Weg gibt, von der Client-Seite aus zu erfahren, welche ID für den gerade erzeugten Satz vergeben wurde.

Man könnte nach dem INSERT den aktuellen Stand des Generators abfragen, aber im Mehrbenutzerbetrieb kann man nicht wirklich sicher sein, dass es die ID des eigenen Datensatzes ist (wegen der Transaktionskontrolle).

Generiert man aber einen neuen Generatorwert vorher, und füllt die ID-Spalte im INSERT-Befehl mit diesem Wert, dann kann man den Datensatz einfach mit einem Select ... where ID=<GenWert> aus der Datenbank holen, um z.B. zu sehen, welche Vorgabewert greifen oder welche Spalten durch INSERT-Trigger verändert wurden. Dies funktioniert deshalb besonders gut, weil man üblicherweise einen eindeutigen Primärindex für die ID-Spalte hat, und das sind so ungefähr die schnellsten Indizes, die man kriegen kann - sie sind unschlagbar in punkto Selektivität, und meist auch kleiner als Indizes für Textfelder vom Typ CHAR(n) (gilt für n>8, abhängig von Zeichensatz und Sortierreihenfolge).

Fazit des Ganzen:

Man sollte immer einen BEFORE INSERT-Trigger erzeugen, um absolut sicher zu sein, dass jeder neue Datensatz eine eindeutige ID erhält, selbst wenn im INSERT-Befehl keine übergeben wurde.

Hat man eine SQL-mäßig "geschlossene" Datenbank (d.h. die eigene Applikation ist die einzige Quelle neuer Datensätze), dann kann man den Trigger weglassen. Dann muss man aber IMMER einen Generatorwert vor dem INSERT holen und ihn im INSERT-Befehl mitübergeben. Das selbe gilt selbstverständlich für INSERTS, die aus Stored Procedures oder Triggern heraus erfolgen.

Siehe auch:
deutschsprachig:
Generator
Stored Procedure
Trigger

zurück zum Seitenanfang
<< SQL-Befehle für Generatoren | Firebird Generatoren-Ratgeber | Was man sonst mit Generatoren machen kann >>