Verhalten der Datenbank abfangen

Guten Tag,

ich stelle mal meine Frage aus dem Lazarusforum (https://www.lazarusforum.de/viewtopic.php?f=17&t=15085) auch nochmal hier, weil ich nicht weiß ob “mein Problem” Lazarusseitig gehandhabt werden muß oder innerhalb der Datenbank gehandhabt werden kann, bzw. das DB-Design geändert werden sollte/muß.

Ich habe eine ButtonClick-Prozedur, bei der in zwei Tabellen (Tabelle_1 und Tabelle_2) nacheinander “INSERT” durchgeführt wird.
Für den Insert in Tabelle_2 ist der Primareykey aus Tabelle_1 notwendig. Den hole ich mir mit “CURRVAL” aus der Sequenz von Tabelle_1.
Das ganze läuft nacheinander durch verschiedne SQL-Zuweisungen über eine ZEOS-TZQuery-Variable (Eingabe) die ich am Anfang initialisiere und am Ende wieder frei gebe (FreeAndNil(Eingabe)).

Die nacheinander ablaufenden Vorgänge sind in einem try-except-Block verpackt.

Tabelle_2 hat jetzt eine UNIQUE-Constraint, die nicht verletzt werden darf. Wenn sie Verletzt wird, dann tritt der except-Fall ein und eine Fehlermeldung wird geworfen. Die Änderung an Tabelle_2 findet nicht statt, wird zurückgewiesen. Allerdings zählt das Autoincrement von Tabelle_2 trotzdem hoch. Das kann man mit PGAdmin nachvollziehen.

Nach dem Zurückweisen müssten drei Dinge rückgängig gemacht werden (von hinten nach vorne):

1.) Das Increment von Tabelle_2 wieder um 1 reduzieren um den Wert nicht zu vergeuden.

2.) Den Eintrag in Tabelle_1 wieder löschen und 3.) auch dort das Increment der Sequenz um 1 reduzieren. Die wurde ja auch hochgezählt.

Ich dachte an einen Trigger in Postgres, aber der müsste auf die Unique-Verletzung von Tabelle_2 reagieren. Die Trigger reagieren aber auf BEFORE/AFTER INSERT/UPDATE/DELETE

DELETE des Datensatz an Tabelle_1 müsste dann die Folge des Triggers sein. Den Datensatz löschen und beide Autoincremente um 1 reduzieren.

Die UNIQUE-Bedingung von Tabelle_2 sind eine Datumsspalte und eine Nummernspalte. Jede Nummer darf es zu jedem Datum nur einmal geben.

Wie löst man sowas?

Ist die Vorgehensweise überhaupt sinnvoll für Mehrbenutzerbetrieb? Es sind zwar nur zwei bis drei Benutzer die parallel Eingaben vornehmen würden und es erscheint unwahrscheinlich dass zwei Nutzer absolut zeitgleich ihre “Inserts” an die Tabellen losschicken. Aber wenn Nutzer 1 seine “Inserts” losschickt bekommt er über CURRVAL ja den Sequenzwert zurück mit dem die zweite Tabelle gefüttert wird. Kann man davon ausgehen, dass Postgres das richtig macht und jedem die richtigen Sequenzwerte mitgeteilt werden? Oder kann das zu Durcheinander führen? Im Einzelnutzerbetrieb funktioniert die Vorgehensweise tadellos, bis auf den Fall, dass man sich ggf. vertut und eine bereits vergebende Nummer nochmal verwendet werden soll.

Ich kann natürlich die UNIQUE-Bedingung rausnehmen und doppelte Nummern zulassen. Das müsste dann nachträglich händisch in der Übersichtstabelle mit den Augen erkannt und entsprechend korrigiert werden, aber ich wollte diesen möglichen Bedienerfehler software- bzw. db-seitig abfangen.

