2009-04-27 5 views
5

Ich habe einen Primärschlüssel, den ich nicht automatisch inkrementieren möchte (aus verschiedenen Gründen), und so suche ich nach einer Möglichkeit, dieses Feld einfach zu erhöhen, wenn ich INSERT. Ich meine einfach ohne gespeicherte Prozeduren und ohne Trigger, also nur eine Reihe von SQL-Befehlen (vorzugsweise einen Befehl). HierMöglich, ein manuelles Inkrement mit nur einfachem SQL INSERT zu implementieren?

ist, was ich versucht habe, so weit:

BEGIN TRAN 

INSERT INTO Table1(id, data_field) 
VALUES ((SELECT (MAX(id) + 1) FROM Table1), '[blob of data]'); 

COMMIT TRAN; 

* Data abstracted to use generic names and identifiers 

Wenn jedoch ausgeführt, der Befehl Fehler und sagte, dass

„Unterabfragen nicht in diesem Kontext erlaubt sind nur skalare Ausdrücke. sind erlaubt "

Also, wie kann ich das tun/was mache ich falsch?


EDIT: Da es als Gegenleistung darauf hingewiesen wurde, um die Tabelle in der bereits mindestens 1 Zeile garantiert eingefügt ist zu haben.

Antwort

9

Sie verstehen, dass Sie Kollisionen haben werden richtig?

brauchen Sie so etwas wie dies zu tun, und dies könnte dazu führen, Deadlocks so ganz sicher sein, was Sie versuchen, hier

DECLARE @id int 
BEGIN TRAN 

    SELECT @id = MAX(id) + 1 FROM Table1 WITH (UPDLOCK, HOLDLOCK) 
    INSERT INTO Table1(id, data_field) 
    VALUES (@id ,'[blob of data]') 
COMMIT TRAN 

zu erreichen, um die Kollision Sache zu erklären, ich habe einige Code zur Verfügung gestellt

zuerst diese Tabelle erstellen, und legen Sie eine Zeile

CREATE TABLE Table1(id int primary key not null, data_field char(100)) 
GO 
Insert Table1 values(1,'[blob of data]') 
Go 

nun zwei Abfragefenster öffnen und führen dies zugleich

declare @i int 
set @i =1 
while @i < 10000 
begin 
BEGIN TRAN 

INSERT INTO Table1(id, data_field) 
SELECT MAX(id) + 1, '[blob of data]' FROM Table1 

COMMIT TRAN; 
set @i [email protected] + 1 
end 

Sie werden eine Reihe von diesen

Server finden Sie unter: Msg 2627, Ebene 14, Status 1, Zeile 7 Verletzung der PRIMARY KEY-Einschränkung 'PK__Table1__3213E83F2962141D'. Kann doppelten Schlüssel in Objekt "Dbo.Table1" nicht einfügen. Die Anweisung wurde beendet.

+1

Warum würde es Kollisionen geben? Solche Dinge werden von der Transaktion behandelt, ja? Wo kämen die Deadlocks hinzu? – cdeszaq

+2

nicht, wenn Sie unter der Standardtransaktionsebene laufen (lesen Sie, verpflichtet), was verbietet zwei Threads, den gleichen maximalen Wert zu lesen? – SQLMenace

+0

Deadlocks können vermieden werden, indem Sie den 'Holdlock' Hinweis weglassen –

2

Versuchen Sie stattdessen:

INSERT INTO Table1 (id, data_field) 
SELECT id, '[blob of data]' FROM (SELECT MAX(id) + 1 as id FROM Table1) tbl 

Ich würde für eine beliebige Anzahl von Gründen, obwohl es auf diese Weise tun (Performance, Transaktionssicherheit, etc.) nicht empfehlen

+0

Was würden Sie empfehlen, sie stattdessen tun? – GernBlandston

+0

Da das ID-Feld indiziert ist, ist der Performance-Hit signifikant? Außerdem sollte es, wenn es in eine Transaktion eingebunden wird, atomar und sicher sein, richtig? – cdeszaq

+0

Ich bin nur nicht sicher, ob die Tabelle gesperrt wird, wenn Sie den SELECT MAX (ID) .. Teil tun. Wenn nicht, gibt es das Potenzial, dass zwei Threads die gleiche ID erhalten könnten. Jemand, der SQL-Server ein bisschen besser kennt, könnte Ihnen sagen, ob das wirklich sicher ist oder nicht - meine Intuition ist, dass es nicht ist, aber ich bin mir wirklich nicht sicher. –

0

