2009-04-27 5 views
5

J'ai une clé primaire que je ne veux pas incrémenter automatiquement (pour diverses raisons) et donc je cherche un moyen d'incrémenter simplement ce champ quand je l'INSÉRER. Par simplement, je veux dire sans procédures stockées et sans déclencheurs, donc juste une série de commandes SQL (de préférence une commande).Possible d'implémenter un incrément manuel avec simplement SQL INSERT?

Voici ce que j'ai essayé jusqu'ici:

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 

Cependant, lorsqu'il est exécuté, les erreurs de commande, en disant que

« Les sous-requêtes ne sont pas autorisés dans ce contexte seulement des expressions scalaires. sont autorisés "

Alors, comment puis-je faire ceci/qu'est-ce que je fais mal?


EDIT: Comme il a été souligné comme une considération, la table à insérer dans est la garantie d'avoir au moins 1 rang déjà.

Répondre

9

Vous comprenez que vous aurez des collisions non?

vous devez faire quelque chose comme ceci et cela pourrait causer des blocages fatals soyez donc très sûr de ce que vous essayez d'accomplir ici

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 

Pour expliquer la chose de collision, j'ai fourni un code

premier créer cette table et insérez une ligne

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

maintenant, ouvrez deux fenêtres de requête et exécuter ce en même temps

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 

Vous verrez un tas de ces

Serveur: Msg 2627, niveau 14, état 1, ligne 7 Violation de contrainte PRIMARY KEY 'PK__Table1__3213E83F2962141D'. Impossible d'insérer une clé en double dans l'objet 'dbo.Table1'. La déclaration a été terminée.

+1

Pourquoi y aurait-il des collisions? De telles choses sont traitées par la transaction, oui? De plus, d'où viendraient les blocages? – cdeszaq

+2

pas si vous exécutez sous le niveau transactionnel par défaut (lire commited), ce qui empêche deux threads de lire la même valeur max? – SQLMenace

+0

Les blocages peuvent être évités en omettant l'indicateur 'Holdlock' –

2

Essayez ceci:

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

Je ne recommanderais pas le faire de cette façon pour un certain nombre de raisons (quoique performances, la sécurité des transactions, etc.)

+0

Que leur recommanderiez-vous plutôt? – GernBlandston

+0

Étant donné que le champ id est indexé, la performance est-elle significative? Aussi, si enveloppé dans une transaction, il devrait être atomique et sûr, correct? – cdeszaq

+0

Je ne suis pas sûr si la table sera verrouillée quand vous faites la partie SELECT MAX (id) ... Sinon, il y a le potentiel que deux threads pourraient obtenir le même identifiant. Quelqu'un qui connaît un peu mieux le serveur SQL serait en mesure de vous dire si c'est vraiment sûr ou pas - mon intuition est que ce n'est pas le cas, mais je ne suis pas vraiment sûr. –

0

Si vous le faire dans un déclencheur, vous pouvez vous assurer qu'il est un « pour » déclencheur et de le faire dans quelques déclarations:

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

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

La seule chose que vous auriez à faire attention à ce que la concurrence - si deux lignes sont Inser En même temps, ils pourraient essayer d'utiliser la même valeur pour @next, provoquant un conflit.

Est-ce que cela accomplit ce que vous voulez?

+0

Non, les triggers ne peuvent pas être utilisés à moins qu'il n'y ait pas d'autre moyen. – cdeszaq

0

Cela devrait fonctionner:

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

Ou ce (substitut LIMIT pour d'autres plates-formes):

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

Un commentaire sur pourquoi -1 de quelqu'un? – gbn

+1

Downvoter Drive-by signifiait probablement que cette requête produirait des identifiants en double dans un environnement de concurrence élevée. –

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

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

commit; 

Mais peut-être une meilleure approche serait d'utiliser une getNextId fonction scalaire ('table1 ')

+0

getNextId (

) serait une procédure stockée a..custom, ou une procédure incluse? Si elle est incluse, sur quelles plateformes est-ce que je ne pense pas que ce soit standard? – cdeszaq

+1

Donc, votre fonction utiliserait SQL dynamique? Ou une table clé séparée? – gbn

1

Il peut être parce qu'il n'y a pas d'enregistrements de sorte que la sous-requête renvoie NULL ... try

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

Dans ce cas, il est garanti qu'il y a au moins une ligne existante, mais bonne chose à retenir. +1 – cdeszaq

+0

vous ne pouvez pas avoir une requête à l'intérieur des valeurs – SQLMenace

+0

@Jon: affiche déjà essayé – gbn

0

Il semble très étrange de faire ce genre de chose sans une colonne IDENTITY (auto-increment), ce qui me fait questionner l'architecture elle-même. Je veux dire, sérieusement, c'est la situation idéale pour une colonne IDENTITY. Cela pourrait nous aider à répondre à votre question si vous expliquez le raisonnement derrière cette décision. =)

