Gestion des accès simultanés à une table de clés sans blocage dans SQL Server


32

J'ai une table qui est utilisée par une application existante pour remplacer les IDENTITYchamps de diverses autres tables.

Chaque ligne de la table stocke le dernier ID utilisé LastIDpour le champ nommé dans IDName.

Parfois, le proc stocké se trouve dans une impasse - je crois avoir construit un gestionnaire d'erreur approprié; Cependant, je suis intéressé de voir si cette méthodologie fonctionne comme je le pense, ou si je me trompe d'arbre ici.

Je suis à peu près certain qu'il devrait y avoir un moyen d'accéder à cette table sans aucune impasse.

La base de données elle-même est configurée avec READ_COMMITTED_SNAPSHOT = 1.

Tout d'abord, voici le tableau:

CREATE TABLE [dbo].[tblIDs](
    [IDListID] [int] NOT NULL 
        CONSTRAINT PK_tblIDs 
        PRIMARY KEY CLUSTERED 
        IDENTITY(1,1) ,
    [IDName] [nvarchar](255) NULL,
    [LastID] [int] NULL,
);

Et l'index non clusterisé sur le IDNamechamp:

CREATE NONCLUSTERED INDEX [IX_tblIDs_IDName] 
ON [dbo].[tblIDs]
(
    [IDName] ASC
) 
WITH (
    PAD_INDEX = OFF
    , STATISTICS_NORECOMPUTE = OFF
    , SORT_IN_TEMPDB = OFF
    , DROP_EXISTING = OFF
    , ONLINE = OFF
    , ALLOW_ROW_LOCKS = ON
    , ALLOW_PAGE_LOCKS = ON
    , FILLFACTOR = 80
);

GO

Quelques exemples de données:

INSERT INTO tblIDs (IDName, LastID) 
    VALUES ('SomeTestID', 1);
INSERT INTO tblIDs (IDName, LastID) 
    VALUES ('SomeOtherTestID', 1);
GO

La procédure stockée utilisée pour mettre à jour les valeurs stockées dans la table et renvoyer l'ID suivant:

CREATE PROCEDURE [dbo].[GetNextID](
    @IDName nvarchar(255)
)
AS
BEGIN
    /*
        Description:    Increments and returns the LastID value from tblIDs
        for a given IDName
        Author:         Max Vernon
        Date:           2012-07-19
    */

    DECLARE @Retry int;
    DECLARE @EN int, @ES int, @ET int;
    SET @Retry = 5;
    DECLARE @NewID int;
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    SET NOCOUNT ON;
    WHILE @Retry > 0
    BEGIN
        BEGIN TRY
            BEGIN TRANSACTION;
            SET @NewID = COALESCE((SELECT LastID 
                FROM tblIDs 
                WHERE IDName = @IDName),0)+1;
            IF (SELECT COUNT(IDName) 
                FROM tblIDs 
                WHERE IDName = @IDName) = 0 
                    INSERT INTO tblIDs (IDName, LastID) 
                    VALUES (@IDName, @NewID)
            ELSE
                UPDATE tblIDs 
                SET LastID = @NewID 
                WHERE IDName = @IDName;
            COMMIT TRANSACTION;
            SET @Retry = -2; /* no need to retry since the operation completed */
        END TRY
        BEGIN CATCH
            IF (ERROR_NUMBER() = 1205) /* DEADLOCK */
                SET @Retry = @Retry - 1;
            ELSE
                BEGIN
                SET @Retry = -1;
                SET @EN = ERROR_NUMBER();
                SET @ES = ERROR_SEVERITY();
                SET @ET = ERROR_STATE()
                RAISERROR (@EN,@ES,@ET);
                END
            ROLLBACK TRANSACTION;
        END CATCH
    END
    IF @Retry = 0 /* must have deadlock'd 5 times. */
    BEGIN
        SET @EN = 1205;
        SET @ES = 13;
        SET @ET = 1
        RAISERROR (@EN,@ES,@ET);
    END
    ELSE
        SELECT @NewID AS NewID;
END
GO

Exemples d'exécution du proc stocké:

EXEC GetNextID 'SomeTestID';

