2009-04-27 5 views
5

Ho una chiave primaria che non desidero incrementare automaticamente (per vari motivi) e quindi sto cercando un modo per incrementare semplicemente quel campo quando INSERISCO. Con parole semplici intendo senza stored procedure e senza trigger, quindi solo una serie di comandi SQL (preferibilmente un comando).È possibile implementare un incremento manuale semplicemente con SQL INSERT semplice?

Ecco quello che ho provato finora:

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 

Tuttavia, quando eseguito, gli errori di comando, dicendo che

"subquery non sono ammessi in questo contesto solo espressioni scalari. sono consentiti "

Quindi, come posso fare questo/cosa sto facendo di sbagliato?


EDIT: Da quando è stato rilevato come considerazione, il tavolo da inserire è garantita per avere già almeno 1 fila.

risposta

9

Hai capito che avrai collisioni giuste?

è necessario fare qualcosa di simile, e questo potrebbe causare situazioni di stallo in modo da essere molto sicuro di quello che si sta cercando di realizzare qui

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 

Per spiegare la cosa collisione, mi hanno fornito un certo codice

primo creare questa tabella e inserire una riga

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

Ora aprire due finestre di query ed eseguire questo allo stesso tempo

012.351.641,061 mila
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 

Vedrete un sacco di questi

Server: Messaggio 2627, livello 14, stato 1, riga 7 violazione di vincolo PRIMARY KEY 'PK__Table1__3213E83F2962141D'. Impossibile inserire la chiave duplicata nell'oggetto 'dbo.Tabella1'. La dichiarazione è stata chiusa.

+1

Perché ci dovrebbero essere collisioni? Tali cose sono gestite dalla transazione, sì? Inoltre, da dove verrebbero i deadlock? – cdeszaq

+2

non se si esegue sotto il livello transazionale predefinito (letto commesso), cosa impedisce a due thread di leggere lo stesso valore massimo? – SQLMenace

+0

I deadlock possono essere evitati omettendo il suggerimento 'Holdlock' –

2

Prova a modificare:

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

io non consiglierei di farlo in questo modo per una serie di ragioni anche se (le prestazioni, la sicurezza delle transazioni, ecc)

+0

Cosa raccomanderesti invece di fare? – GernBlandston

+0

Dato che il campo ID è indicizzato, il successo della prestazione è significativo? Inoltre, se avvolto in una transazione, dovrebbe essere atomico e sicuro, corretto? – cdeszaq

+0

Non sono sicuro che la tabella verrà bloccata quando si esegue la parte SELECT MAX (id) ... In caso contrario, c'è il potenziale che due thread possano ottenere lo stesso id. Qualcuno che conosce un po 'meglio il server SQL sarebbe in grado di dirti se è effettivamente sicuro o meno - la mia intuizione è che non lo è, ma non sono sicuro. –

0

Se si sta facendo in un grilletto, si potrebbe fare in modo che sia un "invece di" grilletto e farlo in un paio di dichiarazioni:

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

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

L'unica cosa che avrebbe dovuto fare attenzione circa è concorrenza - se due righe sono INSER allo stesso tempo, potrebbero tentare di utilizzare lo stesso valore per @next, causando un conflitto.

Compie ciò che desideri?

+0

No, i trigger non possono essere utilizzati a meno che non ci sia un altro modo. – cdeszaq

0

Questo dovrebbe funzionare:

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

O questo (LIMIT sostituto per altre piattaforme):

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

Qualche commento sul perché -1 da qualcuno? – gbn

+1

Il down-drive drive-by probabilmente voleva dire che questa query produrrebbe id duplicati in un ambiente ad alta concorrenza. –

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

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

commit; 

Ma forse un approccio migliore sarebbe utilizzando una funzione getNextId scalare ('tabla1 ')

+0

getNextId (

) sarebbe una stored procedure personalizzata o una inclusa? Se inclusa, su quali piattaforme è attiva, poiché non penso che sia standard. – cdeszaq

+1

Quindi la tua funzione utilizzerà SQL dinamico? O una tabella chiave separata? – gbn

1

Potrebbe essere perché non ci sono record in modo che la query secondaria restituisca NULL ... prova

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

