Modifica della chiave primaria da IDENTITY a persistente Colonna calcolata utilizzando COALESCE


10

Nel tentativo di disaccoppiare un'applicazione dal nostro database monolitico, abbiamo provato a modificare le colonne INT IDENTITY di varie tabelle in una colonna calcolata PERSISTED che utilizza COALESCE. Fondamentalmente, abbiamo bisogno dell'applicazione disaccoppiata di poter ancora aggiornare il database per i dati comuni condivisi tra molte applicazioni pur consentendo alle applicazioni esistenti di creare dati in queste tabelle senza la necessità di modificare il codice o la procedura.

Quindi, essenzialmente, siamo passati dalla definizione di colonna di;

PkId INT IDENTITY(1,1) PRIMARY KEY

per;

PkId AS AS COALESCE(old_id, external_id, new_id) PERSISTED NOT NULL,
old_id INT NULL, -- Values here are from existing records of PkId before table change
external_id INT NULL,
new_id INT IDENTITY(2000000,1) NOT NULL

In tutti i casi, PkId è anche una CHIAVE PRIMARIA e in tutti i casi tranne uno, è CLUSTER. Tutte le tabelle hanno le stesse chiavi e indici esterni di prima. In sostanza, il nuovo formato consente a PkId di essere fornito dall'applicazione disaccoppiata (come external_id), ma consente anche a PkId di essere il valore della colonna IDENTITY consentendo quindi il codice esistente che si basa sulla colonna IDENTITY attraverso l'uso di SCOPE_IDENTITY e @@ IDENTITY funzionare come una volta.

Il problema che abbiamo avuto è che ci siamo imbattuti in un paio di query che venivano eseguite in un tempo accettabile per ora esplodere completamente. I piani di query generati utilizzati da queste query non assomigliano a quelli di una volta.

Dato che la nuova colonna è un PRIMARY KEY, lo stesso tipo di dati di prima e PERSISTED, mi sarei aspettato che le query e i piani di query si comportassero come prima. Il PkId INT PERSISTENTE COMPUTATO dovrebbe comportarsi essenzialmente allo stesso modo di una definizione INT esplicita in termini di come SQL Server produrrà il piano di esecuzione? Ci sono altri probabili problemi con questo approccio che puoi vedere?

Lo scopo di questa modifica doveva consentirci di cambiare la definizione della tabella senza la necessità di modificare le procedure e il codice esistenti. Alla luce di questi problemi, non mi sento di poter seguire questo approccio.


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Paul White 9

Risposte:


4

PRIMO

Probabilmente non c'è bisogno tutte le tre colonne: old_id, external_id, new_id. La new_idcolonna, essendo un IDENTITY, avrà un nuovo valore generato per ogni riga, anche quando si inserisce in external_id. Ma tra old_ide external_id, quelli si escludono a vicenda: o c'è già un old_idvalore o quella colonna, nella concezione attuale, sarà solo NULLse si usa external_ido new_id. Dal momento che non aggiungerai un nuovo ID "esterno" a una riga già esistente (ovvero uno che ha un old_idvalore) e non ci saranno nuovi valori in arrivo old_id, quindi può esserci una colonna che viene utilizzata per entrambi gli scopi.

Quindi, sbarazzati della external_idcolonna e rinomina old_idper essere qualcosa del genere old_or_external_ido qualunque cosa. Ciò non dovrebbe richiedere cambiamenti reali a nulla, ma riduce alcune delle complicazioni. Al massimo potresti dover chiamare la colonna external_id, anche se contiene valori "vecchi", se il codice dell'app è già scritto per essere inserito external_id.

Ciò riduce la nuova struttura in modo che sia:

PkId AS AS COALESCE(old_or_external_id, new_id, -1) PERSISTED NOT NULL,
old_or_external_id INT NULL, -- values from existing record OR passed in from app
new_id INT IDENTITY(2000000, 1) NOT NULL

