Denna månads T-SQL-tisdag är värd av Mike Fal (blogg | twitter), och ämnet är Trick Shots, där vi är inbjudna att berätta för communityn om någon lösning vi använde i SQL Server som kändes, åtminstone för oss, som ett slags "trickshot" – något som liknar att använda massé, "engelska" eller komplicerade bankshots i biljard eller snooker. Efter att ha arbetat med SQL Server i cirka 15 år, har jag haft tillfälle att komma på knep för att lösa några ganska intressanta problem, men ett som verkar vara ganska återanvändbart, lätt anpassar sig till många situationer och är enkelt att implementera, är något jag kallar "schema switch-a-roo."
Låt oss säga att du har ett scenario där du har en stor uppslagstabell som behöver uppdateras med jämna mellanrum. Denna uppslagstabell behövs på många servrar och kan innehålla data som fylls i från en extern eller tredje parts källa, t.ex. IP- eller domändata, eller kan representera data från din egen miljö.
De första scenarierna där jag behövde en lösning för detta var att göra metadata och denormaliserade data tillgängliga för skrivskyddade "datacacher" - egentligen bara SQL Server MSDE (och senare Express)-instanser installerade på olika webbservrar, så webbservrarna drog denna cachade data lokalt istället för att störa det primära OLTP-systemet. Detta kan tyckas överflödigt, men att avlasta läsaktivitet bort från det primära OLTP-systemet, och att kunna ta nätverksanslutningen ur ekvationen helt, ledde till en rejäl stöt i all-around-prestanda och, framför allt, för slutanvändare .
Dessa servrar behövde inte uppdaterade kopior av data; Faktum är att många av cachetabellerna bara uppdaterades dagligen. Men eftersom systemen var 24×7, och vissa av dessa uppdateringar kunde ta flera minuter, kom de ofta i vägen för riktiga kunder som gjorde riktiga saker på systemet.
Det ursprungliga tillvägagångssättet
I början var koden ganska förenklad:vi tog bort rader som hade tagits bort från källan, uppdaterade alla rader som vi kunde se hade ändrats och infogade alla nya rader. Det såg ut ungefär så här (felhantering etc. togs bort för korthetens skull):
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;
Onödigt att säga att denna transaktion kan orsaka några verkliga prestandaproblem när systemet användes. Det fanns säkert andra sätt att göra detta på, men varje metod vi provade var lika långsam och dyr. Hur långsamt och dyrt? "Låt mig räkna skanningarna..."
Eftersom denna pre-daterade MERGE, och vi redan hade förkastat "externa" tillvägagångssätt som DTS, bestämde vi genom vissa tester att det skulle vara mer effektivt att bara torka tabellen och fylla i den igen, snarare än att försöka synkronisera till källan :
BEGIN TRANSACTION; TRUNCATE TABLE dbo.Lookup; INSERT dbo.Lookup([cols]) SELECT [cols] FROM [source]; COMMIT TRANSACTION;
Nu, som jag förklarade, kunde den här frågan från [källa] ta ett par minuter, speciellt om alla webbservrar uppdaterades parallellt (vi försökte växla dit vi kunde). Och om en kund var på webbplatsen och försökte köra en fråga som involverade uppslagstabellen, var de tvungna att vänta på att transaktionen skulle slutföras. I de flesta fall, om de kör den här frågan vid midnatt, skulle det egentligen inte spela någon roll om de fick gårdagens kopia av uppslagsdata eller dagens; så att få dem att vänta på uppdateringen verkade fånigt, och det ledde faktiskt till ett antal supportsamtal.
Så även om det här var bättre, var det verkligen långt ifrån perfekt.
Min första lösning:sp_rename
Min första lösning, när SQL Server 2000 var cool, var att skapa en "skuggtabell":
CREATE TABLE dbo.Lookup_Shadow([cols]);
På så sätt kunde jag fylla i skuggtabellen utan att avbryta användare alls, och sedan utföra ett trevägsbyte - en snabb operation med endast metadata - först efter att populationen var klar. Något så här (igen, grovt förenklat):
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';
Nackdelen med detta initiala tillvägagångssätt var att sp_rename har ett icke-undertryckbart utdatameddelande som varnar dig om farorna med att byta namn på objekt. I vårt fall utförde vi den här uppgiften genom SQL Server Agent-jobb, och vi hanterade en hel del metadata och andra cachetabeller, så jobbhistoriken översvämmades med alla dessa värdelösa meddelanden och orsakade faktiskt att riktiga fel trunkerades från historikdetaljerna. (Jag klagade över detta 2007, men mitt förslag avvisades till slut och stängdes som "Kommer inte att fixa.")
En bättre lösning:scheman
När vi uppgraderade till SQL Server 2005 upptäckte jag detta fantastiska kommando som heter CREATE SCHEMA. Det var trivialt att implementera samma typ av lösning med hjälp av scheman istället för att byta namn på tabeller, och nu skulle agenthistoriken inte förorenas med alla dessa ohjälpsamma meddelanden. I princip skapade jag två nya scheman:
CREATE SCHEMA fake AUTHORIZATION dbo; CREATE SCHEMA shadow AUTHORIZATION dbo;
Sedan flyttade jag Lookup_Shadow-tabellen till cache-schemat och döpte om den:
ALTER SCHEMA shadow TRANSFER dbo.Lookup_Shadow; EXEC sp_rename N'shadow.Lookup_Shadow', N'Lookup';
(Om du bara implementerar den här lösningen skulle du skapa en ny kopia av tabellen i schemat, inte flytta den befintliga tabellen dit och byta namn på den.)
Med dessa två scheman på plats, och en kopia av uppslagstabellen i skuggschemat, blev mitt trevägsbytenamn en trevägsschemaöverföring:
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;
Vid det här laget kan du naturligtvis tömma skuggkopian av tabellen, men i vissa fall tyckte jag att det var användbart att lämna den "gamla" kopian av data i felsökningssyfte:
TRUNCATE TABLE shadow.Lookup;
Allt ytterligare som du gör med skuggkopian, vill du se till att du gör utanför transaktionen – de två överföringsoperationerna ska vara så kortfattade och snabba som möjligt.
Några varningar
- Främmande nycklar
Detta fungerar inte direkt om uppslagstabellen refereras av främmande nycklar. I vårt fall pekade vi inte på några begränsningar vid dessa cachetabeller, men om du gör det kan du behöva hålla fast vid påträngande metoder som MERGE. Eller använd bara tilläggsmetoder och inaktivera eller släpp de främmande nycklarna innan du utför några dataändringar (återskapa eller återaktivera dem sedan). Om du håller fast vid MERGE / UPSERT-tekniker och du gör detta mellan servrar eller, ännu värre, från ett fjärrsystem, rekommenderar jag starkt att du hämtar rådata lokalt istället för att försöka använda dessa metoder mellan servrar.
- Statistik
Att byta tabeller (med hjälp av byta namn eller schemaöverföring) kommer att leda till att statistik bläddrar fram och tillbaka mellan de två kopiorna av tabellen, och detta kan uppenbarligen vara ett problem för planer. Så du kan överväga att lägga till explicita statistikuppdateringar som en del av denna process.
- Andra tillvägagångssätt
Det finns såklart andra sätt att göra detta på som jag helt enkelt inte har haft tillfälle att prova. Partitionsbyte och användning av en vy + synonym är två tillvägagångssätt som jag kan komma att undersöka i framtiden för en mer grundlig behandling av ämnet. Jag skulle vara intresserad av att höra dina erfarenheter och hur du har löst detta problem i din miljö. Och ja, jag inser att detta problem till stor del löses av tillgänglighetsgrupper och läsbara sekundärer i SQL Server 2012, men jag anser att det är ett "trickshot" om du kan lösa problemet utan att kasta avancerade licenser på problemet, eller replikera en hela databasen för att göra några tabeller överflödiga. :-)
Slutsats
Om du kan leva med begränsningarna här kan det här tillvägagångssättet mycket väl fungera bättre än ett scenario där du i princip tar ett bord offline med SSIS eller din egen MERGE / UPSERT-rutin, men var noga med att testa båda teknikerna. Den viktigaste punkten är att slutanvändaren som kommer åt tabellen bör ha exakt samma upplevelse, när som helst på dygnet, även om de träffar bordet mitt under din periodiska uppdatering.