Il martedì di T-SQL di questo mese è ospitato da Mike Fal (blog | twitter) e l'argomento è Trick Shots, in cui siamo invitati a parlare alla comunità di alcune soluzioni che abbiamo usato in SQL Server che sembravano, almeno per noi, come una sorta di "tiro a segno" - qualcosa di simile all'uso di colpi di massa, "inglese" o complicati in banca nel biliardo o nello snooker. Dopo aver lavorato con SQL Server per circa 15 anni, ho avuto l'occasione di escogitare trucchi per risolvere alcuni problemi piuttosto interessanti, ma uno che sembra essere abbastanza riutilizzabile, si adatta facilmente a molte situazioni ed è semplice da implementare, è qualcosa che chiamo "schema switch-a-roo".
Supponiamo che tu abbia uno scenario in cui hai una tabella di ricerca di grandi dimensioni che deve essere aggiornata periodicamente. Questa tabella di ricerca è necessaria su molti server e può contenere dati che vengono popolati da un'origine esterna o di terze parti, ad es. IP o dati di dominio, oppure possono rappresentare dati dall'interno del tuo ambiente.
I primi due scenari in cui avevo bisogno di una soluzione per questo erano rendere disponibili metadati e dati denormalizzati per "cache di dati" di sola lettura:in realtà solo istanze SQL Server MSDE (e successive Express) installate su vari server Web, quindi i server Web sono stati estratti questi dati memorizzati nella cache localmente invece di disturbare il sistema OLTP primario. Questo può sembrare ridondante, ma scaricare l'attività di lettura dal sistema OLTP primario ed essere in grado di eliminare completamente la connessione di rete dall'equazione, ha portato a un vero aumento delle prestazioni a 360 gradi e, in particolare, per gli utenti finali .
Questi server non avevano bisogno di copie aggiornate dei dati; infatti, molte tabelle della cache venivano aggiornate solo quotidianamente. Ma poiché i sistemi erano 24×7 e alcuni di questi aggiornamenti potevano richiedere diversi minuti, spesso intralciavano i clienti reali che facevano cose reali sul sistema.
L'approccio(i) originale(i)
All'inizio, il codice era piuttosto semplicistico:abbiamo cancellato le righe che erano state rimosse dal sorgente, aggiornato tutte le righe che potevamo dire fossero cambiate e inserito tutte le nuove righe. Sembrava qualcosa del genere (gestione degli errori ecc. rimossi per brevità):
BEGIN TRANSACTION; DELETE dbo.Lookup WHERE [key] NOT IN (SELECT [key] FROM [source]); UPDATE d SET [col] = s.[col] FROM dbo.Lookup AS d INNER JOIN [source] AS s ON d.[key] = s.[key] -- AND [condition to detect change]; INSERT dbo.Lookup([cols]) SELECT [cols] FROM [source] WHERE [key] NOT IN (SELECT [key] FROM dbo.Lookup); COMMIT TRANSACTION;
Inutile dire che questa transazione potrebbe causare alcuni problemi di prestazioni reali quando il sistema era in uso. Sicuramente c'erano altri modi per farlo, ma ogni metodo che abbiamo provato era ugualmente lento e costoso. Quanto è lento e costoso? "Fammi contare le scansioni..."
Poiché questo MERGE precedente e avevamo già scartato approcci "esterni" come DTS, attraverso alcuni test abbiamo stabilito che sarebbe stato più efficiente semplicemente cancellare la tabella e ripopolarla, piuttosto che provare a sincronizzare con l'origine :
BEGIN TRANSACTION; TRUNCATE TABLE dbo.Lookup; INSERT dbo.Lookup([cols]) SELECT [cols] FROM [source]; COMMIT TRANSACTION;
Ora, come ho spiegato, questa query da [source] potrebbe richiedere un paio di minuti, soprattutto se tutti i server Web venissero aggiornati in parallelo (abbiamo provato a scaglionare dove potevamo). E se un cliente era sul sito e tentava di eseguire una query che coinvolgeva la tabella di ricerca, doveva attendere il completamento della transazione. Nella maggior parte dei casi, se eseguono questa query a mezzanotte, non avrebbe molta importanza se ricevessero la copia di ieri dei dati di ricerca o quella di oggi; quindi, farli aspettare l'aggiornamento sembrava sciocco e in realtà ha portato a una serie di chiamate di supporto.
Quindi, sebbene questo fosse migliore, era certamente tutt'altro che perfetto.
La mia soluzione iniziale:sp_rename
La mia soluzione iniziale, quando SQL Server 2000 era interessante, era creare una tabella "ombra":
CREATE TABLE dbo.Lookup_Shadow([cols]);
In questo modo ho potuto popolare la tabella shadow senza interrompere affatto gli utenti, quindi eseguire una ridenominazione in tre modi, un'operazione rapida di soli metadati, solo dopo che il popolamento è stato completato. Qualcosa del genere (di nuovo, grossolanamente semplificato):
TRUNCATE TABLE dbo.Lookup_Shadow; INSERT dbo.Lookup_Shadow([cols]) SELECT [cols] FROM [source]; BEGIN TRANSACTION; EXEC sp_rename N'dbo.Lookup', N'dbo.Lookup_Fake'; EXEC sp_rename N'dbo.Lookup_Shadow', N'dbo.Lookup'; COMMIT TRANSACTION; -- if successful: EXEC sp_rename N'dbo.Lookup_Fake', N'dbo.Lookup_Shadow';
Lo svantaggio di questo approccio iniziale era che sp_rename ha un messaggio di output non sopprimibile che avvisa sui pericoli della ridenominazione degli oggetti. Nel nostro caso abbiamo eseguito questa attività tramite i processi di SQL Server Agent e abbiamo gestito molti metadati e altre tabelle della cache, quindi la cronologia dei processi è stata inondata da tutti questi messaggi inutili e ha effettivamente causato il troncamento di errori reali dai dettagli della cronologia. (Mi sono lamentato di questo nel 2007, ma alla fine il mio suggerimento è stato respinto e chiuso come "Non si risolverà.")
Una soluzione migliore:schemi
Dopo l'aggiornamento a SQL Server 2005, ho scoperto questo fantastico comando chiamato CREATE SCHEMA. Era banale implementare lo stesso tipo di soluzione usando schemi invece di rinominare le tabelle, e ora la cronologia dell'agente non sarebbe stata inquinata da tutti questi messaggi inutili. Fondamentalmente ho creato due nuovi schemi:
CREATE SCHEMA fake AUTHORIZATION dbo; CREATE SCHEMA shadow AUTHORIZATION dbo;
Quindi ho spostato la tabella Lookup_Shadow nello schema della cache e l'ho rinominata:
ALTER SCHEMA shadow TRANSFER dbo.Lookup_Shadow; EXEC sp_rename N'shadow.Lookup_Shadow', N'Lookup';
(Se stai solo implementando questa soluzione, creerai una nuova copia della tabella nello schema, non sposterai lì la tabella esistente e la rinominerai.)
Con questi due schemi in atto e una copia della tabella di ricerca nello schema shadow, la mia ridenominazione a tre vie è diventata un trasferimento di schema a tre vie:
TRUNCATE TABLE shadow.Lookup; INSERT shadow.Lookup([cols]) SELECT [cols] FROM [source]; -- perhaps an explicit statistics update here BEGIN TRANSACTION; ALTER SCHEMA fake TRANSFER dbo.Lookup; ALTER SCHEMA dbo TRANSFER shadow.Lookup; COMMIT TRANSACTION; ALTER SCHEMA shadow TRANSFER fake.Lookup;
A questo punto puoi ovviamente svuotare la copia shadow della tabella, tuttavia in alcuni casi ho trovato utile lasciare la "vecchia" copia dei dati in giro per la risoluzione dei problemi:
TRUNCATE TABLE shadow.Lookup;
Qualsiasi altra cosa tu faccia con la copia shadow, ti consigliamo di assicurarti di farlo al di fuori della transazione:le due operazioni di trasferimento dovrebbero essere il più concise e rapide possibile.
Alcuni avvertimenti
- Chiavi straniere
Questo non funzionerà immediatamente se la tabella di ricerca è referenziata da chiavi esterne. Nel nostro caso non abbiamo indicato alcun vincolo a queste tabelle della cache, ma in tal caso, potrebbe essere necessario attenersi a metodi intrusivi come MERGE. Oppure usa i metodi di sola aggiunta e disabilita o elimina le chiavi esterne prima di eseguire qualsiasi modifica ai dati (quindi ricreale o riabilitale in seguito). Se ti attieni alle tecniche MERGE / UPSERT e lo stai facendo tra server o, peggio ancora, da un sistema remoto, ti consiglio vivamente di ottenere i dati grezzi localmente piuttosto che provare a utilizzare questi metodi tra server.
- Statistiche
Cambiare le tabelle (usando la ridenominazione o il trasferimento dello schema) porterà a statistiche che si spostano avanti e indietro tra le due copie della tabella e questo può ovviamente essere un problema per i piani. Potresti quindi considerare l'aggiunta di aggiornamenti statistici espliciti come parte di questo processo.
- Altri approcci
Ci sono ovviamente altri modi per farlo che semplicemente non ho avuto l'occasione di provare. Il cambio di partizione e l'utilizzo di una vista + sinonimo sono due approcci che potrei studiare in futuro per un trattamento più approfondito dell'argomento. Sarei interessato a conoscere le tue esperienze e come hai risolto questo problema nel tuo ambiente. E sì, mi rendo conto che questo problema è in gran parte risolto da gruppi di disponibilità e secondari leggibili in SQL Server 2012, ma lo considero un "trucco" se riesci a risolvere il problema senza lanciare licenze di fascia alta al problema o replicare un intero database per rendere ridondanti alcune tabelle. :-)
Conclusione
Se riesci a convivere con i limiti qui, questo approccio potrebbe essere un esecutore migliore di uno scenario in cui essenzialmente metti un tavolo offline usando SSIS o la tua routine MERGE / UPSERT, ma assicurati di testare entrambe le tecniche. Il punto più significativo è che l'utente finale che accede al tavolo dovrebbe avere la stessa identica esperienza, in qualsiasi momento della giornata, anche se ha raggiunto il tavolo nel mezzo del tuo aggiornamento periodico.