Wenn in andern Feldern Mist eingetragen wird ist das nicht tragisch. Per Update-Befehl auf die DB ist das einfach zu korrigieren. Eine Update-Prozedur gibt es, zumal bei Anlage der Datensätze nicht zwingend schon alle Informationen bekannt sind und nachträglich ergänzt werden müssen.

Vielen Dank für Eure Meinungen

Ja, das ist das erwartete und dokumentierte Verhalten.

Die einzige Aufgabe eines Primärschlüssels ist es, einen Datensatz eindeutig zu identifizieren. Der eigentliche Wert völlig irrelevant. Es ist egal, ob das eine 1,2,42,-37584 oder 965398963 ist.

Du musst nichts tun. Selbst wenn Du 10000 Werte pro Sekunden 24 Stunden am Tag, 7 Tage die Woche 365 Tage im Jahr “verschwendest” wirst Du 29247120 Jahre brauchen um eine Überlauf der Sequence zu bekommen (bigint).

Wenn Deine Spalte “nur” ein integer ist, und Du jede Sekunden eines Tages einen Wert “verbrauchst” (also 86400 mal am Tag so einen Fehler hast), dauert es immer noch rund 68 Jahre bis der Wertebereich erschöpft ist. Zeit genug die Spalte auf bigint zu ändern.

1 „Gefällt mir“

ergänzend zu @castorp : versuche nicht, eine Sequenz wieder zu reduzieren. Du kannst nicht ausschließen, daß diese in einer anderen Transaktion ebenfalls verändert wurde. Das kann in einem völligen Chaos enden. don’t do that!

Falls Du absehen kannst, daß INTEGER zu klein ist, dann nehme gleich BIGINT. Eine spätere Änderung, wenn die Tabelle sehr groß ist, ist schlicht mit Schmerzen verbunden.

Die Primärschlüssel sind als Serial oder Bigserial bei Erstellung der Tabellen mit PGAdmin ausreichend dimensioniert. Das “Vergeuden” des Primärschlüssel ist auch nicht so mein Problem, sondern dass in Tabelle_2 die Verknüpfung zum frisch erzeugten Datensatz aus Tabelle_1 nicht stattfindet und ich vor einem “neuen Anlauf” erst die Fehlversuche “sauber rückgängig” machen wollte/will. Aber das brauche ich eigentlich gar nicht.
Ich muß nach dem “Rauswurf” in Tabelle_2 einfach nochmal versuchen den in Tabelle_1 erzeugten Datensatz in Tabelle_2 “neu” zu verknüpfen. Und zwar OHNE an den Schlüsseln zu schrauben, sondern den nächsten nehmen der zur Verfügung steht. Den aus Tabelle_1 habe ich ja noch…

Da muß ich mir mal überlegen wie ich in Lazarus auf den “Rauswurf” reagieren kann…
Vielen Dank für Eure Anregungen!

Aber das hast Du doch geschrieben: “Das Increment von Tabelle_2 wieder um 1 reduzieren um den Wert nicht zu vergeuden.

Diesen Teil verstehe ich nicht:

Es klingt so als wäre der Unique constraint auf Tabelle_2 eine Kombination aus einem serial (oder dem modernen identity) und einem Datum - das klingt für mich erst mal komisch. Ich kann mir eigentlich keine Situation vorstellen, in der ein Unique constraint über mehrere Spalten Sinn macht, wenn eine dieser Spalten auch noch eine serial/identity Spalte ist - die ist ja per Definition immer unique.

Ich kann mit Namen wie “Tabelle 1” und Tabelle 2" wenig anfangen. Könntest Du die vollständigen CREATE TABLE Befehle beider Tabellen zeigen (incl aller Unique, Primary und Foreign Key Constraints)?

1 „Gefällt mir“

Nachdem was ich hier gelesen habe, kann man den PK “vergeuden”, deshalb meinte ich, dass ich da nicht mehr so ein Problem damit habe. Ich wollte einfach bei fehlgeschlagenem Zugriff die begonnenen Änderungen wieder “sauber rückgängig” machen.

Ich habe momentan den Rechner nicht zur Verfügung wo ich drauf bastel.