NewID
2

EXEC GetNextID 'SomeTestID';

NewID
3

EXEC GetNextID 'SomeOtherTestID';

NewID
2

MODIFIER:

J'ai ajouté un nouvel index car l'index existant IX_tblIDs_Name n'est pas utilisé par le SP; Je suppose que le processeur de requêtes utilise l'index clusterisé car il a besoin de la valeur stockée dans LastID. Quoi qu'il en soit, cet index est utilisé par le plan d'exécution réel:

CREATE NONCLUSTERED INDEX IX_tblIDs_IDName_LastID 
ON dbo.tblIDs
(
    IDName ASC
) 
INCLUDE
(
    LastID
)
WITH (FILLFACTOR = 100
    , ONLINE=ON
    , ALLOW_ROW_LOCKS = ON
    , ALLOW_PAGE_LOCKS = ON);

EDIT # 2:

J'ai suivi le conseil donné par @AaronBertrand et l'ai légèrement modifié. L’idée générale ici est d’affiner la déclaration pour éliminer les verrouillages inutiles et, dans l’ensemble, pour rendre le SP plus efficace.

Le code ci-dessous remplace le code ci-dessus de BEGIN TRANSACTIONà END TRANSACTION:

BEGIN TRANSACTION;
SET @NewID = COALESCE((SELECT LastID 
        FROM dbo.tblIDs 
        WHERE IDName = @IDName), 0) + 1;

IF @NewID = 1
    INSERT INTO tblIDs (IDName, LastID) 
    VALUES (@IDName, @NewID);
ELSE
    UPDATE dbo.tblIDs 
    SET LastID = @NewID 
    WHERE IDName = @IDName;

COMMIT TRANSACTION;

Étant donné que notre code n'ajoute jamais d'enregistrement à cette table avec 0 dans, LastIDnous pouvons supposer que si @NewID est égal à 1, l'intention est d'ajouter un nouvel ID à la liste, sinon nous mettons à jour une ligne existante de la liste.


La manière dont vous avez configuré la base de données pour prendre en charge RCSI n'est pas pertinente. Vous montez intentionnellement SERIALIZABLEici.
Aaron Bertrand

oui, je voulais juste ajouter toutes les informations pertinentes. Je suis content que vous confirmiez que ce n'est pas pertinent!
Max Vernon

Il est très facile de faire de sp_getapplock une victime de l’impasse, mais pas si vous commencez la transaction, appelez une fois sp_getapplock pour obtenir un verrou exclusif et poursuivez votre modification.
AK

1
Est-ce que IDName est unique? Puis, recommandez "créer un index non cluster unique ". Toutefois, si vous avez besoin de valeurs NULL, alors l'index devra également être filtré .
crokusek

Réponses:


15

Premièrement, je voudrais éviter de faire un aller-retour à la base de données pour chaque valeur. Par exemple, si votre application sait qu'elle a besoin de 20 nouveaux identifiants, ne faites pas 20 allers-retours. N'effectuez qu'un seul appel de procédure stockée et incrémentez le compteur de 20. De plus, il peut être préférable de scinder votre table en plusieurs.

Il est possible d'éviter complètement les impasses. Je n'ai aucune impasse dans mon système. Il y a plusieurs façons d'accomplir cela. Je vais vous montrer comment utiliser sp_getapplock pour éliminer les blocages. Je ne sais pas si cela fonctionnera pour vous, car SQL Server est une source fermée. Je ne peux donc pas voir le code source. Par conséquent, je ne sais pas si j'ai testé tous les cas possibles.

Ce qui suit décrit ce qui fonctionne pour moi. YMMV.

Tout d’abord, commençons par un scénario dans lequel nous obtenons toujours un nombre considérable de blocages. Deuxièmement, nous utiliserons sp_getapplock pour les éliminer. Le point le plus important ici est de tester votre solution. Votre solution peut être différente, mais vous devez l'exposer à une concurrence élevée, comme je le montrerai plus tard.

Conditions préalables

Laissez-nous mettre en place une table avec des données de test:

CREATE TABLE dbo.Numbers(n INT NOT NULL PRIMARY KEY); 
GO 

