Det har længe været fastslået, at tabelvariable med et stort antal rækker kan være problematiske, da optimeringsværktøjet altid ser dem som at have én række. Uden en rekompilering, efter at tabelvariablen er blevet udfyldt (siden før den er tom), er der ingen kardinalitet for tabellen, og automatiske rekompileringer sker ikke, fordi tabelvariabler ikke engang er underlagt en rekompileringstærskel. Planer er derfor baseret på en tabelkardinalitet på nul, ikke én, men minimum er øget til én, som Paul White (@SQL_Kiwi) beskriver i dette dba.stackexchange-svar.
Den måde, vi typisk kan løse dette problem på, er at tilføje OPTION (RECOMPILE)
til forespørgslen, der refererer til tabelvariablen, hvilket tvinger optimeringsværktøjet til at inspicere tabelvariablens kardinalitet, efter at den er blevet udfyldt. For at undgå behovet for manuelt at ændre hver forespørgsel for at tilføje et eksplicit genkompileringstip, er et nyt sporingsflag (2453) blevet introduceret i SQL Server 2012 Service Pack 2 og SQL Server 2014 Kumulativ opdatering #3:
- KB #2952444 :RETNING:Dårlig ydeevne, når du bruger tabelvariabler i SQL Server 2012 eller SQL Server 2014
Når sporingsflag 2453 er aktivt, kan optimeringsværktøjet opnå et nøjagtigt billede af tabelkardinalitet, efter at tabelvariablen er blevet oprettet. Dette kan være A Good Thing™ til mange forespørgsler, men sandsynligvis ikke alle, og du bør være opmærksom på, hvordan det fungerer anderledes end OPTION (RECOMPILE)
. Mest bemærkelsesværdigt forekommer parameterindlejringsoptimeringen Paul White taler om i dette indlæg under OPTION (RECOMPILE)
, men ikke under dette nye sporingsflag.
En simpel test
Min første test bestod i blot at udfylde en tabelvariabel og vælge fra den; dette gav det alt for velkendte estimerede rækkeantal på 1. Her er testen, jeg kørte (og jeg tilføjede et genkompileringstip for at sammenligne):
Ved at bruge SQL Sentry Plan Explorer kan vi se, at den grafiske plan for begge forespørgsler i dette tilfælde er identisk, sandsynligvis i det mindste delvist, fordi dette bogstaveligt talt er en triviel plan:
Grafisk plan for en triviel indeksscanning mod @t
Estimaterne er dog ikke de samme. Selvom sporingsflaget er aktiveret, får vi stadig et estimat på 1, der kommer ud af indeksscanningen, hvis vi ikke bruger genkompileringstip:
Sammenligning af estimater for en triviel plan i erklæringsgitteret
Sammenligning af estimater mellem sporingsflag (venstre) og rekompilere (højre)
Hvis du nogensinde har været omkring mig personligt, kan du sikkert forestille dig det ansigt, jeg lavede på dette tidspunkt. Jeg troede med sikkerhed, at enten KB-artiklen angav det forkerte sporingsflagnummer, eller at jeg havde brug for en anden indstilling aktiveret, for at den virkelig kunne være aktiv.
Benjamin Nevarez (@BenjaminNevarez) gjorde mig hurtigt opmærksom på, at jeg var nødt til at se nærmere på artiklen "Bugs, der er rettet i SQL Server 2012 Service Pack 2" KB-artiklen. Mens de har sløret teksten bag en skjult punkttegn under Højdepunkter> Relationel motor, gør fixlisteartiklen et lidt bedre stykke arbejde med at beskrive sporingsflagets adfærd end den originale artikel (fremhæv min):
Hvis en tabelvariabel sammenføjes med andre tabeller i SQL Server kan det resultere i langsom ydeevne på grund af ineffektivt valg af forespørgselsplan, fordi SQL Server ikke understøtter statistik eller sporer antallet af rækker i en tabelvariabel under kompilering af en forespørgselsplan.Så det fremgår af denne beskrivelse, at sporingsflaget kun er beregnet til at løse problemet, når tabelvariablen deltager i en joinforbindelse. (Hvorfor den skelnen ikke er lavet i den originale artikel, aner jeg ikke.) Men det virker også, hvis vi får forespørgslerne til at arbejde lidt mere – ovenstående forespørgsel anses for trivielt af optimeringsværktøjet, og sporingsflaget gør' ikke engang forsøge at gøre noget i det tilfælde. Men det vil slå ind, hvis omkostningsbaseret optimering udføres, selv uden en join; sporflaget har simpelthen ingen effekt på trivielle planer. Her er et eksempel på en ikke-triviel plan, der ikke involverer en joinforbindelse:
Denne plan er ikke længere triviel; optimering er markeret som fuld. Hovedparten af omkostningerne flyttes til en sorteringsoperatør:
Mindre triviel grafisk plan
Og estimaterne er på linje for begge forespørgsler (jeg gemmer dig værktøjstip denne gang, men jeg kan forsikre dig om, at de er de samme):
Udsagnsgitter til mindre trivielle planer med og uden genkompileringstip
Så det ser ud til, at KB-artiklen ikke er helt præcis – jeg var i stand til at tvinge den forventede adfærd af sporflaget uden at indføre en join. Men jeg vil også gerne teste det med en join.
En bedre test
Lad os tage dette simple eksempel med og uden sporingsflaget:
Uden sporingsflaget estimerer optimeringsværktøjet, at en række vil komme fra indeksscanningen mod tabelvariablen. Men med sporingsflaget aktiveret, får den de 1.000 rækker bang på:
Sammenligning af indeksscanningsestimater (ingen sporingsflag til venstre, sporflag til højre)
Forskellene stopper ikke der. Hvis vi kigger nærmere, kan vi se en række forskellige beslutninger, som optimeringsværktøjet har truffet, som alle stammer fra disse bedre estimater:
Sammenligning af planer (ingen sporingsflag til venstre, sporingsflag til højre)
En hurtig oversigt over forskellene:
- Forespørgslen uden sporingsflaget har udført 4.140 læseoperationer, mens forespørgslen med det forbedrede estimat kun har udført 424 (ca. 90 % reduktion).
- Optimeringsværktøjet anslog, at hele forespørgslen ville returnere 10 rækker uden sporingsflaget og meget mere nøjagtige 2.318 rækker, når sporingsflaget blev brugt.
- Uden sporingsflaget valgte optimeringsværktøjet at udføre en indlejret loops-sammenføjning (hvilket giver mening, når et af inputs estimeres til at være meget lille). Dette førte til, at sammenkædningsoperatoren og begge indekssøgninger blev udført 1.000 gange, i modsætning til hash-matchet valgt under sporingsflaget, hvor sammenkædningsoperatoren og begge scanninger kun blev udført én gang.
- Tabel I/O-fanen viser også 1.000 scanninger (områdescanninger forklædt som indekssøgninger) og et meget højere logisk læseantal mod
syscolpars
(systemtabellen bagsys.all_columns
). - Selvom varigheden ikke var væsentligt påvirket (24 millisekunder vs. 18 millisekunder), kan du sikkert forestille dig, hvilken indvirkning disse andre forskelle kan have på en mere seriøs forespørgsel.
- Hvis vi skifter diagrammet til estimerede omkostninger, kan vi se, hvor vidt forskellige tabelvariablen kan narre optimeringsværktøjet uden sporingsflaget:
Sammenligning af estimerede rækkeantal (ingen sporingsflag til venstre, spor flag til højre)
Det er tydeligt og ikke chokerende, at optimeringsværktøjet gør et bedre stykke arbejde med at vælge den rigtige plan, når den har et præcist overblik over den involverede kardinalitet. Men til hvilken pris?
Rekompilerer og overhead
Når vi bruger OPTION (RECOMPILE)
med ovenstående batch, uden sporingsflaget aktiveret, får vi følgende plan – som er stort set identisk med planen med sporingsflaget (den eneste mærkbare forskel er, at de estimerede rækker er 2.316 i stedet for 2.318):
Samme forespørgsel med OPTION (RECOMPILE)
Så dette kan få dig til at tro, at sporingsflaget opnår lignende resultater ved at udløse en genkompilering for dig hver gang. Vi kan undersøge dette ved at bruge en meget simpel session med udvidede begivenheder:
OPRET EVENT SESSION [CaptureRecompiles] PÅ SERVER TILFØJ BEGIVENHED sqlserver.sql_statement_recompile ( ACTION(sqlserver.sql_text) ) TILFØJ MÅL pakke0.asynchronous_file_target ( SET FILENAME =N'CaptureRecompilEVS.TempileReg:\TempileRecompilESS:\TempileRecompilEV); CaptureRecompiles] PÅ SERVER STATE =START;
Jeg kørte følgende sæt batches, som udførte 20 forespørgsler uden (a) ingen genkompileringsindstilling eller sporingsflag, (b) genkompileringsindstillingen og (c) et sporingsflag på sessionsniveau.
/* standard - intet sporingsflag, ingen rekompilere */ DECLARE @t TABLE(id INT PRIMÆR NØGLE, navn SYSNAVN IKKE NULL UNIK); INSERT @t SELECT TOP (1000) [object_id], navn FRA sys.all_objects; VÆLG t.navn, c.navn FRA @t AS t VENSTRE YDRE JOIN sys.all_columns AS c PÅ t.id =c.[object_id]; GO 20 /* recompile */ DECLARE @t TABLE(id INT PRIMARY KEY, name SYSNAME NOT NULL UNIQUE); INSERT @t SELECT TOP (1000) [object_id], navn FRA sys.all_objects; SELECT t.name, c.name FRA @t AS t LEFT OUTER JOIN sys.all_columns AS c ON t.id =c.[object_id] OPTION (RECOMPILE); GO 20 /* sporingsflag */ DBCC TRACEON(2453); DECLARE @t TABLE(id INT PRIMÆR NØGLE, navn SYSNAVN IKKE NULL UNIK); INSERT @t SELECT TOP (1000) [object_id], navn FRA sys.all_objects; VÆLG t.navn, c.navn FRA @t AS t VENSTRE YDRE JOIN sys.all_columns AS c PÅ t.id =c.[object_id]; DBCC TRACEOFF(2453); GO 20
Så kiggede jeg på begivenhedsdataene:
SELECT sql_text =LEFT(sql_text, 255), recompile_count =COUNT(*)FROM ( SELECT x.x.value(N'(event/action[@name="sql_text"]/value)[1]',N'nvarchar (max)') FRA sys.fn_xe_file_target_read_file(N'C:\temp\CaptureRecompiles*.xel',NULL,NULL,NULL) SOM f CROSS APPLY (SELECT CONVERT(XML, f.event_data)) AS x(x)) AS x(sql_text)GROUP BY LEFT(sql_text, 255);
Resultaterne viser, at der ikke skete nogen genkompilering under standardforespørgslen, sætningen, der refererer til tabelvariablen, blev genkompileret en gang under sporingsflaget og, som du kunne forvente, hver gang med RECOMPILE
mulighed:
sql_text | recompile_count |
---|---|
/* recompile */ DECLARE @t TABEL (i INT … | 20 |
/* sporingsflag */ DBCC TRACEON(2453); ERKLÆR @t … | 1 |
Resultater af forespørgsel mod XEvents-data
Dernæst slukkede jeg for udvidede hændelser og ændrede derefter batchen til at måle i skala. Grundlæggende måler koden 1.000 iterationer af oprettelse og udfyldning af en tabelvariabel, og vælger derefter dens resultater til en #temp-tabel (en måde at undertrykke output af så mange engangsresultatsæt), ved at bruge hver af de tre metoder.
INDSTIL ANTAL TIL; /* standard - intet sporingsflag, ingen rekompilering */ SELECT SYSDATETIME();GODECLARE @t TABLE(id INT PRIMARY KEY, navn SYSNAME IKKE NULL UNIK); INSERT @t SELECT TOP (1000) [object_id], navn FRA sys.all_objects; VÆLG t.id, c.name INTO #x FRA @t AS t VENSTRE YDRE JOIN sys.all_columns AS c ON t.id =c.[object_id]; SLIP TABEL #x; GO 1000SELECT SYSDATETIME();GO /* recompile */ DECLARE @t TABLE(id INT PRIMARY KEY, name SYSNAME NOT NULL UNIQUE); INSERT @t SELECT TOP (1000) [object_id], navn FRA sys.all_objects; SELECT t.id, c.name INTO #x FROM @t AS t LEFT OUTER JOIN sys.all_columns AS c ON t.id =c.[object_id] OPTION (RECOMPILE); SLIP TABEL #x; GO 1000SELECT SYSDATETIME();GO /* trace flag */ DBCC TRACEON(2453); DECLARE @t TABLE(id INT PRIMÆR NØGLE, navn SYSNAVN IKKE NULL UNIK); INSERT @t SELECT TOP (1000) [object_id], navn FRA sys.all_objects; VÆLG t.id, c.name INTO #x FRA @t AS t VENSTRE YDRE JOIN sys.all_columns AS c ON t.id =c.[object_id]; SLIP TABEL #x; DBCC TRACEOFF(2453); GO 1000SELECT SYSDATETIME();GO
Jeg kørte denne batch 10 gange og tog gennemsnittet; de var:
Metode | Gennemsnitlig varighed (millisekunder) |
---|---|
Standard | 23.148,4 |
Genkompiler | 29.959,3 |
Sporingsflag | 22.100,7 |
Gennemsnitlig varighed for 1.000 iterationer
I dette tilfælde var det meget langsommere at få de rigtige estimater hver gang ved at bruge genkompileringshintet end standardadfærden, men det var lidt hurtigere at bruge sporingsflaget. Dette giver mening, fordi – mens begge metoder korrigerer standardadfærden ved at bruge et falsk estimat (og få en dårlig plan som følge heraf), tager omkompileringer ressourcer og, når de ikke eller ikke kan give en mere effektiv plan, har de en tendens til at bidrage til den samlede batchvarighed.
Det virker ligetil, men vent...
Ovenstående test er lidt – og med vilje – mangelfuld. Vi indsætter det samme antal rækker (1.000) i tabelvariablen hver gang . Hvad sker der, hvis den indledende population af tabelvariablen varierer for forskellige batches? Så vil vi helt sikkert se omkompileringer, selv under sporflaget, ikke? Tid til endnu en test. Lad os konfigurere en lidt anderledes udvidet begivenhedssession, bare med et andet målfilnavn (for ikke at blande data fra den anden session):
OPRET BEGIVENHED SESSION [CaptureRecompiles_v2] PÅ SERVER TILFØJ BEGIVENHED sqlserver.sql_statement_recompile ( ACTION(sqlserver.sql_text) ) TILFØJ MÅL-pakke0.asynchronous_file_target ( SET FILERecompiles.temp.S. FILERecompiles \ N'CEVALESS:N'CEVALESS:N'CEVELESS.\temp. CaptureRecompiles_v2] PÅ SERVER STATE =START;
Lad os nu inspicere denne batch og opsætte rækkeantal for hver iteration, der er væsentligt forskellige. Vi kører dette tre gange og fjerner de relevante kommentarer, så vi har én batch uden et sporingsflag eller eksplicit rekompilering, én batch med sporingsflaget og én batch med OPTION (RECOMPILE)
(at have en nøjagtig kommentar i begyndelsen gør disse partier nemmere at identificere på steder som Extended Events output):
/* standard, intet sporingsflag eller recompile *//* recompile *//* trace flag */ DECLARE @i INT =1; WHILE @i <=6BEGIN --DBCC TRACEON(2453); -- fjern kommenter dette for sporingsflag DECLARE @t TABLE(id INT PRIMARY KEY); INSERT @t VÆLG TOP (CASE @i NÅR 1 SÅ 24 NÅR 2 SÅ 1782 NÅR 3 SÅ 1701 NÅR 4 SÅ 12 NÅR 5 SÅ 15 NÅR 6 SÅ SLUT 1560) [object_id] FROM sys.all_object; VÆLG t.id, c.name FRA @t AS t INNER JOIN sys.all_objects AS c ON t.id =c.[object_id] --OPTION (RECOMPILE); -- fjern kommentere dette for genkompilering --DBCC TRACEOFF(2453); -- fjern kommenter dette for sporingsflag DELETE @t; SET @i +=1;END
Jeg kørte disse batches i Management Studio, åbnede dem individuelt i Plan Explorer og filtrerede sætningstræet på kun SELECT
forespørgsel. Vi kan se den forskellige adfærd i de tre batches ved at se på estimerede og faktiske rækker:
Sammenligning af tre batches, ser på estimerede vs. faktiske rækker
I gitteret længst til højre kan du tydeligt se, hvor genkompileringer ikke fandt sted under sporingsflaget
Vi kan tjekke XEvents-dataene for at se, hvad der rent faktisk skete med omkompileringer:
SELECT sql_text =LEFT(sql_text, 255), recompile_count =COUNT(*)FROM ( SELECT x.x.value(N'(event/action[@name="sql_text"]/value)[1]',N'nvarchar (max)') FRA sys.fn_xe_file_target_read_file(N'C:\temp\CaptureRecompiles_v2*.xel',NULL,NULL,NULL) SOM f CROSS APPLY (SELECT CONVERT(XML, f.event_data)) AS x(x)) AS x(sql_text)GROUP BY LEFT(sql_text, 255);
Resultater:
sql_text | recompile_count |
---|---|
/* recompile */ DECLARE @i INT =1; MENS … | 6 |
/* sporingsflag */ DECLARE @i INT =1; MENS … | 4 |
Resultater af forespørgsel mod XEvents-data
Meget interessant! Under sporingsflaget *ser* vi rekompileringer, men kun når runtime-parameterværdien har varieret væsentligt fra den cachelagrede værdi. Når runtime-værdien er anderledes, men ikke ret meget, får vi ikke en rekompilering, og de samme estimater bruges. Så det er klart, at sporingsflaget introducerer en genkompileringstærskel til tabelvariabler, og jeg har bekræftet (gennem en separat test), at denne bruger den samme algoritme som den, der er beskrevet for #temp-tabeller i dette "gamle" men stadig relevante papir. Det vil jeg bevise i et opfølgende indlæg.
Igen tester vi ydeevnen, kører batchen 1.000 gange (med udvidet begivenhedssession slået fra) og måler varigheden:
Metode | Gennemsnitlig varighed (millisekunder) |
---|---|
Standard | 101.285,4 |
Genkompiler | 111.423,3 |
Sporingsflag | 110.318.2 |
Gennemsnitlig varighed for 1.000 iterationer
I dette specifikke scenarie mister vi omkring 10 % af ydeevnen ved at tvinge en omkompilering hver gang eller ved at bruge et sporingsflag. Ikke helt sikker på, hvordan deltaet blev fordelt:Var planerne baseret på bedre skøn ikke væsentligt bedre? Opvejede genkompileringer eventuelle præstationsgevinster med så meget ? Jeg vil ikke bruge for meget tid på dette, og det var et trivielt eksempel, men det viser dig, at det kan være en uforudsigelig affære at lege med den måde, optimizeren fungerer på. Nogle gange kan du være bedre stillet med standardadfærden kardinalitet =1, vel vidende at du aldrig vil forårsage unødige rekompileringer. Hvor sporingsflaget kan give meget mening er, hvis du har forespørgsler, hvor du gentagne gange udfylder tabelvariabler med det samme sæt data (f.eks. en postnummeropslagstabel), eller du altid bruger 50 eller 1.000 rækker (f.eks. udfylder en tabelvariabel til brug i paginering). Under alle omstændigheder bør du bestemt teste, hvilken indvirkning dette har på enhver arbejdsbyrde, hvor du planlægger at introducere sporingsflaget eller eksplicitte rekompilere.
TVP'er og tabeltyper
Jeg var også nysgerrig efter, hvordan dette ville påvirke tabeltyper, og om vi ville se nogen forbedringer i kardinalitet for TVP'er, hvor det samme symptom eksisterer. Så jeg oprettede en simpel tabeltype, der efterligner tabelvariablen, der er brugt indtil videre:
BRUG MyTestDB;GO OPRET TYPE dbo.t SOM TABEL (id INT PRIMÆR NØGLE);
Så tog jeg ovenstående batch og erstattede simpelthen DECLARE @t TABLE(id INT PRIMARY KEY);
med DECLARE @t dbo.t;
– alt andet forblev nøjagtig det samme. Jeg kørte de samme tre batches, og her er, hvad jeg så:
Sammenligning af estimater og faktiske værdier mellem standardadfærd, mulighedsrekompilering og sporingsflag 2453
Så ja, det ser ud til, at sporingsflaget fungerer på nøjagtig samme måde med TVP'er – genkompileringer genererer nye estimater for optimeringsværktøjet, når rækkeantallet overstiger genkompileringstærsklen, og springes over, når rækkeantallet er "tæt nok på."
Fordele, ulemper og forbehold
En fordel ved sporingsflaget er, at du kan undgå nogle rekompilerer og stadig ser tabelkardinalitet – så længe du forventer, at antallet af rækker i tabelvariablen er stabilt, eller ikke observerer væsentlige planafvigelser på grund af varierende kardinalitet. En anden er, at du kan aktivere det globalt eller på sessionsniveau og ikke behøver at introducere genkompileringstip til alle dine forespørgsler. Og endelig, i det mindste i det tilfælde, hvor tabelvariablens kardinalitet var stabil, førte korrekte estimater til bedre ydeevne end standarden og også bedre ydeevne end at bruge genkompileringsindstillingen – alle disse kompileringer kan helt sikkert lægges sammen.
Der er selvfølgelig også nogle ulemper. En, som jeg nævnte ovenfor, er den sammenlignet med OPTION (RECOMPILE)
du går glip af visse optimeringer, såsom parameterindlejring. En anden er, at sporflaget ikke vil have den indvirkning, du forventer på trivielle planer. Og en jeg opdagede undervejs er at bruge QUERYTRACEON
tip til at håndhæve sporingsflaget på forespørgselsniveauet virker ikke – så vidt jeg kan se, skal sporingsflaget være på plads, når tabelvariablen eller TVP oprettes og/eller udfyldes, for at optimizeren kan se kardinaliteten ovenfor 1.
Husk, at kørsel af sporingsflaget globalt introducerer muligheden for forespørgselsplanregression til enhver forespørgsel, der involverer en tabelvariabel (hvilket er grunden til, at denne funktion blev introduceret under et sporingsflag i første omgang), så sørg for at teste hele din arbejdsbyrde uanset hvordan du bruger sporflaget. Når du tester denne adfærd, skal du også gøre det i en brugerdatabase; nogle af de optimeringer og forenklinger, som du normalt forventer vil ske, sker bare ikke, når konteksten er sat til tempdb, så enhver adfærd, du observerer der, forbliver muligvis ikke konsistent, når du flytter koden og indstillingerne til en brugerdatabase.
Konklusion
Hvis du bruger tabelvariabler eller TVP'er med et stort, men relativt ensartet antal rækker, kan du finde det fordelagtigt at aktivere dette sporingsflag for bestemte batches eller procedurer for at få nøjagtig tabelkardinalitet uden manuelt at tvinge en rekompilering på individuelle forespørgsler. Du kan også bruge sporingsflaget på instansniveau, hvilket vil påvirke alle forespørgsler. Men som enhver ændring skal du i begge tilfælde være omhyggelig med at teste ydeevnen af hele din arbejdsbyrde, se eksplicit efter eventuelle regressioner og sikre, at du vil have sporingsflag-adfærden, fordi du kan stole på stabiliteten af din tabelvariabel. række tæller.
Jeg er glad for at se sporingsflaget tilføjet til SQL Server 2014, men det ville være bedre, hvis det bare blev standardadfærden. Ikke at der er nogen væsentlig fordel ved at bruge store tabelvariable i forhold til store #temp-tabeller, men det ville være rart at se mere paritet mellem disse to midlertidige strukturtyper, som kunne dikteres på et højere niveau. Jo mere paritet vi har, jo mindre skal folk overveje, hvilken en de skal bruge (eller i det mindste have færre kriterier at overveje, når de vælger). Martin Smith har et godt spørgsmål og svar på dba.stackexchange, som sandsynligvis nu skal til en opdatering:Hvad er forskellen mellem en midlertidig tabel og tabelvariabel i SQL Server?
Vigtig bemærkning
Hvis du skal installere SQL Server 2012 Service Pack 2 (uanset om det er for at gøre brug af dette sporingsflag), se venligst også mit indlæg om en regression i SQL Server 2012 og 2014, der – i sjældne scenarier – kan introducere potentielt datatab eller korruption under genopbygning af onlineindeks. Der er kumulative opdateringer tilgængelige for SQL Server 2012 SP1 og SP2 og også til SQL Server 2014. Der vil ikke være nogen rettelse til 2012 RTM-grenen.
Yderligere test
Jeg har andre ting på min liste at teste. For det første vil jeg gerne se, om dette sporingsflag har nogen effekt på In-Memory tabeltyper i SQL Server 2014. Jeg vil også bevise ud over en skygge af tvivl, at sporingsflag 2453 bruger den samme rekompileringstærskel for tabel variabler og TVP'er, som det gør for #temp-tabeller.