Wenn Sie es in einem zu tun sind Trigger, können Sie sicherstellen, dass es ein „anstelle von“ Trigger und tun es in ein paar Erklärungen:

DECLARE @next INT 
SET @next = (SELECT (MAX(id) + 1) FROM Table1) 

INSERT INTO Table1 
VALUES (@next, inserted.datablob) 

Das einzige, was Sie müssen vorsichtig sein würde, ist Gleichzeitigkeit - wenn zwei Reihen inser sind Gleichzeitig könnten sie versuchen, den gleichen Wert für @next zu verwenden, was zu einem Konflikt führen würde.

Erzielt dies, was Sie wollen?

+0

Nein, Trigger können nur verwendet werden, wenn es keinen anderen Weg gibt. – cdeszaq

0

Dies sollte funktionieren:

INSERT INTO Table1 (id, data_field) 
SELECT (SELECT (MAX(id) + 1) FROM Table1), '[blob of data]'; 

Oder diese (Ersatz LIMIT für andere Plattformen):

INSERT INTO Table1 (id, data_field) 
SELECT TOP 1 
    MAX(id) + 1, '[blob of data]' 
FROM 
    Table1 
ORDER BY 
    [id] DESC; 
+0

Irgendwelche Kommentare, warum -1 von jemandem? – gbn

+1

Drive-by Downvoter bedeutet wahrscheinlich, dass diese Abfrage doppelte IDs in einer Umgebung mit hoher Parallelität erzeugt. –

0
declare @nextId int 
set @nextId = (select MAX(id)+1 from Table1) 

insert into Table1(id, data_field) values (@nextId, '[blob of data]') 

commit; 

Aber vielleicht ein besserer Ansatz wäre eine skalare Funktion getNextId werden ('tabelle1 ')

+0

getNextId (

) wäre eine ... benutzerdefinierte gespeicherte Prozedur oder eine eingeschlossene? Falls vorhanden, auf welchen Plattformen läuft es, da ich nicht glaube, dass es Standard ist. – cdeszaq

+1

Ihre Funktion würde also dynamisches SQL verwenden? Oder eine separate Schlüsseltabelle? – gbn

1

Es könnte sein, weil es keine Datensätze gibt, so dass die Unterabfrage NULL zurückgibt ... versuchen Sie

INSERT INTO tblTest(RecordID, Text) 
VALUES ((SELECT ISNULL(MAX(RecordID), 0) + 1 FROM tblTest), 'asdf') 
+0

In diesem Fall ist es garantiert, dass es mindestens eine vorhandene Zeile gibt, aber gut daran zu erinnern. +1 – cdeszaq

+0

Sie können keine Abfrage in VALUES haben – SQLMenace

+0

@Jon: Poster versuchte dies bereits – gbn

0

Es scheint sehr seltsam, so etwas ohne IDENTITY (auto-increment) -Spalte zu machen, was mich die Architektur selbst in Frage stellt. Ich meine, im Ernst, das ist die perfekte Situation für eine IDENTITY-Kolumne. Es könnte uns helfen, Ihre Frage zu beantworten, wenn Sie die Gründe für diese Entscheidung erklären würden. =)

Having said that, einige Optionen sind:

  • ein INSTEAD OF-Trigger für diesen Zweck verwendet wird. Also, Sie würden Ihre INSERT tun (die INSERT-Anweisung würde keine ID übergeben müssen). Der Triggercode würde das Einfügen der entsprechenden ID behandeln. Sie müssen die von einem anderen Beantworter verwendete WITH-Syntax (UPDLOCK, HOLDLOCK) verwenden, um die Sperre für die Dauer des Triggers zu halten (der implizit in eine Transaktion eingeschlossen wird), um den Sperrtyp von "shared" auf "update" zu erhöhen "Schloss (IIRC).
  • Sie können die obige Idee verwenden, haben aber eine Tabelle, deren Zweck es ist, den letzten, in die Tabelle eingefügten Maximalwert zu speichern. Sobald die Tabelle eingerichtet ist, müssen Sie also nicht mehr jedes Mal eine SELECT MAX (ID) ausführen. Sie würden den Wert in der Tabelle einfach erhöhen. Dies ist sicher, vorausgesetzt Sie verwenden eine geeignete Verriegelung (wie besprochen). Auch dies vermeidet wiederholte Tabellen-Scans bei jedem INSERT-Vorgang.
  • Verwenden Sie GUIDs anstelle von IDs. Es ist viel einfacher, Tabellen über Datenbanken hinweg zusammenzuführen, da die GUIDs immer eindeutig sind (während Datensätze in Datenbanken widersprüchliche Ganzzahl-IDs haben). Um das Teilen von Seiten zu vermeiden, können sequenzielle GUIDs verwendet werden. Dies ist nur von Vorteil, wenn Sie die Datenbank zusammenführen müssen.
  • Verwenden Sie einen gespeicherten Prozess anstelle des Trigger-Ansatzes (da Trigger aus irgendeinem Grund vermieden werden sollen). Sie haben immer noch das Sperrproblem (und die Leistungsprobleme, die auftreten können). Aber Sprocs werden gegenüber dynamischem SQL (im Kontext von Anwendungen) bevorzugt und sind oft viel performanter.

