web-dev-qa-db-fra.com

Insérer une ligne uniquement si elle n'est pas déjà là

J'avais toujours utilisé quelque chose de similaire à ce qui suit pour y parvenir:

INSERT INTO TheTable
SELECT
    @primaryKey,
    @value1,
    @value2
WHERE
    NOT EXISTS
    (SELECT
        NULL
    FROM
        TheTable
    WHERE
        PrimaryKey = @primaryKey)

... mais une fois sous charge, une violation de clé primaire s'est produite. Il s'agit de la seule instruction qui s'insère dans ce tableau. Cela signifie-t-il donc que la déclaration ci-dessus n'est pas atomique?

Le problème est qu'il est presque impossible de recréer à volonté.

Je pourrais peut-être le changer en quelque chose comme ceci:

INSERT INTO TheTable
WITH
    (HOLDLOCK,
    UPDLOCK,
    ROWLOCK)
SELECT
    @primaryKey,
    @value1,
    @value2
WHERE
    NOT EXISTS
    (SELECT
        NULL
    FROM
        TheTable
    WITH
        (HOLDLOCK,
        UPDLOCK,
        ROWLOCK)
    WHERE
        PrimaryKey = @primaryKey)

Bien que j'utilise peut-être les mauvais verrous ou que j'utilise trop de verrouillage ou quelque chose.

J'ai vu d'autres questions sur stackoverflow.com où les réponses suggèrent un "IF (SELECT COUNT (*) ... INSERT" etc.), mais j'étais toujours sous l'hypothèse (peut-être incorrecte) qu'une seule instruction SQL serait atomique.

Quelqu'un a-t-il une idée?

66
Adam

Qu'en est-il du modèle "JFDI" ?

BEGIN TRY
   INSERT etc
END TRY
BEGIN CATCH
    IF ERROR_NUMBER() <> 2627
      RAISERROR etc
END CATCH

Sérieusement, c'est le plus rapide et le plus simultané sans verrous, en particulier à des volumes élevés. Que faire si l'UPDLOCK est escaladé et que toute la table est verrouillée?

Lire la leçon 4 :

Leçon 4: Lors du développement du processus upsert avant de régler les index, je me suis d'abord assuré que la ligne If Exists(Select…) se déclencherait pour n'importe quel élément et interdirait les doublons. Nada. En peu de temps, il y a eu des milliers de doublons car le même élément atteindrait l'upert à la même milliseconde et les deux transactions verraient un inexistant et effectueraient l'insertion. Après de nombreux tests, la solution consistait à utiliser l'index unique, à détecter l'erreur et à réessayer, permettant à la transaction de voir la ligne et d'effectuer une mise à jour à la place d'une insertion.

57
gbn

J'ai ajouté HOLDLOCK qui n'était pas présent à l'origine. Veuillez ignorer la version sans cette indication.

En ce qui me concerne, cela devrait suffire:

INSERT INTO TheTable 
SELECT 
    @primaryKey, 
    @value1, 
    @value2 
WHERE 
    NOT EXISTS 
    (SELECT 0
     FROM TheTable WITH (UPDLOCK, HOLDLOCK)
     WHERE PrimaryKey = @primaryKey) 

De plus, si vous voulez réellement mettre à jour une ligne si elle existe et l'insérer si elle ne l'est pas, vous pourriez trouver cette question utile.

23
GSerg

Vous pouvez utiliser MERGE:

MERGE INTO Target
USING (VALUES (@primaryKey, @value1, @value2)) Source (key, value1, value2)
ON Target.key = Source.key
WHEN MATCHED THEN
    UPDATE SET value1 = Source.value1, value2 = Source.value2
WHEN NOT MATCHED BY TARGET THEN
    INSERT (Name, ReasonType) VALUES (@primaryKey, @value1, @value2)
17
Chris Smith

Je ne sais pas si c'est la voie "officielle", mais vous pouvez essayer le INSERT, et retomber sur UPDATE s'il échoue.

1
Marcelo Cantos

Tout d'abord, un grand merci à notre homme @gbn pour ses contributions à la communauté. Je ne peux même pas commencer à expliquer à quelle fréquence je me retrouve à suivre ses conseils.

Quoi qu'il en soit, assez de fanboy.

Pour ajouter un peu à sa réponse, peut-être "l'améliorer". Pour ceux qui, comme moi, se sont sentis instables de ce qu'il fallait faire dans le <> 2627 scénario (et pas de CATCH vide n'est pas une option). J'ai trouvé cette petite pépite de technet .

    BEGIN TRY
       INSERT etc
    END TRY
    BEGIN CATCH
        IF ERROR_NUMBER() <> 2627
          BEGIN
                DECLARE @ErrorMessage NVARCHAR(4000);
                DECLARE @ErrorSeverity INT;
                DECLARE @ErrorState INT;

                SELECT @ErrorMessage = ERROR_MESSAGE(),
                @ErrorSeverity = ERROR_SEVERITY(),
                @ErrorState = ERROR_STATE();

                    RAISERROR (
                        @ErrorMessage,
                        @ErrorSeverity,
                        @ErrorState
                    );
          END
    END CATCH
1
pim