In dem Link zum Lazarusforum beschreibe ich grob um was für eine Art Anwendung es sich handelt.
Es ist eine Fahrzeugversteigerung die (fast) immer am ersten Mittwoch im Monat stattfindet. Also gibt es eine Tabelle, die nimmt nur die Datumswerte der Versteigerungen auf. Eine weitere Tabelle enthält nur Zahlen von 1 bis 200. Diese sin gleichzeitig der PK. Die PK dieser beiden Tabellen sind FK meiner besagten Tabelle_2, eine Koppeltabelle. Es kann zu jedem Datumseintrag nur eine Versteigerungsnummer geben. deshalb das UNIQUE.
Wenn ein Fahrzeug stehen bleibt und erste im nächsten Monat versteigert wird, dann wird nur die Tabelle_2 erweitert und eine neue Datum’s-Nummer-Verbindung hergestellt. Der Datensatz in Tabelle_1 bleibt unberührt. Da stehen so Dinge wie Fahrgestellnummer, Erstzulassung, Tachostand usw. drin. Das ändert sich ja nicht, nur weil die Karre nicht versteigert wurde. Es darf somit halt zu jedem Datum nur eine Versteigerungsnummer gehören. Der “Stehenbleiber” darf bei Datum einen Monat weiter seine Nummer entweder behalten oder er bekommt eine neue Nummer. Dann darf die neue Nummer in dem Monat nicht anderweitig vergeben werden. Das sieht der Anwender aber in der Übersicht, welche Nummern schon weg sind.
Im günstigsten Fall gibt es keine Überhänge und ein neuer Monat beginnt frisch bei “1”. Jetzt könnte es passieren, dass der Kollege/die Kollegin (es sind nur drei) welche die Autos eingeben sollen, sich mal vertut und die Nummer nicht fortschreibt. Er ist bei 5 angekommen, will die 6 Wegschreiben, hat aber in der Eingabemaske die “5” nicht durch “6” ersetzt. Durch die UNIQUE-Bedingung moppet die DB. Jetzt müsste die Lazarus-Prozedur nach der nächsten freien Nummer suchen und diese für das Wegschreiben verwenden. Die nächste freie Nummer findet man z.B. mit MAX.

Bisschen blöd ist, wenn es mehrere “Stehenbleiber” gab, die ihre alte Nummer behalten haben. Wenn die “9”, die “22” und die “40” stehengeblieben sind, dann ist MAX erstmal 40 und es geht mit 41 weiter.
Jetzt erkennt aber der Kollege/Kollegin in der Übersichtsview, dass zum x.ten-Versteigerungsdatum schon Datensätze mit den Nummern “1 bis 5”, “9”, “22” und “40” existieren. Das System schlägt ihm die “41” vor. Kann er nehmen, er kann aber auch zwischen “9” und “22” weiter machen oder zwischen “5” und “9”. Wichtig ist nur, dass zum x.ten-Datum jede Nummer nur einmal existieren darf.
Bisher läuft alles über Excel und Überhänge werden händisch aus einer Liste in die Nächste kopiert. Dabei behalten sie in der Regel ihre Nummern, manchmal aber auch nicht. Hängt auch davon ab, wo die Karren in der Halle stehen usw. Die Nummern werden mit Klebeetiketten auf den linken Scheinwerfer geklebt. Ändert der Bolide durch stehenbleiben seine Nummer, wird sie runter gekratzt und neu geklebt. Behält er sie, braucht sie nicht abgekratzt zu werden. Aber der Datensatz mit Fahrgestellnummer wird nicht kopiert. Es gibt ihn genau einmal. Über weitere Tabellen werden beispielsweise zu diesem Datensatz noch die Mängel, Bemerkungen usw. verknüpft. Das bleibt alles bestehen. An der Objektbeschreibung ändert sich auch erstmal nix. Es sei denn ein Mangel war die Ursache für’s Stehenbleiben. Dann wird der ergänzt. Ist dann aber immer präsent, auch in der Versteigerung aus der Vergangenheit, wo er zunächst gefehlt hat. Aber um das auch noch abzufangen, hab’ ich keine Lust. Das ist dann auch egal. Wenn die Karren verkauft sind, dienen die Daten, hauptsächlich die Erlöse, zu Auswertzwecken. Das ist der Hauptgrund für mein Anliegen das alles auf eine DB umzustellen, dass man auch in einigen Jahren noch Wertentwicklungen nachvollziehen kann. Das geht zwar jetzt auch, aber umständlich und nicht vollumfänglich.