INSERT INTO dbo.Numbers 
    ( n ) 
        VALUES  ( 1 ); 
GO 
DECLARE @i INT; 
    SET @i=0; 
WHILE @i<21  
    BEGIN 
    INSERT INTO dbo.Numbers 
        ( n ) 
        SELECT n + POWER(2, @i) 
        FROM dbo.Numbers; 
    SET @i = @i + 1; 
    END;  
GO

SELECT n AS ID, n AS Key1, n AS Key2, 0 AS Counter1, 0 AS Counter2
INTO dbo.DeadlockTest FROM dbo.Numbers
GO

ALTER TABLE dbo.DeadlockTest ADD CONSTRAINT PK_DeadlockTest PRIMARY KEY(ID);
GO

CREATE INDEX DeadlockTestKey1 ON dbo.DeadlockTest(Key1);
GO

CREATE INDEX DeadlockTestKey2 ON dbo.DeadlockTest(Key2);
GO

Les deux procédures suivantes risquent fort de s’emboîter dans une impasse:

CREATE PROCEDURE dbo.UpdateCounter1 @Key1 INT
AS
SET NOCOUNT ON ;
SET XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ;
UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
SET @Key1=@Key1-10000;
UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
COMMIT;
GO

CREATE PROCEDURE dbo.UpdateCounter2 @Key2 INT
AS
SET NOCOUNT ON ;
SET XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ;
SET @Key2=@Key2-10000;
UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
SET @Key2=@Key2+10000;
UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
COMMIT;
GO

Reproduction des impasses

Les boucles suivantes doivent reproduire plus de 20 blocages chaque fois que vous les exécutez. Si vous obtenez moins de 20, augmentez le nombre d'itérations.

Dans un onglet, lancez ceci;

DECLARE @i INT, @DeadlockCount INT;
SELECT @i=0, @DeadlockCount=0;

WHILE @i<5000 BEGIN ;
  BEGIN TRY 
    EXEC dbo.UpdateCounter1 @Key1=123456;
  END TRY
  BEGIN CATCH
    SET @DeadlockCount = @DeadlockCount + 1;
    ROLLBACK;
  END CATCH ;
  SET @i = @i + 1;
END;
SELECT 'Deadlocks caught: ', @DeadlockCount ;

Dans un autre onglet, exécutez ce script.

DECLARE @i INT, @DeadlockCount INT;
SELECT @i=0, @DeadlockCount=0;

WHILE @i<5000 BEGIN ;
  BEGIN TRY 
    EXEC dbo.UpdateCounter2 @Key2=123456;
  END TRY
  BEGIN CATCH
    SET @DeadlockCount = @DeadlockCount + 1;
    ROLLBACK;
  END CATCH ;
  SET @i = @i + 1;
END;
SELECT 'Deadlocks caught: ', @DeadlockCount ;

Assurez-vous de commencer les deux en quelques secondes.

Utilisation de sp_getapplock pour éliminer les blocages

Modifiez les deux procédures, réexécutez la boucle et vérifiez que vous n'avez plus de blocages:

ALTER PROCEDURE dbo.UpdateCounter1 @Key1 INT
AS
SET NOCOUNT ON ;
SET XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ;
EXEC sp_getapplock @Resource='DeadlockTest', @LockMode='Exclusive';
UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
SET @Key1=@Key1-10000;
UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
COMMIT;
GO

ALTER PROCEDURE dbo.UpdateCounter2 @Key2 INT
AS
SET NOCOUNT ON ;
SET XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ;
EXEC sp_getapplock @Resource='DeadlockTest', @LockMode='Exclusive';
SET @Key2=@Key2-10000;
UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
SET @Key2=@Key2+10000;
UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
COMMIT;
GO

Utiliser une table avec une ligne pour éliminer les blocages

Au lieu d'appeler sp_getapplock, nous pouvons modifier le tableau suivant:

CREATE TABLE dbo.DeadlockTestMutex(
ID INT NOT NULL,
CONSTRAINT PK_DeadlockTestMutex PRIMARY KEY(ID),
Toggle INT NOT NULL);
GO

