Überprüfen Sie, ob eine Zeile vorhanden ist, andernfalls fügen Sie sie ein

237

Ich muss eine gespeicherte T-SQL-Prozedur schreiben, die eine Zeile in einer Tabelle aktualisiert. Wenn die Zeile nicht vorhanden ist, fügen Sie sie ein. All diese Schritte werden von einer Transaktion umschlossen.

Dies ist für ein Buchungssystem, daher muss es atomar und zuverlässig sein . Es muss true zurückgeben, wenn die Transaktion festgeschrieben und der Flug gebucht wurde.

Ich bin neu in T-SQL und nicht sicher, wie ich es verwenden soll @@rowcount. Das habe ich bis jetzt geschrieben. Bin ich auf dem richtigen Weg? Ich bin sicher, das ist ein leichtes Problem für Sie.

-- BEGIN TRANSACTION (HOW TO DO?)

UPDATE Bookings
 SET TicketsBooked = TicketsBooked + @TicketsToBook
 WHERE FlightId = @Id AND TicketsMax < (TicketsBooked + @TicketsToBook)

-- Here I need to insert only if the row doesn't exists.
-- If the row exists but the condition TicketsMax is violated, I must not insert 
-- the row and return FALSE

IF @@ROWCOUNT = 0 
BEGIN

 INSERT INTO Bookings ... (omitted)

END

-- END TRANSACTION (HOW TO DO?)

-- Return TRUE (How to do?)
Whymarrh
quelle
1
Mögliches Duplikat von Lösungen für INSERT OR UPDATE auf SQL Server
Alex Angas

Antworten:

158

Schauen Sie sich den Befehl MERGE an . Sie können tun UPDATE, INSERTund DELETEin einer Erklärung.

Hier ist eine funktionierende Implementierung zur Verwendung MERGE
- Sie prüft, ob der Flug voll ist, bevor ein Update durchgeführt wird, andernfalls wird eine Einfügung durchgeführt.

if exists(select 1 from INFORMATION_SCHEMA.TABLES T 
              where T.TABLE_NAME = 'Bookings') 
begin
    drop table Bookings
end
GO

create table Bookings(
  FlightID    int identity(1, 1) primary key,
  TicketsMax    int not null,
  TicketsBooked int not null
)
GO

insert  Bookings(TicketsMax, TicketsBooked) select 1, 0
insert  Bookings(TicketsMax, TicketsBooked) select 2, 2
insert  Bookings(TicketsMax, TicketsBooked) select 3, 1
GO

select * from Bookings

Und dann ...

declare @FlightID int = 1
declare @TicketsToBook int = 2

--; This should add a new record
merge Bookings as T
using (select @FlightID as FlightID, @TicketsToBook as TicketsToBook) as S
    on  T.FlightID = S.FlightID
      and T.TicketsMax > (T.TicketsBooked + S.TicketsToBook)
  when matched then
    update set T.TicketsBooked = T.TicketsBooked + S.TicketsToBook
  when not matched then
    insert (TicketsMax, TicketsBooked) 
    values(S.TicketsToBook, S.TicketsToBook);

select * from Bookings
dance2die
quelle
6
Sehen Sie auch, warum Sie WITH (HOLDLOCK) für diese MERGE mögen könnten .
Eugene Ryabtsev
4
Ich denke, MERGE wird nach 2005 (also 2008+) unterstützt.
Samis
3
MERGE ohne WITH (UPDLOCK) kann zu Primärschlüsselverletzungen führen, die in diesem Fall schlecht wären. Siehe [Ist MERGE eine atomare Anweisung in SQL2008?] ( Stackoverflow.com/questions/9871644/… )
James
156

Ich nehme für jeden Flug eine einzelne Reihe an? Wenn ja:

IF EXISTS (SELECT * FROM Bookings WHERE FLightID = @Id)
BEGIN
    --UPDATE HERE
END
ELSE
BEGIN
   -- INSERT HERE
END

Ich gehe davon aus, was ich gesagt habe, da Ihre Vorgehensweise einen Flug überbuchen kann, da eine neue Zeile eingefügt wird, wenn maximal 10 Tickets vorhanden sind und Sie 20 buchen.

Gregory A Beamer
quelle
Ja. Es gibt 1 Reihe pro Flug. Ihr Code führt jedoch SELECT aus, prüft jedoch nicht, ob der Flug vor UPDATE voll ist. Wie macht man das?
2
Aufgrund der Rennbedingungen ist dies nur dann korrekt, wenn die aktuelle Transaktionsisolationsstufe serialisierbar ist.
Jarek Przygódzki
1
@ Martin: Die Antwort konzentrierte sich auf die vorliegende Frage. Aus der eigenen Aussage des OP "Alle diese Schritte werden von einer Transaktion umschlossen". Wenn die Transaktion korrekt implementiert ist, sollte das Thread-sichere Problem kein Problem sein.
Gregory A Beamer
14
@GregoryABeamer - Wenn Sie es einfach in eine unterdurchschnittliche BEGIN TRAN ... COMMITIsolationsstufe stecken, wird das Problem nicht behoben . Das OP spezifizierte, dass atomare und zuverlässige Anforderungen waren. Ihre Antwort spricht dies in keiner Form an.
Martin Smith
2
Wäre dies threadsicher, wenn (UPDLOCK, HOLDLOCK) zu SELECT hinzugefügt würde : IF EXISTS (SELECT * FROM Bookings (UPDLOCK, HOLDLOCK) WHERE FLightID = @Id)?
Jim
67