Ora hai aggiunto solo 8 byte per riga anziché 12 byte (supponendo che tu non stia utilizzando l' SPARSEopzione Compressione dati). E non è stato necessario modificare alcun codice, T-SQL o codice app.

SECONDO

Continuando lungo questo percorso di semplificazione, diamo un'occhiata a ciò che ci è rimasto:

  • La old_or_external_idcolonna ha già dei valori o verrà assegnato un nuovo valore dall'app o verrà lasciato come NULL.
  • Il new_idavranno sempre un nuovo valore generato, ma tale valore verrà utilizzato solo se la old_or_external_idcolonna è NULL.

Non c'è mai un momento in cui avresti bisogno di valori in entrambi old_or_external_ide new_id. Sì, ci saranno momenti in cui entrambe le colonne hanno valori dovuti a new_idessere un IDENTITY, ma tali new_idvalori vengono ignorati. Ancora una volta, questi due campi si escludono a vicenda. Così quello che ora?

Ora possiamo capire perché avevamo bisogno external_iddel primo. Considerando che è possibile inserire in una IDENTITYcolonna utilizzando SET IDENTITY_INSERT {table_name} ON;, è possibile evitare di apportare modifiche allo schema e modificare solo il codice dell'app per avvolgere le INSERTdichiarazioni / operazioni SET IDENTITY_INSERT {table_name} ON;e le SET IDENTITY_INSERT {table_name} OFF;istruzioni. È quindi necessario determinare su quale intervallo iniziale reimpostare la IDENTITYcolonna (per i valori appena generati) in quanto dovrà essere ben al di sopra dei valori che il codice dell'app inserirà poiché l'inserimento di un valore più elevato comporterà il successivo valore generato automaticamente essere maggiore dell'attuale valore MAX. Ma puoi sempre inserire un valore inferiore al valore IDENT_CURRENT .

La combinazione delle colonne old_or_external_ide new_idnon aumenta inoltre le possibilità di imbattersi in una situazione di valori sovrapposti tra valori generati automaticamente e valori generati dall'app poiché l'intenzione di avere le colonne 2 o anche 3 è combinarle in un valore Chiave primaria, e quelli sono sempre valori unici.

In questo approccio, devi solo:

  • Lasciare le tabelle come:

    PkId INT IDENTITY(1,1) PRIMARY KEY

    Ciò aggiunge 0 byte a ogni riga, anziché 8 o anche 12.

  • Determina l'intervallo iniziale per i valori generati dall'app. Questi saranno maggiori del valore MAX corrente in ciascuna tabella, ma inferiore a quello che diventerà il valore minimo per i valori generati automaticamente.
  • Determinare da quale valore deve iniziare l'intervallo generato automaticamente. Dovrebbe esserci molto spazio tra l'attuale valore MAX e molto spazio per crescere, sapendo che il limite superiore è di poco superiore a 2,14 miliardi. È quindi possibile impostare questo nuovo valore seed minimo tramite DBCC CHECKIDENT .
  • Inserisci il codice dell'app INSERISCI SET IDENTITY_INSERT {table_name} ON;e le SET IDENTITY_INSERT {table_name} OFF;istruzioni.

SECONDO, Parte B

Una variante del metodo indicato direttamente sopra sarebbe avere il codice App inserto valori iniziano -1 e andando giù di lì. Questo lascia i IDENTITYvalori come gli unici che salgono . Il vantaggio qui è che non solo non complicate lo schema, ma non dovete nemmeno preoccuparvi di imbattervi in ​​ID sovrapposti (se i valori generati dall'app vengono eseguiti nel nuovo intervallo generato automaticamente). Questa è un'opzione solo se non stai già utilizzando valori ID negativi (e sembra piuttosto raro che le persone utilizzino valori negativi su colonne generate automaticamente, quindi questa dovrebbe essere una probabile posibilità nella maggior parte delle situazioni).

In questo approccio, devi solo:

  • Lasciare le tabelle come:

    PkId INT IDENTITY(1,1) PRIMARY KEY

    Ciò aggiunge 0 byte a ogni riga, anziché 8 o anche 12.

  • L'intervallo iniziale per i valori generati dall'app sarà -1.
  • Inserisci il codice dell'app INSERISCI SET IDENTITY_INSERT {table_name} ON;e le SET IDENTITY_INSERT {table_name} OFF;istruzioni.

Qui devi ancora fare il IDENTITY_INSERT, ma: non aggiungi nuove colonne, non devi "ridimensionare" nessuna IDENTITYcolonna e non hai rischi futuri di sovrapposizioni.

SECONDO, Parte 3

Un'ultima variante di questo approccio potrebbe essere quella di scambiare le IDENTITYcolonne e utilizzare invece le sequenze . Il motivo per adottare questo approccio è di poter inserire nel codice dell'app valori che sono: positivi, al di sopra dell'intervallo generato automaticamente (non al di sotto) e non è necessario SET IDENTITY_INSERT ON / OFF.

In questo approccio, devi solo:

  • Crea sequenze usando CREATE SEQUENCE
  • Copia la IDENTITYcolonna in una nuova colonna che non ha la IDENTITYproprietà, ma ha un DEFAULTvincolo che utilizza la funzione PROSSIMO VALORE PER :

    PkId INT PRIMARY KEY CONSTRAINT [DF_TableName_NextID] DEFAULT (NEXT VALUE FOR...)

    Ciò aggiunge 0 byte a ogni riga, anziché 8 o anche 12.

  • L'intervallo iniziale per i valori generati dall'app sarà ben al di sopra di quello che pensi si avvicinino ai valori generati automaticamente.
  • Inserisci il codice dell'app INSERISCI SET IDENTITY_INSERT {table_name} ON;e le SET IDENTITY_INSERT {table_name} OFF;istruzioni.

TUTTAVIA , a causa della necessità che il codice con una SCOPE_IDENTITY()o @@IDENTITYfunzioni ancora correttamente, il passaggio a Sequenze non è attualmente un'opzione in quanto sembra che non ci siano equivalenti di tali funzioni per Sequenze :-(. Triste!


Grazie mille per la tua risposta. Sollevi alcuni punti discussi qui internamente. Sfortunatamente, alcuni di questi non funzioneranno per noi per un paio di motivi. Il nostro database è piuttosto vecchio e un po 'fragile e funziona in modalità di compatibilità 2005, quindi SEQUENCES è fuori. Il push dei dati delle nostre app avviene tramite uno strumento di caricamento dei dati che ottiene nuovi record dalle code del broker di servizi e li invia tramite più thread. IDENTITY_INSERT può essere utilizzato solo per una tabella per sessione e il pensiero attuale è che la nostra architettura non può provvedere a ciò senza cambiamenti significativi. Sto testando il tuo suggerimento per il pugno ora.
Mr Moose,

@MrMoose Sì, ho aggiornato la mia risposta per includere ulteriori informazioni sulle sequenze alla fine. Non funzionerebbe comunque nella tua situazione. E mi chiedevo quali fossero i potenziali problemi di concorrenza IDENTITY_INSERT, ma non l'ho provato. Non sono sicuro che l'opzione n. 1 risolverà il problema generale, è stata solo un'osservazione per ridurre la complessità inutile. Tuttavia, se hai più thread che inseriscono nuovi ID "esterni", come puoi garantire che siano univoci?
Solomon Rutzky,

@MrMoose In realtà, per quanto riguarda " IDENTITY_INSERT può essere utilizzato solo per una tabella per sessione ", qual è esattamente il problema qui? 1) puoi inserire solo in una tabella alla volta, quindi lo spegni per TableA prima di inserirlo in TableB, e 2) Ho appena testato e contrariamente a quanto avevo pensato, non ci sono problemi di concorrenza - Sono stato in grado di avere IDENTITY_INSERT ONper la stessa tabella in due sessioni e si stava inserendo in entrambi senza problemi.
Solomon Rutzky,

1
Come hai suggerito, il cambiamento 1 ha fatto poca differenza. L'ID che useremo verrà allocato al di fuori del database corrente e utilizzato per mettere in relazione i record. È possibile che la mia comprensione delle sessioni non sia corretta, quindi IDENTITY_INSERT potrebbe funzionare. Ci vorrà un po 'di tempo per indagare su questo, quindi non sarò in grado di riferire per un po'. Grazie ancora per l'input. È molto apprezzato
Mr Moose,

1
Penso che il tuo suggerimento di utilizzare IDENTITY_INSERT (con un valore seed elevato per le app esistenti) funzionerà bene. Aaron Bertrand ha fornito una risposta qui con un buon piccolo esempio sul test con concorrenza. Abbiamo modificato il nostro strumento di caricamento dei dati per essere in grado di gestire le tabelle che devono specificare i valori di identità e avremo ulteriori test nelle prossime settimane.
Mr Moose,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.