INSERT INTO dbo.DeadlockTestMutex(ID, Toggle)
VALUES(1,0);

Une fois que cette table est créée et remplie, nous pouvons remplacer la ligne suivante

EXEC sp_getapplock @Resource='DeadlockTest', @LockMode='Exclusive';

avec celui-ci, dans les deux procédures:

UPDATE dbo.DeadlockTestMutex SET Toggle = 1 - Toggle WHERE ID = 1;

Vous pouvez relancer le test de résistance et constater par vous-même que nous n’avons aucune impasse.

Conclusion

Comme nous l'avons vu, sp_getapplock peut être utilisé pour sérialiser l'accès à d'autres ressources. En tant que tel, il peut être utilisé pour éliminer les blocages.

Bien entendu, cela peut considérablement ralentir les modifications. Pour résoudre ce problème, nous devons choisir la granularité appropriée pour le verrou exclusif et, autant que possible, utiliser des ensembles au lieu de lignes individuelles.

Avant d’utiliser cette approche, vous devez faire un test de stress vous-même. Tout d’abord, vous devez vous assurer d’obtenir au moins une vingtaine de blocages avec votre approche originale. Deuxièmement, vous ne devriez avoir aucune impasse lorsque vous réexécutez le même script de repro en utilisant une procédure stockée modifiée.

En général, je ne pense pas qu'il y ait un bon moyen de déterminer si votre T-SQL est à l'abri des blocages simplement en le regardant ou en regardant le plan d'exécution. Messagerie Internet uniquement, le seul moyen de déterminer si votre code est sujet à des blocages est de l'exposer à une simultanéité élevée.

Bonne chance pour éliminer les impasses! Notre système ne connaît aucune impasse, ce qui est excellent pour notre équilibre travail-vie personnelle.


2
+1 comme sp_getapplock est un outil utile et mal connu. Étant donné un désordre épineux qui peut prendre du temps à se dégrader, c’est un truc pratique pour sérialiser un processus qui est dans l’impasse. Mais, devrait-il être le premier choix pour un cas comme celui-ci qui est facilement compris et peut (peut-être devrait) être traité par des mécanismes de verrouillage standard?
Mark Storey-Smith

2
@ MarkStorey-Smith C'est mon premier choix car je n'ai effectué qu'une seule recherche sur le stress et que je peux le réutiliser dans n'importe quelle situation. La sérialisation a déjà eu lieu. Par conséquent, tout ce qui se produit après sp_getapplock n'a aucune incidence sur le résultat. Avec les mécanismes de verrouillage standard, je ne peux jamais en être aussi sûr: ajouter un index ou simplement obtenir un autre plan d'exécution peut entraîner des blocages à un niveau inégalé auparavant. Demande-moi comment je sais.
AK

J'imagine qu'il me manque quelque chose d'évident, mais comment UPDATE dbo.DeadlockTestMutex SET Toggle = 1 - Toggle WHERE ID = 1;prévenir les blocages?
Dale K

9

L’utilisation de l’ XLOCKindice sur votre SELECTapproche ou sur les éléments suivants UPDATEdevrait être à l’abri de ce type de blocage:

DECLARE @Output TABLE ([NewId] INT);
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

BEGIN TRANSACTION;

UPDATE
    dbo.tblIDs WITH (XLOCK)
SET 
    LastID = LastID + 1
OUTPUT
    INSERTED.[LastId] INTO @Output
WHERE
    IDName = @IDName;

IF(@@ROWCOUNT = 1)
BEGIN
    SELECT @NewId = [NewId] FROM @Output;
END
ELSE
BEGIN
    SET @NewId = 1;

    INSERT dbo.tblIDs
        (IDName, LastID)
    VALUES
        (@IDName, @NewId);
END

SELECT [NewId] = @NewId ;

COMMIT TRANSACTION;

Je reviendrai avec quelques autres variantes (si ce n’est pas battu!).


Cela XLOCKempêchera un compteur existant d’être mis à jour à partir de plusieurs connexions, n’avez-vous pas besoin TABLOCKXd’empêcher plusieurs connexions d’ajouter le même nouveau compteur?
Dale K