Hm, ich verstehe nicht wirklich, was da alles passiert. Bleiben wir bei dem Eingangsproblem.
Du brauchst die Sequenz nicht zurückzudrehen, das macht man nicht. (Wurde schon geklärt)

Allgemein:
Du kannst den Forumsbeitrag von Lazarus ruhig verlinken, sowohl hier, als auch dort Deine Frage von hier, spart vielleicht etwas Schreibarbeit, erhöht das Verständnis und den gemeinsamen Nutzen.

Rückbau von Teiländerungen:
Auch das macht man nicht. Kann man, aber die DB kann es viel besser. Dafür ist sie ausgelegt und das erfolgt ganz simpel im Rahmen einer(!) Transaktion. Alle Änderungen, die zusammen gehören, werden in einer Transaktion geschickt. Geht irgendetwas schief, wird die gesamte Transaktion, alles was bis da hin geändert wurde, automatisch zurück gesetzt. Du musst dafür gar nichts machen, außer Dein Try Except, das auf das Scheitern hinweißt und Deine Anwendung an der Stelle auf einen definierten Stand bringt.
Bei einem einzelnen Insert oder Update oder Delete ist man automatisch in einer(1) Transaktion. Kein Problem. Bei mehreren Befehlen entsprechend in mehreren Transaktionenen. Um das zu einer Transaktion zu machen, verwendet man einen “anonymous block”:

do $$ 
<<block_label>>
begin
    -- do something
    
    -- do something else
    
    -- and so on
end block_label $$;

Innerhalb des Blocks machst Du keine Fehlerbehandlung (in Deinem Fall).
Nur Deine 2 Inserts.
Scheitern tun sie im Zweifel von allein, das roleback der Transaktion erfolgt ebenfalls von allein, ebenso das Raise des Fehlers.
Eigenes Raise oder Fehlerbehandlung nur, wenn es wegen komplexer Vorgänge nötig ist.

currval Nutzung
Die Nutzung von Sequence und der Zugriff auf currval und nextval ist eine (alte) Möglichkeit, Sequence Werte weiter zu verwenden (und nicht schlecht). Es gibt aber ein passenderes Verfahren names RETURNING.
Möchtest Du in einem folgenden Schritt Daten aus dem aktuellen weiterverwenden, verwendest Du die Returning Clause. Damit kannst Du alle beteiligten, finalen Werte Deiner Operation ausgeben/ zurückgeben und im nächsten Statement weiter verwenden. Dies können Sequence Werte sein, aber auch andere, auch bspw. durch Trigger manipulierte Werte, egal.

Daraus folgt, Du brauchst für den Zugriff auf die Sequence Werte gar keine Sequence mehr bzw. kein currval, nextval. Du könntest modernere / andere Verfahren nutzen. (Ich persönlich verwende weiterhin gerne Sequences, aber das ist ein anderes Thema)

Block

Returning

Hmm, so ganz verstehe ich das Problem immer noch nicht. Die Zahl für Tabelle_2 wird ja nicht über eine Sequence vergeben, da muss also nichts zurückgestellt werden. Das “Verbrauchen” der Werte für die Sequence aus Tabelle 1 spielt ja keine Rolle. Und wenn Du eine Tabelle mit den Werten von 1-200 hast, warum gibt es dann in Tabelle_2 eine autoincrement Spalte?

