In dit artikel gaan we in op het onderwerp prestatie van tabelvariabelen. In SQL Server kunnen we variabelen maken die als volledige tabellen werken. Misschien hebben andere databases dezelfde mogelijkheden, maar ik heb dergelijke variabelen alleen in MS SQL Server gebruikt.
U kunt dus het volgende schrijven:
declare @t as table (int value)
Hier declareren we de @t-variabele als een tabel die een enkele kolom Waarde van het type Integer zal bevatten. Het is mogelijk om complexere tabellen te maken, maar in ons voorbeeld is één kolom voldoende om de optimalisatie te verkennen.
Nu kunnen we deze variabele in onze query's gebruiken. We kunnen er veel gegevens aan toevoegen en gegevens ophalen uit deze variabele:
insert into @t select UserID from User or select * from @t
Ik heb gemerkt dat tabelvariabelen worden gebruikt wanneer het nodig is om gegevens op te halen voor een grote selectie. Er is bijvoorbeeld een query in de code die gebruikers van de site retourneert. Nu verzamelt u ID's van alle gebruikers, voegt u ze toe aan de tabelvariabele en kunt u adressen voor deze gebruikers zoeken. Misschien vraagt iemand zich af waarom we niet één query op de database uitvoeren en alles meteen krijgen? Ik heb een eenvoudig voorbeeld.
Stel dat gebruikers afkomstig zijn van de webservice, terwijl hun adressen in uw database zijn opgeslagen. In dit geval is er geen uitweg. We hebben een aantal gebruikers-ID's van de service gekregen en om te voorkomen dat de database wordt doorzocht, besluit iemand dat het gemakkelijker is om alle ID's als tabelvariabele aan de queryparameter toe te voegen en dat de query er netjes uitziet:
select * from @t as users join Address a on a.UserID = users.UserID os
Dit alles werkt correct. In de C#-code kun je met LINQ snel de resultaten van beide data-arrays combineren tot één object. De prestaties van de zoekopdracht kunnen er echter onder lijden.
Feit is dat tabelvariabelen niet zijn ontworpen voor het verwerken van grote hoeveelheden gegevens. Als ik me niet vergis, gebruikt de query-optimizer altijd de LOOP-uitvoeringsmethode. Voor elke ID van @t wordt dus gezocht in de adrestabel. Als er 1000 records in @t zijn, zal de server het adres 1000 keer scannen.
Wat de uitvoering betreft, stopt de server vanwege het waanzinnige aantal scans gewoon met het zoeken naar gegevens.
Het is veel effectiever om de hele adrestabel te scannen en alle gebruikers tegelijk te vinden. Deze methode wordt MERGE genoemd. SQL Server kiest het echter wanneer er veel gesorteerde gegevens zijn. In dit geval weet de optimizer niet hoeveel en welke gegevens aan de variabele worden toegevoegd en of er wordt gesorteerd, omdat een dergelijke variabele geen indexen bevat.
Als er weinig gegevens in de tabelvariabele staan en u voegt er geen duizenden rijen in, dan is alles in orde. Als u echter dergelijke variabelen wilt gebruiken en er een enorme hoeveelheid gegevens aan wilt toevoegen, moet u verder lezen.
Zelfs als u de tabelvariabele vervangt door SQL, zal dit de queryprestaties aanzienlijk versnellen:
select * from ( Select 10377 as UserID Union all Select 73736 Union all Select 7474748 …. ) as users join Address a on a.UserID = users.UserID
Er kunnen duizend van dergelijke SELECT-instructies zijn en de querytekst zal enorm zijn, maar het zal duizenden keren sneller worden uitgevoerd voor een grote hoeveelheid gegevens omdat SQL Server een effectief uitvoeringsplan kan kiezen.
Deze zoekopdracht ziet er niet geweldig uit. Het uitvoeringsplan kan echter niet in de cache worden opgeslagen, omdat het wijzigen van slechts één ID ook de hele querytekst zal veranderen en parameters niet kunnen worden gebruikt.
Ik denk dat Microsoft niet had verwacht dat gebruikers tabelvariabelen op deze manier zouden gebruiken, maar er is een goede oplossing.
Er zijn verschillende manieren om dit probleem op te lossen. Naar mijn mening is het echter het meest effectief in termen van prestaties om OPTION (RECOMPILE) toe te voegen aan het einde van de zoekopdracht:
select * from @t as users join Address a on a.UserID = users.UserID OPTION (RECOMPILE)
Deze optie wordt één keer helemaal aan het einde van de query toegevoegd, zelfs na ORDER BY. Het doel van deze optie is om ervoor te zorgen dat SQL Server de query bij elke uitvoering opnieuw compileert.
Als we daarna de zoekopdrachtprestaties meten, zal de tijd voor het uitvoeren van de zoekopdracht hoogstwaarschijnlijk worden verkort. Bij grote hoeveelheden data kan de prestatieverbetering aanzienlijk zijn, van tientallen minuten tot seconden. Nu compileert de server zijn code voordat elke query wordt uitgevoerd en gebruikt niet het uitvoeringsplan uit de cache, maar genereert een nieuwe, afhankelijk van de hoeveelheid gegevens in de variabele, en dit helpt meestal veel.
Het nadeel is dat het uitvoeringsplan niet wordt opgeslagen en de server elke keer de query moet compileren en moet zoeken naar een effectief uitvoeringsplan. Ik heb echter geen query's gezien waarbij dit proces meer dan 100 ms duurde.
Is het een slecht idee om tabelvariabelen te gebruiken? Nee dat is het niet. Onthoud alleen dat ze niet zijn gemaakt voor grote hoeveelheden gegevens. Soms is het beter om een tijdelijke tabel te maken, als er veel gegevens zijn, en gegevens in deze tabel in te voegen, of zelfs direct een index te maken. Ik heb dit met rapporten moeten doen, zij het slechts één keer. Destijds heb ik de tijd voor het genereren van één rapport teruggebracht van 3 uur naar 20 minuten.
Ik gebruik liever één grote query in plaats van deze op te splitsen in meerdere query's en de resultaten op te slaan in variabelen. Sta SQL Server toe om de prestaties van een grote query af te stemmen en het zal u niet teleurstellen. Houd er rekening mee dat u alleen in extreme gevallen uw toevlucht moet nemen tot tabelvariabelen als u hun voordelen echt ziet.