1
@DaleBurrell Non, vous auriez une PK ou une contrainte unique sur IDName.
Mark Storey-Smith le

7

Mike Defehr m'a montré un moyen élégant d'accomplir cela de manière très légère:

ALTER PROCEDURE [dbo].[GetNextID](
    @IDName nvarchar(255)
)
AS
BEGIN
    /*
        Description:    Increments and returns the LastID value from tblIDs for a given IDName
        Author:         Max Vernon / Mike Defehr
        Date:           2012-07-19

    */

    DECLARE @Retry int;
    DECLARE @EN int, @ES int, @ET int;
    SET @Retry = 5;
    DECLARE @NewID int;
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    SET NOCOUNT ON;
    WHILE @Retry > 0
    BEGIN
        BEGIN TRY
            UPDATE dbo.tblIDs 
            SET @NewID = LastID = LastID + 1 
            WHERE IDName = @IDName;

            IF @NewID IS NULL
            BEGIN
                SET @NewID = 1;
                INSERT INTO tblIDs (IDName, LastID) VALUES (@IDName, @NewID);
            END
            SET @Retry = -2; /* no need to retry since the operation completed */
        END TRY
        BEGIN CATCH
            IF (ERROR_NUMBER() = 1205) /* DEADLOCK */
                SET @Retry = @Retry - 1;
            ELSE
                BEGIN
                SET @Retry = -1;
                SET @EN = ERROR_NUMBER();
                SET @ES = ERROR_SEVERITY();
                SET @ET = ERROR_STATE()
                RAISERROR (@EN,@ES,@ET);
                END
        END CATCH
    END
    IF @Retry = 0 /* must have deadlock'd 5 times. */
    BEGIN
        SET @EN = 1205;
        SET @ES = 13;
        SET @ET = 1
        RAISERROR (@EN,@ES,@ET);
    END
    ELSE
        SELECT @NewID AS NewID;
END
GO

(Pour être complet, voici la table associée au proc stocké)

CREATE TABLE [dbo].[tblIDs]
(
    IDName nvarchar(255) NOT NULL,
    LastID int NULL,
    CONSTRAINT [PK_tblIDs] PRIMARY KEY CLUSTERED 
    (
        [IDName] ASC
    ) WITH 
    (
        PAD_INDEX = OFF
        , STATISTICS_NORECOMPUTE = OFF
        , IGNORE_DUP_KEY = OFF
        , ALLOW_ROW_LOCKS = ON
        , ALLOW_PAGE_LOCKS = ON
        , FILLFACTOR = 100
    ) 
);
GO

Voici le plan d'exécution de la dernière version:

entrez la description de l'image ici

Et voici le plan d’exécution de la version originale (susceptible de blocage):

entrez la description de l'image ici

Clairement, la nouvelle version gagne!

À des fins de comparaison, la version intermédiaire avec (XLOCK)etc, produit le plan suivant:

entrez la description de l'image ici

Je dirais que c'est une victoire! Merci pour l'aide de tous!


2
Devrait en effet fonctionner, mais vous utilisez SERIALIZABLE où ce n’est pas applicable. Les lignes fantômes ne peuvent pas exister ici, alors pourquoi utiliser un niveau d'isolation existant pour les empêcher? De même, si une personne appelle votre procédure depuis une autre personne ou depuis une connexion au cours de laquelle une transaction externe a été lancée, toutes les actions qu'elle initie se poursuivront à SERIALIZABLE. Cela peut devenir compliqué.
Mark Storey-Smith

2
SERIALIZABLEn'existe pas pour empêcher les fantômes. Il existe une sémantique d’isolation sérialisable , c’est-à-dire le même effet persistant sur la base de données que si les transactions impliquées avaient été exécutées en série dans un ordre non spécifié.
Paul White dit: GoFundMonica

6

