Vérifiez s'il existe une ligne, sinon insérez

237

J'ai besoin d'écrire une procédure stockée T-SQL qui met à jour une ligne dans une table. Si la ligne n'existe pas, insérez-la. Toutes ces étapes enveloppées par une transaction.

Il s'agit d'un système de réservation, il doit donc être atomique et fiable . Il doit retourner vrai si la transaction a été commise et le vol réservé.

Je suis nouveau sur T-SQL et je ne sais pas comment l'utiliser @@rowcount. C'est ce que j'ai écrit jusqu'à présent. Suis-je sur la bonne route? Je suis sûr que c'est un problème facile pour vous.

-- 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
la source

Réponses:

158

Jetez un œil à la commande MERGE . Vous pouvez le faire UPDATE, INSERT& DELETEen une seule déclaration.

Voici une implémentation de travail sur l'utilisation MERGE
- Il vérifie si le vol est plein avant de faire une mise à jour, sinon fait un insert.

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

Puis ...

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
la source
6
Voir aussi pourquoi vous aimeriez AVEC (HOLDLOCK) pour cette MERGE.
Eugene Ryabtsev
4
Je pense que MERGE est pris en charge après 2005 (donc 2008+).
samis
3
MERGE sans WITH (UPDLOCK) peut avoir des violations de clé primaire, ce qui serait mauvais dans ce cas. Voir [MERGE est-il une instruction atomique dans SQL2008?] ( Stackoverflow.com/questions/9871644/… )
James
156

J'assume une seule ligne pour chaque vol? Si c'est le cas:

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

Je suppose que ce que j'ai dit, car votre façon de faire peut surréserver un vol, car elle insérera une nouvelle ligne lorsqu'il y aura 10 billets maximum et que vous en réserverez 20.

Gregory A Beamer
la source
Oui. Il y a 1 ligne par vol. Mais votre code fait le SELECT mais ne vérifie pas si le vol est complet avant de METTRE À JOUR. Comment faire ça?
2
En raison des conditions de concurrence, ce n'est correct que si le niveau d'isolement des transactions actuel est sérialisable.
Jarek Przygódzki
1
@Martin: La réponse était concentrée sur la question à portée de main. Extrait de la propre déclaration du PO "Toutes ces étapes enveloppées par une transaction". Si la transaction est implémentée correctement, le problème de thread sécurisé ne devrait pas être un problème.
Gregory A Beamer
14
@GregoryABeamer - Le coller simplement dans un BEGIN TRAN ... COMMITniveau d'isolement sous par défaut ne résoudra pas le problème. L'OP a précisé que atomique et fiable étaient des exigences. Votre réponse ne parvient pas à résoudre ce problème sous quelque forme que ce soit.
Martin Smith
2
Serait - ce thread-safe si (UPDLOCK, HOLDLOCK) a été ajouté à la commande SELECT: IF EXISTS (SELECT * FROM Bookings (UPDLOCK, HOLDLOCK) WHERE FLightID = @Id)?
Jim
67

Passez les astuces updlock, rowlock, holdlock lorsque vous testez l'existence de la ligne.

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 */

Le conseil de mise à jour force la requête à prendre un verrou de mise à jour sur la ligne si elle existe déjà, empêchant d'autres transactions de la modifier jusqu'à ce que vous validiez ou annuliez.

L'astuce de verrouillage oblige la requête à prendre un verrou de plage, empêchant d'autres transactions d'ajouter une ligne correspondant à vos critères de filtre jusqu'à ce que vous validiez ou annuliez.

L'astuce rowlock force la granularité du verrouillage au niveau de la ligne au lieu du niveau de page par défaut, de sorte que votre transaction ne bloquera pas les autres transactions essayant de mettre à jour des lignes non liées dans la même page (mais soyez conscient du compromis entre la réduction des conflits et l'augmentation de la frais généraux de verrouillage - vous devez éviter de prendre un grand nombre de verrous au niveau des lignes en une seule transaction).

Voir http://msdn.microsoft.com/en-us/library/ms187373.aspx pour plus d'informations.

Notez que les verrous sont pris lorsque les instructions qui les prennent sont exécutées - l'invocation de begin tran ne vous donne pas l'immunité contre une autre transaction qui pince les verrous sur quelque chose avant d'y arriver. Vous devez essayer de factoriser votre SQL pour maintenir les verrous le plus rapidement possible en validant la transaction dès que possible (acquisition tardive, libération anticipée).