Entschuldigung über das Wandern. Ich hoffe, das hilft.

1

Ich weiß nicht, ob jemand noch nach einer Antwort suchen, aber hier ist eine Lösung, die zu funktionieren scheint:

-- Preparation: execute only once 
    CREATE TABLE Test (Value int) 

CREATE TABLE Lock (LockID uniqueidentifier) 
INSERT INTO Lock SELECT NEWID() 

-- Real insert 

    BEGIN TRAN LockTran 

    -- Lock an object to block simultaneous calls. 
    UPDATE Lock WITH(TABLOCK) 
    SET  LockID = LockID 

    INSERT INTO Test 
    SELECT ISNULL(MAX(T.Value), 0) + 1 
    FROM Test T 

    COMMIT TRAN LockTran 
1

Wir haben eine ähnliche Situation, wo wir erhöhen mussten und konnten nicht haben Lücken in die Zahlen. (Wenn Sie einen Identitätswert verwenden und eine Transaktion zurückgesetzt wird, wird diese Nummer nicht eingefügt und Sie haben Lücken, da der Identitätswert nicht zurückgesetzt wird.)

Wir haben eine separate Tabelle für letzte Nummer verwendet und entkernten mit 0

Unser Einsatz ein paar Schritte.

--increment die Anzahl aktualisieren dbo.NumberTable eingestellte Anzahl = Anzahl + 1

--find heraus, was die erhöhte Zahl wählen @number = Anzahl von dbo.NumberTable

- VCI Lager die Anzahl Insert in dbo.MyTable mit der @number

begehen oder ROLLBACK

Dies führt dazu, dass gleichzeitige Transaktionen in einer einzelnen Zeile verarbeitet werden, da jede gleichzeitige Transaktion wartet, weil die NumberTable gesperrt ist. Sobald die wartende Transaktion die Sperre erhält, erhöht sie den aktuellen Wert und sperrt ihn von anderen. Dieser aktuelle Wert ist die letzte verwendete Nummer. Wenn eine Transaktion zurückgesetzt wird, wird die NumberTable-Aktualisierung ebenfalls zurückgesetzt, sodass keine Lücken entstehen.

Hoffnung, das hilft.

Eine weitere Möglichkeit zur Ausführung einer einzelnen Datei besteht in der Verwendung einer SQL-Anwendungssperre. Wir haben diesen Ansatz für länger laufende Prozesse wie das Synchronisieren von Daten zwischen Systemen verwendet, sodass nur ein Synchronisationsprozess gleichzeitig ausgeführt werden kann.

+1

Sie heben einen guten Punkt über Rollbacks. Aber was verhindert, dass zwei Threads dieselbe Nummer bekommen? Verwenden Sie eine serialisierbare Transaktion? – Leigh

0

Irgendwelche Kritik daran? Funktioniert bei mir.

DECLARE @m_NewRequestID INT 
     , @m_IsError BIT = 1 
     , @m_CatchEndless INT = 0 

WHILE @m_IsError = 1 
    BEGIN TRY 
     SELECT @m_NewRequestID = (SELECT ISNULL(MAX(RequestID), 0) + 1 FROM Requests) 

     INSERT INTO Requests ( RequestID 
           , RequestName 
           , Customer 
           , Comment 
           , CreatedFromApplication) 
      SELECT RequestID = @m_NewRequestID 
        , RequestName = dbo.ufGetNextAvailableRequestName(PatternName) 
        , Customer = @Customer 
        , Comment = [Description] 
        , CreatedFromApplication = @CreatedFromApplication 
       FROM RequestPatterns 
       WHERE PatternID = @PatternID 

     SET @m_IsError = 0 
    END TRY 
    BEGIN CATCH 
     SET @m_IsError = 1 
     SET @m_CatchEndless = @m_CatchEndless + 1 
     IF @m_CatchEndless > 1000 
      THROW 51000, '[upCreateRequestFromPattern]: Unable to get new RequestID', 1 
    END CATCH 
Verwandte Themen