Ne pas voler le tonnerre de Mark Storey-Smith, mais il est sur quelque chose avec son poste ci-dessus (qui a d'ailleurs reçu le plus de votes positifs). Le conseil que j'ai donné à Max était centré autour de la construction "UPDATE set @variable = column = column + column" que je trouve vraiment cool, mais je pense qu'elle est peut-être non documentée (elle doit être prise en charge, même si elle existe spécifiquement pour le protocole TCP repères).

Voici une variante de la réponse de Mark - puisque vous renvoyez la nouvelle valeur d'ID sous forme de jeu d'enregistrements, vous pouvez supprimer complètement la variable scalaire, aucune transaction explicite ne devrait être nécessaire non plus, et je conviendrais qu'il est inutile de modifier le niveau d'isolation ainsi que. Le résultat est très propre et assez lisse ...

ALTER PROC [dbo].[GetNextID]
  @IDName nvarchar(255)
  AS
BEGIN
SET NOCOUNT ON;

DECLARE @Output TABLE ([NewID] INT);

UPDATE dbo.tblIDs SET LastID = LastID + 1
OUTPUT inserted.[LastId] INTO @Output
WHERE IDName = @IDName;

IF(@@ROWCOUNT = 1)
    SELECT [NewID] FROM @Output;
ELSE
    INSERT dbo.tblIDs (IDName, LastID)
    OUTPUT INSERTED.LastID AS [NewID]
    VALUES (@IDName,1);
END

3
D'accord, cela devrait être à l'abri d'une impasse, mais il est sujet à une situation de concurrence critique sur l'insert, si vous omettez la transaction.
Mark Storey-Smith

4

J'ai corrigé un blocage similaire dans un système l'année dernière en modifiant ceci:

IF (SELECT COUNT(IDName) FROM tblIDs WHERE IDName = @IDName) = 0 
  INSERT INTO tblIDs (IDName, LastID) VALUES (@IDName, @NewID)
ELSE
  UPDATE tblIDs SET LastID = @NewID WHERE IDName = @IDName;

Pour ça:

UPDATE tblIDs SET LastID = @NewID WHERE IDName = @IDName;
IF @@ROWCOUNT = 0
BEGIN
  INSERT ...
END

En général, choisir un COUNTjuste pour déterminer la présence ou l'absence est très inutile. Dans ce cas, puisque c’est 0 ou 1, ce n’est pas comme si c’était beaucoup de travail, mais (a) cette habitude peut se répercuter sur d’autres cas où elle coûtera beaucoup plus cher (dans ce cas, utilisez IF NOT EXISTSplutôt que IF COUNT() = 0), et (b) l'analyse supplémentaire est complètement inutile. Le UPDATEeffectue essentiellement le même contrôle.

En outre, cela ressemble à une odeur sérieuse de code pour moi:

SET @NewID = COALESCE((SELECT LastID FROM tblIDs WHERE IDName = @IDName),0)+1;

Quel est le point ici? Pourquoi ne pas simplement utiliser une colonne d'identité ou dériver cette séquence en utilisant ROW_NUMBER()au moment de la requête?


La plupart des tables que nous avons utilisent un IDENTITY. Cette table prend en charge certains codes hérités écrits dans MS Access qui seraient assez impliqués pour la mise à niveau. La SET @NewID=ligne incrémente simplement la valeur stockée dans la table pour l'ID donné (mais vous le savez déjà). Pouvez-vous développer sur comment je pourrais utiliser ROW_NUMBER()?
Max Vernon

@ MaxVernon non sans savoir ce que LastIDsignifie réellement votre modèle. Quel est son objectif? Le nom n'est pas tout à fait explicite. Comment Access l'utilise-t-il?
Aaron Bertrand

Une fonction dans Access veut ajouter une ligne à une table donnée sans IDENTITY. First Access appelle GetNextID('WhatevertheIDFieldIsCalled')pour obtenir le prochain ID à utiliser, puis l'insère dans la nouvelle ligne avec les données nécessaires.
Max Vernon

Je vais mettre en œuvre votre changement. Un pur cas de "moins, c'est plus"!
Max Vernon

1
Votre impasse fixée peut ressurgir. Votre deuxième modèle est également vulnérable: sqlblog.com/blogs/alexander_kuznetsov/archive/2010/01/12/… Pour éliminer les blocages, j'utiliserais sp_getapplock. Il se peut que le système de chargement mixte avec des centaines d'utilisateurs ne soit pas bloqué.
AK
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.