Übergeben Sie Updlock-, Rowlock- und Holdlock-Hinweise, wenn Sie die Existenz der Zeile testen.

begin tran /* default read committed isolation level is fine */

if not exists (select * from Table with (updlock, rowlock, holdlock) where ...)
    /* insert */
else
    /* update */

commit /* locks are released here */

Der Updlock-Hinweis zwingt die Abfrage, eine Update-Sperre für die Zeile zu aktivieren, falls diese bereits vorhanden ist, und verhindert, dass andere Transaktionen sie ändern, bis Sie sie festschreiben oder zurücksetzen.

Der Holdlock-Hinweis erzwingt, dass die Abfrage eine Bereichssperre durchführt, wodurch verhindert wird, dass andere Transaktionen eine Zeile hinzufügen, die Ihren Filterkriterien entspricht, bis Sie ein Commit durchführen oder ein Rollback durchführen.

Der Rowlock-Hinweis erzwingt die Sperrgranularität auf Zeilenebene anstelle der Standard-Seitenebene, sodass Ihre Transaktion andere Transaktionen nicht blockiert, die versuchen, nicht verwandte Zeilen auf derselben Seite zu aktualisieren (beachten Sie jedoch den Kompromiss zwischen reduzierter Konkurrenz und der Zunahme von Overhead sperren - Sie sollten vermeiden, eine große Anzahl von Sperren auf Zeilenebene in einer einzigen Transaktion zu verwenden.

Weitere Informationen finden Sie unter http://msdn.microsoft.com/en-us/library/ms187373.aspx .

Beachten Sie, dass Sperren verwendet werden, wenn die Anweisungen ausgeführt werden, die sie ausführen. Wenn Sie begin tran aufrufen, erhalten Sie keine Immunität gegen eine andere Transaktion, die Sperren für etwas einklemmt, bevor Sie dazu gelangen. Sie sollten versuchen, Ihr SQL so zu faktorisieren, dass Sperren für die kürzestmögliche Zeit gehalten werden, indem Sie die Transaktion so schnell wie möglich festschreiben (spät erwerben, früh freigeben).

Beachten Sie, dass Sperren auf Zeilenebene möglicherweise weniger effektiv sind, wenn Ihre PK ein Bigint ist, da das interne Hashing auf SQL Server für 64-Bit-Werte entartet ist (verschiedene Schlüsselwerte können auf dieselbe Sperren-ID hashen).

Cassius Porcus
quelle
4
Das Sperren ist SEHR wichtig, um eine Überbuchung zu vermeiden. Ist es richtig anzunehmen, dass eine in der IF-Anweisung deklarierte Sperre bis zum Ende der IF-Anweisung gehalten wird, dh für eine Aktualisierungsanweisung? Dann ist es möglicherweise ratsam, den obigen Code mithilfe von Start-End-Block-Markierungen anzuzeigen, um zu verhindern, dass Neulinge Ihren Code kopieren und einfügen und ihn trotzdem falsch verstehen.
Simon B.
Gibt es ein Problem, wenn meine PK ein Varchar (allerdings NICHT max) oder eine Kombination aus drei VARCHAR-Spalten ist?
Steam
Ich habe eine Frage zu dieser Antwort unter - stackoverflow.com/questions/21945850/… gestellt. Die Frage ist, ob dieser Code zum Einfügen von Millionen von Zeilen verwendet werden kann.
Steam
Diese Lösung würde zu viel Sperraufwand verursachen, wenn viele Threads häufig bereits vorhandene Zeilen testen. Ich denke, dies kann mit einer Art doppelt überprüftem Sperren durch vorbeugende zusätzliche existsÜberprüfung ohne Sperrhinweise umgangen werden.
Vadzim
38

Ich schreibe meine Lösung. Meine Methode steht nicht für 'if' oder 'merge'. Meine Methode ist einfach.

INSERT INTO TableName (col1,col2)
SELECT @par1, @par2
   WHERE NOT EXISTS (SELECT col1,col2 FROM TableName
                     WHERE col1=@par1 AND col2=@par2)

Beispielsweise:

INSERT INTO Members (username)
SELECT 'Cem'
   WHERE NOT EXISTS (SELECT username FROM Members
                     WHERE username='Cem')

Erläuterung:

(1) SELECT col1, col2 FROM TableName WHERE col1 = @ par1 AND col2 = @ par2 Es wählt aus den gesuchten Werten von TableName aus

(2) SELECT @ par1, @ par2 WO NICHT EXISTIERT Es dauert, wenn es nicht aus (1) Unterabfrage existiert

(3) Fügt in TableName (2) Schrittwerte ein

Cem
quelle
1
Es dient nur zum Einfügen, nicht zum Aktualisieren.
Cem
Es ist tatsächlich immer noch möglich, dass diese Methode fehlschlägt, da die Existenzprüfung vor dem Einfügen durchgeführt wird - siehe stackoverflow.com/a/3790757/1744834
Roman Pekar
3

Mit dem folgenden Modell konnte ich endlich eine Zeile einfügen, sofern sie noch nicht vorhanden war:

INSERT INTO table ( column1, column2, column3 )
(
    SELECT $column1, $column2, $column3
      WHERE NOT EXISTS (
        SELECT 1
          FROM table 
          WHERE column1 = $column1
          AND column2 = $column2
          AND column3 = $column3 
    )
)

was ich gefunden habe bei:

http://www.postgresql.org/message-id/[email protected]

Paul G.
quelle
1
Dies ist eine Antwort nur zum Kopieren und Einfügen von Links ... besser geeignet als Kommentar.
Ian
2

Das musste ich erst kürzlich tun:

set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[cjso_UpdateCustomerLogin]
    (
      @CustomerID AS INT,
      @UserName AS VARCHAR(25),
      @Password AS BINARY(16)
    )
AS 
    BEGIN
        IF ISNULL((SELECT CustomerID FROM tblOnline_CustomerAccount WHERE CustomerID = @CustomerID), 0) = 0
        BEGIN
            INSERT INTO [tblOnline_CustomerAccount] (
                [CustomerID],
                [UserName],
                [Password],
                [LastLogin]
            ) VALUES ( 
                /* CustomerID - int */ @CustomerID,
                /* UserName - varchar(25) */ @UserName,
                /* Password - binary(16) */ @Password,
                /* LastLogin - datetime */ NULL ) 
        END
        ELSE
        BEGIN
            UPDATE  [tblOnline_CustomerAccount]
            SET     UserName = @UserName,
                    Password = @Password
            WHERE   CustomerID = @CustomerID    
        END

    END
TheTXI
quelle
1

Sie können die Zusammenführungsfunktion verwenden , um dies zu erreichen. Andernfalls können Sie Folgendes tun:

declare @rowCount int

select @rowCount=@@RowCount

if @rowCount=0
begin
--insert....
JoshBerke
quelle
0

Die vollständige Lösung finden Sie weiter unten (einschließlich Cursorstruktur). Vielen Dank an Cassius Porcus für den begin trans ... commitCode aus dem obigen Beitrag.

declare @mystat6 bigint
declare @mystat6p varchar(50)
declare @mystat6b bigint

DECLARE mycur1 CURSOR for

 select result1,picture,bittot from  all_Tempnogos2results11

 OPEN mycur1

 FETCH NEXT FROM mycur1 INTO @mystat6, @mystat6p , @mystat6b

 WHILE @@Fetch_Status = 0
 BEGIN

 begin tran /* default read committed isolation level is fine */

 if not exists (select * from all_Tempnogos2results11_uniq with (updlock, rowlock, holdlock)
                     where all_Tempnogos2results11_uniq.result1 = @mystat6 
                        and all_Tempnogos2results11_uniq.bittot = @mystat6b )
     insert all_Tempnogos2results11_uniq values (@mystat6 , @mystat6p , @mystat6b)

 --else
 --  /* update */

 commit /* locks are released here */

 FETCH NEXT FROM mycur1 INTO @mystat6 , @mystat6p , @mystat6b

 END

 CLOSE mycur1

 DEALLOCATE mycur1
 go
user2836818
quelle
0
INSERT INTO [DatabaseName1].dbo.[TableName1] SELECT * FROM [DatabaseName2].dbo.[TableName2]
 WHERE [YourPK] not in (select [YourPK] from [DatabaseName1].dbo.[TableName1])
Almamun
quelle
-2
INSERT INTO table ( column1, column2, column3 )
SELECT $column1, $column2, $column3
EXCEPT SELECT column1, column2, column3
FROM table
Aaron
quelle
INSERT INTO Tabelle (Spalte1, Spalte2, Spalte3) SELECT $ Spalte1, $ Spalte2, $ Spalte3 AUSSER SELECT Spalte1, Spalte2, Spalte3 aus Tabelle
Aaron
1
Es gibt viele hochgelobte Antworten auf diese Frage. Könnten Sie näher erläutern, was diese Antwort zu den vorhandenen Antworten hinzufügt?
Francis
-2

Der beste Ansatz für dieses Problem besteht darin, zuerst die Datenbankspalte EINZIGARTIG zu machen

ALTER TABLE table_name ADD UNIQUE KEY

THEN INSERT IGNORE INTO table_name wird der Wert nicht eingefügt, wenn er zu einem doppelten Schlüssel führt / bereits in der Tabelle vorhanden ist.

Maurice Elagu
quelle