Wenn ich das richtig verstehen, dann müssen aber die Einträge in Tabelle_1 und Tabelle_2 gar nicht unbedingt in ein und derselben Transaktion laufen. Das Anlegen der Stammdaten (Tabelle_1) ist ja unabhängig vom Einstellen des Fahrzeuges für eine Versteigerung. Warum machst Du nicht zwei Transaktionen daraus?

Also so etwas in der Art:

begin;
  insert into tabelle_1 (....)
  returning id;
commit;

Die ID aus dem Schritt kannst Du Dir ja in einer Variable merken. Dann ermittelst Du die nächste Versteigerungsnummer und erstellst den Eintrag in tabelle_2

begin
    insert into tabelle_2 (fahrzeug_id, datum, v_nr) 
    values (<id aus dem vorherigen Schritt>, ..., <manuelle Eingabe>);
commit;

Wenn das scheitert, kannst Du das mit dem Ermitteln nochmal versuchen. Oder Du sperrst Die beiden Tabellen (tabelle_2 und die Nummerntabelle) und holst den nächsten freien Wert direkt von dort:

begin;
lock nummern_tabelle in exclusive mode;
lock tabelle_2 in exclusive mode;
insert into tabelle_2 (datum, v_nr)
select date '2023-11-15', x.nr
from (
   select n1.nr
   from nummern_tabelle n1
   where not exists (select *
                     from tabelle_2 t2
                     where t2.v_nr = n1.nr)
   order by n1.nr 
   limit 1
) x   
commit;

Das Sperren ist wichtig damit nicht zwei konkurrierende Transaktionen den gleichen Wert verwenden.

Ich habe mal die SQL-Definitionen der beiden Tabellen hier:
Tabelle_1:

CREATE TABLE IF NOT EXISTS public.tbl_versteigerung
(
    versteigerung_id bigint NOT NULL DEFAULT nextval('tbl_versteigerung_id_seq'::regclass),
    abgebende_dienststelle bigint NOT NULL,
    kfz_briefnummer character varying(255) COLLATE pg_catalog."default",
    kennzeichen character varying(10) COLLATE pg_catalog."default",
    fahrzeugart bigint,
    fabrikat bigint,
    motor character varying(255) COLLATE pg_catalog."default",
    fahrgestellnummer character varying(17) COLLATE pg_catalog."default",
    hubraum character varying(10) COLLATE pg_catalog."default",
    leistung character varying(10) COLLATE pg_catalog."default",
    erstzulassung character varying(20) COLLATE pg_catalog."default",
    hu character varying(20) COLLATE pg_catalog."default",
    laufleistung character varying(50) COLLATE pg_catalog."default",
    adv_nummer character varying(255) COLLATE pg_catalog."default",
    funktionsnummer character varying(255) COLLATE pg_catalog."default",
    aktenzeichen character varying(255) COLLATE pg_catalog."default",
    getriebe character varying(255) COLLATE pg_catalog."default",
    unfall character varying(255) COLLATE pg_catalog."default",
    mindestgebot numeric(20,2),
    fahrbereit character varying(255) COLLATE pg_catalog."default",
    bild character varying(255) COLLATE pg_catalog."default",
    geloescht boolean NOT NULL DEFAULT false,
    CONSTRAINT tbl_versteigerung_pkey PRIMARY KEY (versteigerung_id),
    CONSTRAINT abgebende_dienststelle_fk FOREIGN KEY (abgebende_dienststelle)
        REFERENCES public.tbl_dienststellen (dienststelle_id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION,
    CONSTRAINT fabrikat_fk FOREIGN KEY (fabrikat)
        REFERENCES public.tbl_fahrzeuge (fahrzeug_id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION,
    CONSTRAINT fahrzeugart_fk FOREIGN KEY (fahrzeugart)
        REFERENCES public.tbl_fahrzeugart (fahrzeugart_id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
)

TABLESPACE pg_default;

Und hier Tabelle_2 (die Koppeltabelle):

CREATE TABLE IF NOT EXISTS public.htbl_versteigerung_nummer_datum_kaeufer
(
    versteigerungsnummer_id bigint NOT NULL,
    datum_id bigint NOT NULL,
    kunde_id bigint,
    stehengeblieben boolean NOT NULL DEFAULT false,
    geloescht boolean NOT NULL DEFAULT false,
    htbl_vndk_id bigint NOT NULL DEFAULT nextval('htbl_vndk_id_seq'::regclass),
    erloes numeric(20,2),
    versteigerung_id bigint NOT NULL,
    CONSTRAINT htbl_vndk_id_pkey PRIMARY KEY (htbl_vndk_id),
    CONSTRAINT unique_nummer_datum UNIQUE (versteigerungsnummer_id, datum_id),
    CONSTRAINT kunden_id_fk FOREIGN KEY (kunde_id)
        REFERENCES public.tbl_kunden (kunde_id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
        NOT VALID,
    CONSTRAINT versteigerungsdatum_id_fk FOREIGN KEY (datum_id)
        REFERENCES public.tbl_datum (datum_id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
        NOT VALID,
    CONSTRAINT versteigerungsnummer_id_fk FOREIGN KEY (versteigerungsnummer_id)
        REFERENCES public.tbl_versteigerungsnummer (nummer_id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
        NOT VALID
)

TABLESPACE pg_default;

ALTER TABLE IF EXISTS public.htbl_versteigerung_nummer_datum_kaeufer
    OWNER to postgres;

COMMENT ON TABLE public.htbl_versteigerung_nummer_datum_kaeufer
    IS 'Hilfstabelle für die Kombination von Versteigerung mit Nummer, Datum, Stehen geblieben und Käufer';

tbl vorneweg steht bei mir für Tabelle, htbl bedeutet Hilfstabelle

Hier mal die Prozedur in Lazarus:

var Eingabe: TZQuery;
  pk_tbl_verst: Integer;
begin
  Eingabe:= TZQuery.Create(nil);
  Eingabe.Connection:= ZeosDatenModul.PGConnect;
 try
  with Eingabe do begin
    SQL.Clear;
    SQL.Add('INSERT INTO "tbl_versteigerung" VALUES (DEFAULT, :abgeb, :brief, :kennz, :art, :fab, :mot, :fin, :hub, :leist, :ez, :hu, :km, :adv, :fkt, :az, :get, :unfall, :mindgeb, :fahrb);');
    Params.Clear;
    Params.CreateParam(ftInteger,'abgeb',ptInput);
    ParamByName('abgeb').AsInteger:= StrToInt(RxLupCoboAbgebDienst.Value);
    Params.CreateParam(ftString,'brief',ptInput);
    ParamByName('brief').AsString:= DBEditKfzBrief.Text;
    Params.CreateParam(ftString,'kennz',ptInput);
    ParamByName('kennz').AsString:= DBEditKennzeichen.Text;
    Params.CreateParam(ftInteger,'art',ptInput);
    ParamByName('art').AsInteger:= StrToInt(RxDBLupCobArt.Value);
    Params.CreateParam(ftInteger,'fab',ptInput);
    ParamByName('fab').AsInteger:= StrToInt(RxDBLupFabrikat.Value);
    Params.CreateParam(ftString,'mot',ptInput);
    ParamByName('mot').AsString:= RxDBCmbMotor.Text;
    Params.CreateParam(ftString,'fin',ptInput);
    ParamByName('fin').AsString:= DBEditFIN.Text;
    Params.CreateParam(ftString,'hub',ptInput);
    ParamByName('hub').AsString:= DBEditHubraum.Text;
    Params.CreateParam(ftString,'leist',ptInput);
    ParamByName('leist').AsString:= DBEditLeistung.Text;
    Params.CreateParam(ftString,'ez',ptInput);
    ParamByName('ez').AsString:= DBEditEZ.Text;
    Params.CreateParam(ftString,'hu',ptInput);
    ParamByName('hu').AsString:= RxDBComb_HU.Text;
    Params.CreateParam(ftString,'km',ptInput);
    ParamByName('km').AsString:= RxDBComb_Km_Stand.Text;
    Params.CreateParam(ftString,'adv',ptInput);
    ParamByName('adv').AsString:= DBEdit_ADV_NR.Text;
    Params.CreateParam(ftString,'fkt',ptInput);
    ParamByName('fkt').AsString:= DBEdit_Fkt_NR.Text;
    Params.CreateParam(ftString,'az',ptInput);
    ParamByName('az').AsString:= DBEdit_AZ.Text;
    Params.CreateParam(ftString,'get',ptInput);
    ParamByName('get').AsString:= RxDBCombGetriebe.Text;
    Params.CreateParam(ftString,'unfall',ptInput);
    ParamByName('unfall').AsString:= RxDBCombUnfall.Text;
    Params.CreateParam(ftCurrency,'mindgeb',ptInput);
    ParamByName('mindgeb').AsCurrency:= RxDBMindGeb.Value;
    Params.CreateParam(ftString,'fahrb',ptInput);
    ParamByName('fahrb').AsString:= RxDBCombFahrbereit.Text;
    ExecSQL;

    SQL.Clear;
    SQL.Add('SELECT CURRVAL('+'''"tbl_versteigerung_id_seq"'''+');');
    Open;
    FindFirst;
    pk_tbl_verst:= Fields[0].AsInteger;

    SQL.Clear;
    SQL.Add('INSERT INTO "htbl_versteigerung_nummer_datum_kaeufer"(versteigerungsnummer_id, datum_id, stehengeblieben, geloescht, htbl_vndk_id, versteigerung_id) VALUES (:verstnr, :datum, DEFAULT, DEFAULT, DEFAULT, :pk_verst);');
    Params.Clear;
    Params.CreateParam(ftInteger,'verstnr',ptInput);
    ParamByName('verstnr').AsInteger:= StrToInt(RxDBLupNR.Value);
    Params.CreateParam(ftInteger,'datum',ptInput);
    ParamByName('datum').AsInteger:= StrToInt(RxDBLupDatum.Value);
    Params.CreateParam(ftInteger,'pk_verst',ptInput);
    ParamByName('pk_verst').AsInteger:= pk_tbl_verst;
    ExecSQL;
  end;
 except
   on E: EDatabaseError do ShowMessage('Neuanlage Fahrzeug nicht gelungen');
 end; 

Es wird in tbl_versteigerung ein Datensatz eingetragen. Dieser lässt die Sequenz hochzählen. Die Sequenz lese ich aus und nehme sie für htbl_versteigerung_nummer_datum_kaeufer. Die Tabelle hat den UNIQUE und es wird an dieser Tabelle der Rollback ausgelöst. Jetzt existiert in der tbl_versteigerung ein Eintrag, der keinen korrespondierenden Eintrag in der htbl_versteigerung_nummer_datum_kaeufer hat.
Wenn sich jetzt was bauen lässt, das “voraussieht”, dass der Insert in htbl_versteigerung_nummer_datum_kaeufer scheitern wird, dann auch nicht den Datensatz in tbl_versteigerung anlegen.

Wie spricht man denn die “Block Structure” per externer Programmiersprache an und übergibt bzw. liest Werte?

Was über RETURNING zurück kommt, kann ich vermutlich ähnlich oben im Quelltext via Parameter auslesen und weiterverarbeiten.

Bitte kein LOCK TABLE ... IN EXCLUSIVE MODE. Das ist nie eine gute Lösung: es läßt Autovacuum-Worker sterben und führt zu Leid und Hämorrhoiden.

So wie ich die Aufgabe verstehe, geht es nur darum, einen Vorschlag einer Nummer zu generieren, die es noch nicht gibt. Wenn zufällig ein anderer von den drei Mitarbeitern zuerst diese Nummer gewählt hat, gibt es halt eine Unique-Constraint-Verletzung und man gibt dem User eine Fehlermeldung und schlägt ihm eine neue Zahl vor. “Der Sitzplatz, den Sie reservieren wollen, ist leider schon vergeben. Wählem Sie einen anderen”. Es wird schon nicht so oft vorkommen.

Naja, wenn man die Locks schnell genug wieder freigibt, dann sehe ich da kein so großes Problem. Wenn es wirklich notwendig ist, eine “freie” Nummer zu finden, dann sehe ich da keine andere Möglichkeit mit dem aktuellen Tabellendesign.

Das Problem ist nicht die Dauer der Locks, sondern wie oft sie vorkommen. Aber natürlich habe ich übertrieben: wenn man so einen Lock nur selten nimmt, ist es kein großer Schaden.

Wenn ich die Frage richtig verstanden habe, ist die freie Nummer, die so gefunden wird, ja nur ein Vorschlag, den der Benutzer verändern kann. Dann müßte man diese Sperre (und eine Transaktion) über eine Benutzerinteraktion hinweg halten, und das ist sicher keine gute Idee.

Naja, eine steigende Dauer (Datenvolumen) und eine steigende Frequenz (Nutzeranzahl bzw. Aktivität) erhöht die Anzahl der Ereignisse (das Vorkommen).Also geht es um die Skalierbarkeit eines gewählten / implementierten Verfahrens.
Ich würde aber auch alles andere tun, bevor ich mit (selbst implementierten) Locks anfange.
Und da verstehe ich das Problem nicht.

Wir reden von freien Nummern. Das ist eine unendlich verfügbare Ressource in einer DB. Wir reden nicht von z.B. “items in stock” oder sowas. Die DB kann sich in Sekundenbruchteilen eine neue Nummer ausdenken, also eine Sequenz erhöhen oder im Zweifel eine UUID randomisieren, was auch immer. Das sollte im Rahmen einer Transaktion problemlos funktionieren.
Wenn man (ohne Not) mehrere Transaktionen daraus macht (2 statt 1 reicht bereits), wird es halt schwieriger.

Was die Locks betrifft, habe ich mich unklar ausgedrückt. Natürlich ist die Dauer von Sperren eine wichtige Sache, und lange Sperren sind von Übel. Aber bei starken Sperren gibt es ein zusätzliches Problem, das nicht mit der Dauer zusammenhängt: diese Sperrem stehen in Konflikt mit Autovacuum. Wenn jetzt Autovacuum auf einer Tabelle läuft und ich so eine Sperre anfordere, stirbt Autovacuum freiwillig eine Sekunde später, um mich nicht zu blockieren. Wenn ich das öfter mache, kann Autovacuum nie fertig werden. Wenn Autovacuum nie fertig wird, habe ich über kurz oder lang ein massives Problem.

Was das eigentliche Problem anbelangt, so werden wir wohl auf @PGThustra warten müssen. Niemand anderer kann entscheiden, welcher Ansatz sinnvoll ist. Ich möchte nicht weiter theoretische Überlegungen anstellen. Aber ich stimme zu: die einfachste brauchbare Lösung ist sehr oft die beste.

1 „Gefällt mir“

Hallo,
vielen Dank für Eure Mitteilungen.

Leider geht es momentan an meinem Projekt nur etwas zäh weiter und “richtig am Start” ist das ganze auch noch nicht mal testweise. Momentan ist mein “Getüftelt” so, dass ich ja nur alleine ausprobiere. Also die DB könnte auch SQLite sein, sprich kein Mehrbenutzerbetrieb. Das wird alles noch etwas dauern.
Aber trotzdem danke ich allen Mitlesen und Ideengebern für ihre Mitwirkung.

Postgres- und Lazarusforum sind beides sehr gute Foren stelle ich mal wieder fest. :ok_hand: :+1:

Dieses Thema wurde automatisch 60 Tage nach der letzten Antwort geschlossen. Es sind keine neuen Antworten mehr erlaubt.