In questo caso, è garantito che ci sia almeno una riga esistente, ma buona cosa da ricordare. +1 – cdeszaq

+0

non puoi avere una query dentro VALORI – SQLMenace

+0

@Jon: poster lo ha già provato – gbn

0

Sembra molto strano fare questo genere di cose senza una colonna IDENTITY (auto-incremento), facendomi mettere in discussione l'architettura stessa. Voglio dire, seriamente, questa è la situazione perfetta per una colonna di IDENTITÀ. Potrebbe aiutarci a rispondere alla tua domanda se spieghi il ragionamento alla base di questa decisione. =)

Detto questo, alcune opzioni sono:

  • usando un trigger INSTEAD OF per questo scopo. Quindi, faresti il ​​tuo INSERT (l'istruzione INSERT non avrebbe bisogno di passare un ID). Il codice di attivazione gestiva l'inserimento dell'ID appropriato. Dovresti utilizzare la sintassi WITH (UPDLOCK, HOLDLOCK) utilizzata da un altro risponditore per mantenere il blocco per la durata del trigger (che è implicitamente incluso in una transazione) & per elevare il tipo di blocco da "condiviso" a "aggiornamento" "lock (IIRC).
  • è possibile utilizzare l'idea di cui sopra, ma avere una tabella il cui scopo è quello di memorizzare l'ultimo valore massimo inserito nella tabella. Quindi, una volta impostato il tavolo, non sarà più necessario eseguire un SELECT MAX (ID) ogni volta. Dovresti semplicemente incrementare il valore nella tabella. Questo è sicuro a condizione che si usi il blocco appropriato (come discusso). Ancora una volta, evita ripetute scansioni della tabella ogni volta che INSERIRE.
  • utilizza GUID anziché ID. È molto più semplice unire tabelle tra database, dal momento che i GUID saranno sempre univoci (mentre i record tra i database avranno ID interi in conflitto). Per evitare la divisione della pagina, è possibile utilizzare i GUID sequenziali. Questo è utile solo se potresti aver bisogno di unire i database.
  • Utilizzare un processo memorizzato al posto dell'approccio trigger (poiché i trigger devono essere evitati, per qualche motivo). Avresti ancora il problema di blocco (e i problemi di prestazioni che possono sorgere). Ma gli sproc sono preferiti rispetto all'SQL dinamico (nel contesto delle applicazioni) e sono spesso molto più performanti.

Siamo spiacenti per le escursioni. Spero possa aiutare.

1

Non so se qualcuno è ancora alla ricerca di una risposta, ma qui è una soluzione che sembra funzionare:

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

Abbiamo una situazione simile dove abbiamo bisogno di incrementare e non potrebbe avere lacune i numeri. (Se si utilizza un valore di identità e si esegue il rollback di una transazione, quel numero non verrà inserito e si avranno spazi vuoti perché il valore di identità non viene ripristinato.)

abbiamo creato una tabella separata per l'ultimo numero utilizzato e seminati con 0.

nostro inserto fa qualche passo.

--increment il numero aggiornamento dbo.NumberTable set number = number + 1

--find che cosa il numero incrementato è select @number = numero da dbo.NumberTable

- -utilizzare dell'inserto numero in dbo.MyTable usando l'@number

commit o rollback

In questo modo le transazioni simultanee vengono elaborate in una singola riga, poiché ogni transazione simultanea sarà attesa perché il NumberTable è bloccato. Non appena la transazione in attesa ottiene il blocco, incrementa il valore corrente e lo blocca dagli altri. Quel valore corrente è l'ultimo numero utilizzato e se una transazione viene sottoposta a rollback, anche l'aggiornamento NumberTable viene eseguito il rollback in modo da non lasciare spazi vuoti.

Spero che questo aiuti.

Un altro modo per causare l'esecuzione di file singoli consiste nell'utilizzare un blocco dell'applicazione SQL. Abbiamo usato questo approccio per processi più lunghi come la sincronizzazione dei dati tra sistemi in modo che possa essere eseguito un solo processo di sincronizzazione alla volta.

+1

Si ottiene un buon punto sui rollback. Ma cosa impedisce a due thread di ottenere lo stesso numero? Stai utilizzando una transazione serializzabile? – Leigh

0

Eventuali critiche a questo? Per me va bene.

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