Cela étant dit, certaines options sont:

  • en utilisant un déclencheur INSTEAD OF à cet effet. Donc, vous feriez votre INSERT (l'instruction INSERT n'aurait pas besoin de transmettre un ID). Le code de déclenchement gérerait l'insertion de l'identifiant approprié. Vous devez utiliser la syntaxe WITH (UPDLOCK, HOLDLOCK) utilisée par un autre répondeur pour conserver le verrou pendant la durée du déclencheur (qui est implicitement enveloppée dans une transaction) & pour élever le type de verrou de "shared" à "update" "serrure (IIRC).
  • Vous pouvez utiliser l'idée ci-dessus, mais avoir une table dont le but est de stocker la dernière valeur maximale insérée dans la table. Ainsi, une fois la table configurée, vous n'aurez plus à faire un SELECT MAX (ID) à chaque fois. Vous devez simplement incrémenter la valeur dans la table. Ceci est sûr à condition que vous utilisiez un verrouillage approprié (comme indiqué). Encore une fois, cela évite les analyses de table répétées chaque fois que vous insérez.
  • utilisez les GUID au lieu des ID. Il est beaucoup plus facile de fusionner des tables entre des bases de données, puisque les GUID seront toujours uniques (alors que les enregistrements dans les bases de données auront des ID entiers en conflit). Pour éviter le fractionnement des pages, des GUID séquentiels peuvent être utilisés. Cela n'est utile que si vous devez effectuer une fusion de base de données.
  • Utilisez un proc stocké au lieu de l'approche de déclenchement (puisque les déclencheurs doivent être évités, pour une raison quelconque). Vous auriez toujours le problème de verrouillage (et les problèmes de performance qui peuvent survenir). Mais les sprocs sont préférés au SQL dynamique (dans le contexte des applications), et sont souvent beaucoup plus performants.

Désolé pour la randonnée. J'espère que cela pourra aider.

1

Je ne sais pas si quelqu'un cherche toujours une réponse, mais voici une solution qui semble fonctionner:

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

Nous avons une situation similaire où nous avions besoin d'incrémenter et ne pouvait pas avoir des lacunes dans les nombres. (Si vous utilisez une valeur d'identité et qu'une transaction est annulée, ce numéro ne sera pas inséré et vous aurez des trous car la valeur d'identité ne reviendra pas.)

Nous avons créé une table séparée pour le dernier numéro utilisé et ensemencé 0.

Notre insert prend quelques pas.

--increment le numéro Mise à jour dbo.NumberTable nombre set = nombre + 1

--find ce que le nombre est incrémentée select @nombre = nombre de dbo.NumberTable

- -utiliser le nombre insert en utilisant le dbo.MyTable @number

valider ou annuler

Cela provoque des transactions simultanées à traiter dans une seule ligne que chaque transaction simultanée attendra parce que le NumberTable est verrouillé. Dès que la transaction en attente obtient le verrou, elle incrémente la valeur actuelle et la verrouille des autres. Cette valeur actuelle est le dernier nombre utilisé et, si une transaction est annulée, la mise à jour NumberTable est également annulée, de sorte qu'il n'y a aucun espace.

Espérons que ça aide.

Une autre façon de provoquer l'exécution d'un seul fichier consiste à utiliser un verrou d'application SQL. Nous avons utilisé cette approche pour des processus plus longs tels que la synchronisation de données entre systèmes afin qu'un seul processus de synchronisation puisse s'exécuter à la fois.

+1

Vous soulignez un bon point à propos des rollbacks. Mais qu'est-ce qui empêche deux threads d'obtenir le même nombre? Utilisez-vous une transaction sérialisable? – Leigh

0

Des critiques de ceci? Travaille pour moi.

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 
Questions connexes