Notez que les verrous au niveau de la ligne peuvent être moins efficaces si votre PK est un gros point, car le hachage interne sur SQL Server est dégénéré pour les valeurs 64 bits (différentes valeurs de clé peuvent hacher vers le même ID de verrouillage).

Cassius Porcus
la source
4
Le verrouillage est TRÈS important pour éviter la surréservation. Est-il correct de supposer qu'un verrou déclaré dans l'instruction IF est conservé jusqu'à la fin de l'instruction IF, c'est-à-dire pour une instruction de mise à jour? Ensuite, il peut être judicieux d'afficher le code ci-dessus en utilisant les marqueurs de début de bloc pour empêcher les débutants de copier et de coller votre code et de se tromper.
Simon B.
Y a-t-il un problème si mon PK est un varchar (pas max cependant) ou une combinaison de trois colonnes VARCHAR?
Steam
J'ai posé une question relative à cette réponse sur - stackoverflow.com/questions/21945850/… La question est de savoir si ce code peut être utilisé pour insérer des millions de lignes.
Steam
Cette solution imposerait trop de surcharge de verrouillage dans les cas où de nombreux threads testent souvent des lignes déjà existantes. Je suppose que cela peut être résolu avec une sorte de verrouillage à double vérification via une existsvérification supplémentaire préventive sans conseils de verrouillage.
Vadzim
38

j'écris ma solution. ma méthode ne tient pas "si" ou "fusionner". ma méthode est simple.

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

Par exemple:

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

Explication:

(1) SELECT col1, col2 FROM TableName WHERE col1 = @ par1 AND col2 = @ par2 Il sélectionne parmi les valeurs recherchées TableName

(2) SELECT @ par1, @ par2 WHERE NOT EXISTS Il faut s'il n'existe pas de (1) sous-requête

(3) Insère dans les valeurs d'étape TableName (2)

Cem
la source
1
c'est uniquement pour l'insertion, pas pour la mise à jour.
Cem
Il est en fait toujours possible que cette méthode échoue car la vérification de l'existence est effectuée avant l'insertion - voir stackoverflow.com/a/3790757/1744834
Roman Pekar
3

J'ai finalement pu insérer une ligne, à condition qu'elle n'existait pas déjà, en utilisant le modèle suivant:

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 
    )
)

que j'ai trouvé sur:

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

Paul G
la source
1
Ceci est un lien copier-coller seulement répondre ... mieux adapté comme commentaire.
Ian
2

C'est quelque chose que j'ai récemment dû faire:

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
la source
1

Vous pouvez utiliser la fonctionnalité de fusion pour y parvenir. Sinon, vous pouvez faire:

declare @rowCount int

select @rowCount=@@RowCount

if @rowCount=0
begin
--insert....
JoshBerke
la source
0

La solution complète est ci-dessous (y compris la structure du curseur). Un grand merci à Cassius Porcus pour le begin trans ... commitcode affiché ci-dessus.

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
la source
0
INSERT INTO [DatabaseName1].dbo.[TableName1] SELECT * FROM [DatabaseName2].dbo.[TableName2]
 WHERE [YourPK] not in (select [YourPK] from [DatabaseName1].dbo.[TableName1])
Almamun
la source
-2
INSERT INTO table ( column1, column2, column3 )
SELECT $column1, $column2, $column3
EXCEPT SELECT column1, column2, column3
FROM table
Aaron
la source
INSERT INTO table (colonne1, colonne2, colonne3) SELECT $ colonne1, $ colonne2, $ colonne3 SAUF SELECT SELECT colonne1, colonne2, colonne3 du tableau
Aaron
1
Il existe de nombreuses réponses très positives à cette question. Pourriez-vous nous expliquer ce que cette réponse ajoute aux réponses existantes?
francis
-2

La meilleure approche de ce problème consiste à rendre la colonne de base de données UNIQUE

ALTER TABLE table_name ADD UNIQUE KEY

THEN INSERT IGNORE INTO table_name , la valeur ne sera pas insérée si elle se traduit par une clé en double / existe déjà dans la table.

Maurice Elagu
la source