Denne artikel er den tolvte del i en serie om navngivne tabeludtryk. Indtil videre har jeg dækket afledte tabeller og CTE'er, som er sætningsomfangede navngivne tabeludtryk, og visninger, som er genbrugelige navngivne tabeludtryk. I denne måned introducerer jeg inline-tabelværdierede funktioner eller iTVF'er og beskriver deres fordele sammenlignet med de andre navngivne tabeludtryk. Jeg sammenligner dem også med lagrede procedurer, hovedsageligt med fokus på forskelle med hensyn til standardoptimeringsstrategi og planlægning af caching og genbrugsadfærd. Der er meget at dække med hensyn til optimering, så jeg starter diskussionen i denne måned og fortsætter den næste måned.
I mine eksempler vil jeg bruge en prøvedatabase kaldet TSQLV5. Du kan finde scriptet, der opretter og udfylder det her, og dets ER-diagram her.
Hvad er en inline-tabel-vurderet funktion?
Sammenlignet med de tidligere dækkede navngivne tabeludtryk, ligner iTVF'er for det meste visninger. Ligesom visninger oprettes iTVF'er som et permanent objekt i databasen og kan derfor genbruges af brugere, der har tilladelser til at interagere med dem. Den største fordel iTVF'er har sammenlignet med visninger er, at de understøtter inputparametre. Så den nemmeste måde at beskrive en iTVF på er som en parametriseret visning, selvom du teknisk set opretter den med en CREATE FUNCTION-sætning og ikke med en CREATE VIEW-sætning.
Det er vigtigt ikke at forveksle iTVF'er med multi-statement table-valued functions (MSTVF'er). Førstnævnte er et inlinable navngivet tabeludtryk baseret på en enkelt forespørgsel, der ligner en visning og er fokus for denne artikel. Sidstnævnte er et programmatisk modul, der returnerer en tabelvariabel som output, med multi-sætningsflow i sin krop, hvis formål er at fylde den returnerede tabelvariabel med data.
Syntaks
Her er T-SQL-syntaksen til at oprette en iTVF:
OPRET [ ELLER ÆNDRING ] FUNKTION [
[ (
RETURTABEL
[ MED
SOM
TILBAGE
Observer i syntaksen evnen til at definere inputparametre.
Formålet med SCHEMABIDNING-attributten er det samme som med visninger og bør evalueres ud fra lignende overvejelser. For detaljer, se del 10 i serien.
Et eksempel
Som et eksempel for en iTVF, antag, at du skal oprette et genbrugeligt navngivet tabeludtryk, der accepterer som input et kunde-id (@custid) og et tal (@n) og returnerer det anmodede antal af de seneste ordrer fra Sales.Orders-tabellen for inputkunden.
Du kan ikke implementere denne opgave med en visning, da visninger mangler understøttelse af inputparametre. Som nævnt kan du tænke på en iTVF som en parametriseret visning, og som sådan er det det rigtige værktøj til denne opgave.
Før du implementerer selve funktionen, er her kode til at oprette et understøttende indeks på Sales.Orders-tabellen:
USE TSQLV5; GO CREATE INDEX idx_nc_cid_odD_oidD_i_eid ON Sales.Orders(custid, orderdate DESC, orderid DESC) INCLUDE(empid);
Og her er koden til at oprette funktionen med navnet Sales.GetTopCustOrders:
CREATE OR ALTER FUNCTION Sales.GetTopCustOrders ( @custid AS INT, @n AS BIGINT ) RETURNS TABLE AS RETURN SELECT TOP (@n) orderid, orderdate, empid FROM Sales.Orders WHERE custid = @custid ORDER BY orderdate DESC, orderid DESC; GO
Ligesom med basistabeller og visninger, når du er ude efter at hente data, angiver du iTVF'er i FROM-sætningen i en SELECT-sætning. Her er et eksempel, der anmoder om de tre seneste ordrer for kunde 1:
SELECT orderid, orderdate, empid FROM Sales.GetTopCustOrders(1, 3);
Jeg vil referere til dette eksempel som forespørgsel 1. Planen for forespørgsel 1 er vist i figur 1.
Figur 1:Plan for forespørgsel 1
Hvad er inline ved iTVF'er?
Hvis du undrer dig over kilden til udtrykket inline i inline tabel-værdisatte funktioner, har det at gøre med, hvordan de bliver optimeret. Inlining-konceptet er anvendeligt på alle fire slags navngivne tabeludtryk, som T-SQL understøtter, og involverer til dels, hvad jeg beskrev i del 4 i serien som unnesting/substitution. Sørg for at gense det relevante afsnit i del 4, hvis du har brug for en genopfriskning.
Som du kan se i figur 1, takket være det faktum, at funktionen blev inlinet, var SQL Server i stand til at skabe en optimal plan, der interagerer direkte med den underliggende basistabels indekser. I vores tilfælde udfører planen en søgning i det understøttende indeks, du oprettede tidligere.
iTVF'er tager inlining-konceptet et skridt videre ved at anvende parameterindlejringsoptimering som standard. Paul White beskriver parameterindlejringsoptimering i sin fremragende artikel Parameter Sniffing, Embedding, and the RECOMPILE Options. Med parameterindlejringsoptimering erstattes forespørgselsparameterreferencer med de bogstavelige konstantværdier fra den aktuelle udførelse, og derefter bliver koden med konstanterne optimeret.
Bemærk i planen i figur 1, at både søgeprædikatet for indekssøgningsoperatoren og topudtrykket for topoperatoren viser de indlejrede bogstavelige konstantværdier 1 og 3 fra den aktuelle forespørgselsudførelse. De viser ikke parametrene henholdsvis @custid og @n.
Med iTVF'er bruges parameterindlejringsoptimering som standard. Med lagrede procedurer er parametriserede forespørgsler optimeret som standard. Du skal tilføje OPTION(RECOMPILE) til en lagret procedures forespørgsel for at anmode om parameterindlejringsoptimering. Flere detaljer om optimering af iTVF'er versus lagrede procedurer, herunder implikationer, snart.
Ændring af data gennem iTVF'er
Husk fra del 11 i serien, at så længe visse krav er opfyldt, kan navngivne tabeludtryk være et mål for modifikationsudsagn. Denne evne gælder for iTVF'er på samme måde som den gælder for visninger. For eksempel, her er kode, du kan bruge til at slette de tre seneste ordrer fra kunde 1 (kør faktisk ikke dette):
DELETE FROM Sales.GetTopCustOrders(1, 3);
Specifikt i vores database vil et forsøg på at køre denne kode mislykkes på grund af håndhævelse af referenceintegritet (de berørte ordrer har tilfældigvis relaterede ordrelinjer i tabellen Sales.OrderDetails), men det er gyldig og understøttet kode.
iTVF'er vs. lagrede procedurer
Som tidligere nævnt er standardforespørgselsoptimeringsstrategien for iTVF'er anderledes end den for lagrede procedurer. Med iTVF'er er standardindstillingen at bruge parameterindlejringsoptimering. Med lagrede procedurer er standarden at optimere parametriserede forespørgsler, mens parametersniffing anvendes. For at få parameterindlejring for en lagret procedureforespørgsel skal du tilføje OPTION(RECOMPILE).
Som med mange optimeringsstrategier og -teknikker har parameterindlejring sine plusser og minusser.
Det største plus er, at det muliggør forespørgselsforenklinger, der nogle gange kan resultere i mere effektive planer. Nogle af disse forenklinger er virkelig fascinerende. Paul demonstrerer dette med lagrede procedurer i sin artikel, og jeg vil demonstrere dette med iTVFs næste måned.
Det største minus ved parameterindlejringsoptimering er, at du ikke får effektiv plan-caching og genbrugsadfærd, som du gør for parametriserede planer. Med hver enkelt kombination af parameterværdier får du en særskilt forespørgselsstreng og dermed en separat kompilering, der resulterer i en separat cachelagret plan. Med iTVF'er med konstante input kan du få plangenbrugsadfærd, men kun hvis de samme parameterværdier gentages. Det er klart, at en lagret procedureforespørgsel med OPTION(RECOMPILE) ikke genbruger en plan, selv når de samme parameterværdier gentages efter anmodning.
Jeg vil demonstrere tre tilfælde:
- Genanvendelige planer med konstanter som følge af standardparameterindlejringsoptimering for iTVF-forespørgsler med konstanter
- Genanvendelige parametriserede planer som følge af standardoptimeringen af parameteriserede lagrede procedureforespørgsler
- Ikke-genanvendelige planer med konstanter som følge af parameterindlejringsoptimering for lagrede procedureforespørgsler med OPTION(RECOMPILE)
Lad os starte med case #1.
Brug følgende kode til at forespørge på vores iTVF med @custid =1 og @n =3:
SELECT orderid, orderdate, empid FROM Sales.GetTopCustOrders(1, 3);
Som en påmindelse ville dette være den anden udførelse af den samme kode, da du allerede har udført den én gang med de samme parameterværdier tidligere, hvilket resulterer i planen vist i figur 1.
Brug følgende kode til at forespørge iTVF med @custid =2 og @n =3 én gang:
SELECT orderid, orderdate, empid FROM Sales.GetTopCustOrders(2, 3);
Jeg vil referere til denne kode som forespørgsel 2. Planen for forespørgsel 2 er vist i figur 2.
Figur 2:Plan for forespørgsel 2
Husk, at planen i figur 1 for forespørgsel 1 refererede til det konstante kunde-id 1 i søgeprædikatet, hvorimod denne plan refererer til det konstante kunde-id 2.
Brug følgende kode til at undersøge udførelsesstatistikker for forespørgsler:
SELECT Q.plan_handle, Q.execution_count, T.text, P.query_plan FROM sys.dm_exec_query_stats AS Q CROSS APPLY sys.dm_exec_sql_text(Q.plan_handle) AS T CROSS APPLY sys.dm_exec_query_plan(Q.plan_handle) AS P WHERE T.text LIKE '%Sales.' + 'GetTopCustOrders(%';
Denne kode genererer følgende output:
plan_handle execution_count text query_plan ------------------- --------------- ---------------------------------------------- ---------------- 0x06000B00FD9A1... 1 SELECT ... FROM Sales.GetTopCustOrders(2, 3); <ShowPlanXML...> 0x06000B00F5C34... 2 SELECT ... FROM Sales.GetTopCustOrders(1, 3); <ShowPlanXML...> (2 rows affected)
Der er oprettet to separate planer her:en for forespørgslen med kunde-id 1, som blev brugt to gange, og en anden for forespørgslen med kunde-id 2, som blev brugt én gang. Med et meget stort antal forskellige kombinationer af parameterværdier ender du med et stort antal kompileringer og cachelagrede planer.
Lad os fortsætte med case #2:standardoptimeringsstrategien for parameteriserede lagrede procedureforespørgsler. Brug følgende kode til at indkapsle vores forespørgsel i en lagret procedure kaldet Sales.GetTopCustOrders2:
CREATE OR ALTER PROC Sales.GetTopCustOrders2 ( @custid AS INT, @n AS BIGINT ) AS SET NOCOUNT ON; SELECT TOP (@n) orderid, orderdate, empid FROM Sales.Orders WHERE custid = @custid ORDER BY orderdate DESC, orderid DESC; GO
Brug følgende kode til at udføre den lagrede procedure med @custid =1 og @n =3 to gange:
EXEC Sales.GetTopCustOrders2 @custid = 1, @n = 3; EXEC Sales.GetTopCustOrders2 @custid = 1, @n = 3;
Den første udførelse udløser optimeringen af forespørgslen, hvilket resulterer i den parametriserede plan vist i figur 3:
Figur 3:Plan for Sales.GetTopCustOrders2 proc
Bemærk referencen til parameteren @custid i søgeprædikatet og til parameteren @n i det øverste udtryk.
Brug følgende kode til at udføre den lagrede procedure med @custid =2 og @n =3 én gang:
EXEC Sales.GetTopCustOrders2 @custid = 2, @n = 3;
Den cachelagrede parametriserede plan vist i figur 3 genbruges igen.
Brug følgende kode til at undersøge udførelsesstatistikker for forespørgsler:
SELECT Q.plan_handle, Q.execution_count, T.text, P.query_plan FROM sys.dm_exec_query_stats AS Q CROSS APPLY sys.dm_exec_sql_text(Q.plan_handle) AS T CROSS APPLY sys.dm_exec_query_plan(Q.plan_handle) AS P WHERE T.text LIKE '%Sales.' + 'GetTopCustOrders2%';
Denne kode genererer følgende output:
plan_handle execution_count text query_plan ------------------- --------------- ----------------------------------------------- ---------------- 0x05000B00F1604... 3 ...SELECT TOP (@n)...WHERE custid = @custid...; <ShowPlanXML...> (1 row affected)
Kun én parametriseret plan blev oprettet og cachelagret og brugt tre gange på trods af de skiftende kunde-id-værdier.
Lad os gå videre til sag #3. Som nævnt kan du med stored procedure-forespørgsler få parameterindlejringsoptimering, når du bruger OPTION(RECOMPILE). Brug følgende kode til at ændre procedureforespørgslen til at inkludere denne mulighed:
CREATE OR ALTER PROC Sales.GetTopCustOrders2 ( @custid AS INT, @n AS BIGINT ) AS SET NOCOUNT ON; SELECT TOP (@n) orderid, orderdate, empid FROM Sales.Orders WHERE custid = @custid ORDER BY orderdate DESC, orderid DESC OPTION(RECOMPILE); GO
Udfør proceduren med @custid =1 og @n =3 to gange:
EXEC Sales.GetTopCustOrders2 @custid = 1, @n = 3; EXEC Sales.GetTopCustOrders2 @custid = 1, @n = 3;
Du får den samme plan vist tidligere i figur 1 med de indlejrede konstanter.
Udfør proceduren med @custid =2 og @n =3 én gang:
EXEC Sales.GetTopCustOrders2 @custid = 2, @n = 3;
Du får den samme plan vist tidligere i figur 2 med de indlejrede konstanter.
Undersøg statistikker for udførelse af forespørgsler:
SELECT Q.plan_handle, Q.execution_count, T.text, P.query_plan FROM sys.dm_exec_query_stats AS Q CROSS APPLY sys.dm_exec_sql_text(Q.plan_handle) AS T CROSS APPLY sys.dm_exec_query_plan(Q.plan_handle) AS P WHERE T.text LIKE '%Sales.' + 'GetTopCustOrders2%';
Denne kode genererer følgende output:
plan_handle execution_count text query_plan ------------------- --------------- ----------------------------------------------- ---------------- 0x05000B00F1604... 1 ...SELECT TOP (@n)...WHERE custid = @custid...; <ShowPlanXML...> (1 row affected)
Udførelsesantallet viser 1, hvilket kun afspejler den sidste udførelse. SQL Server cacherer den sidst udførte plan, så den kan vise statistik for den udførelse, men efter anmodning genbruger den ikke planen. Hvis du tjekker planen vist under query_plan-attributten, vil du opdage, at det er den, der er oprettet for konstanterne i den sidste udførelse, vist tidligere i figur 2.
Hvis du er ude efter færre kompileringer og effektiv plancaching og genbrugsadfærd, er standardmetoden til optimering af lagrede procedurer for parameteriserede forespørgsler vejen frem.
Der er en stor fordel, som en iTVF-baseret implementering har frem for en lagret procedure-baseret – når du skal anvende funktionen på hver række i en tabel og sende kolonner fra tabellen som input. Antag for eksempel, at du skal returnere de tre seneste ordrer for hver kunde i tabellen Salg.Kunder. Ingen forespørgselskonstruktion giver dig mulighed for at anvende en lagret procedure pr. række i en tabel. Du kan implementere en iterativ løsning med en markør, men det er altid en god dag, hvor du kan undgå markører. Ved at kombinere APPLY-operatøren med et iTVF-opkald kan du udføre opgaven pænt og rent, sådan:
SELECT C.custid, O.orderid, O.orderdate, O.empid FROM Sales.Customers AS C CROSS APPLY Sales.GetTopCustOrders( C.custid, 3 ) AS O;
Denne kode genererer følgende output (forkortet):
custid orderid orderdate empid ----------- ----------- ---------- ----------- 1 11011 2019-04-09 3 1 10952 2019-03-16 1 1 10835 2019-01-15 1 2 10926 2019-03-04 4 2 10759 2018-11-28 3 2 10625 2018-08-08 3 ... (263 rows affected)
Funktionskaldet bliver inlinet, og referencen til parameteren @custid erstattes med korrelationen C.custid. Dette resulterer i planen vist i figur 4.
Figur 4:Planlæg forespørgsel med APPLY og Sales.GetTopCustOrders iTVF
Planen scanner noget indeks på Sales.Customers-tabellen for at få sættet af kunde-id'er og anvender en søgning i det understøttende indeks, du oprettede tidligere på Sales.Orders pr. kunde. Der er kun én plan, da funktionen blev indlejret i den ydre forespørgsel og blev til en korreleret eller en lateral joinforbindelse. Denne plan er yderst effektiv, især når custid-kolonnen i Sales.Orders er meget tæt, hvilket betyder, når der er et lille antal forskellige kunde-id'er.
Selvfølgelig er der andre måder at implementere denne opgave på, såsom at bruge en CTE med funktionen ROW_NUMBER. En sådan løsning har en tendens til at fungere bedre end den APPLY-baserede, når custid-kolonnen i Sales.Orders-tabellen har lav tæthed. Uanset hvad, er den specifikke opgave, jeg brugte i mine eksempler, ikke så vigtig for vores diskussions formål. Min pointe var at forklare de forskellige optimeringsstrategier, SQL Server anvender med de forskellige værktøjer.
Når du er færdig, skal du bruge følgende kode til oprydning:
DROP INDEX IF EXISTS idx_nc_cid_odD_oidD_i_eid ON Sales.Orders;
Oversigt og hvad er det næste
Så hvad har vi lært af dette?
En iTVF er et genbrugeligt parameteriseret navngivet tabeludtryk.
SQL Server bruger som standard en parameterindlejringsoptimeringsstrategi med iTVF'er og en parametriseret forespørgselsoptimeringsstrategi med lagrede procedureforespørgsler. Tilføjelse af OPTION(RECOMPILE) til en lagret procedureforespørgsel kan resultere i parameterindlejringsoptimering.
Hvis du ønsker at få færre kompileringer og effektiv plancaching og genbrugsadfærd, er parametriserede procedureforespørgselsplaner vejen at gå.
Planer for iTVF-forespørgsler cachelagres og kan genbruges, så længe de samme parameterværdier gentages.
Du kan bekvemt kombinere brugen af APPLY-operatoren og en iTVF for at anvende iTVF'en til hver række fra den venstre tabel ved at sende kolonner fra den venstre tabel som input til iTVF'en.
Som nævnt er der meget at dække om iTVFs optimering. I denne måned sammenlignede jeg iTVF'er og lagrede procedurer med hensyn til standardoptimeringsstrategien og plancaching og genbrugsadfærd. Næste måned vil jeg grave dybere ned i forenklinger som følge af parameterindlejringsoptimering.