sql >> Database teknologi >  >> RDS >> Database

Problemet med vinduesfunktioner og visninger

Introduktion

Siden deres introduktion i SQL Server 2005 har vinduesfunktioner som ROW_NUMBER og RANK har vist sig at være yderst nyttige til at løse en lang række almindelige T-SQL-problemer. I et forsøg på at generalisere sådanne løsninger søger databasedesignere ofte at inkorporere dem i visninger for at fremme kodeindkapsling og genbrug. Desværre betyder en begrænsning i SQL Server-forespørgselsoptimering ofte, at visninger, der indeholder vinduesfunktioner, ikke fungerer så godt som forventet. Dette indlæg gennemgår et illustrativt eksempel på problemet, beskriver årsagerne og giver en række løsninger.

Dette problem kan også forekomme i afledte tabeller, almindelige tabeludtryk og in-line funktioner, men jeg ser det oftest med visninger, fordi de med vilje er skrevet til at være mere generiske.

Vinduefunktioner

Vinduesfunktioner er kendetegnet ved tilstedeværelsen af ​​en OVER() klausul og findes i tre varianter:

  • Raneringsvinduets funktioner
    • ROW_NUMBER
    • RANK
    • DENSE_RANK
    • NTILE
  • Aggregerede vinduesfunktioner
    • MIN , MAX , AVG , SUM
    • COUNT , COUNT_BIG
    • CHECKSUM_AGG
    • STDEV , STDEVP , VAR , VARP
  • Analytiske vinduesfunktioner
    • LAG , LEAD
    • FIRST_VALUE , LAST_VALUE
    • PERCENT_RANK , PERCENTILE_CONT , PERCENTILE_DISC , CUME_DIST

Rangerings- og aggregerede vinduesfunktioner blev introduceret i SQL Server 2005 og udvidet betydeligt i SQL Server 2012. De analytiske vinduesfunktioner er nye for SQL Server 2012.

Alle ovennævnte vinduesfunktioner er følsomme over for optimeringsbegrænsningen beskrevet i denne artikel.

Eksempel

Ved at bruge AdventureWorks-eksempeldatabasen er opgaven at skrive en forespørgsel, der returnerer alle produkt #878-transaktioner, der fandt sted på den seneste tilgængelige dato. Der er alle mulige måder at udtrykke dette krav på i T-SQL, men vi vil vælge at skrive en forespørgsel, der bruger en vinduesfunktion. Det første trin er at finde transaktionsposter for produkt #878 og rangordne dem i faldende datorækkefølge:

SELECT th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.Quantity, rnk =RANK() OVER (ORDER BY th.TransactionDate DESC)FROM Production.TransactionHistory AS thWHERE th.ProductID =878ORDER BY rnk; før> 

Resultaterne af forespørgslen er som forventet, med seks transaktioner, der finder sted på den seneste tilgængelige dato. Udførelsesplanen indeholder en advarselstrekant, der gør os opmærksom på et manglende indeks:

Som sædvanligt for manglende indeksforslag skal vi huske, at anbefalingen ikke er resultatet af en gennemgående analyse af forespørgslen – det er mere en indikation af, at vi skal tænke lidt over, hvordan denne forespørgsel får adgang til de data, den har brug for.

Det foreslåede indeks ville helt sikkert være mere effektivt end at scanne tabellen fuldstændigt, da det ville tillade en indekssøgning til det specifikke produkt, vi er interesseret i. Indekset ville også dække alle de nødvendige kolonner, men det ville ikke undgå sorteringen (ved TransactionDate aftagende). Det ideelle indeks for denne forespørgsel ville tillade en søgning på ProductID , returnerer de valgte poster omvendt TransactionDate rækkefølge, og dække de andre returnerede kolonner:

OPRET IKKE-KLUSTERET INDEX ixON Production.TransactionHistory (ProductID, TransactionDate DESC)INCLUDE (ReferenceOrderID, Quantity);

Med det indeks på plads er eksekveringsplanen meget mere effektiv. Den klyngede indeksscanning er blevet erstattet af en rækkeviddesøgning, og en eksplicit sortering er ikke længere nødvendig:

Det sidste trin for denne forespørgsel er at begrænse resultaterne til kun de rækker, der rangerer #1. Vi kan ikke filtrere direkte i WHERE klausul i vores forespørgsel, fordi vinduesfunktioner muligvis kun vises i SELECT og ORDER BY klausuler.

Vi kan omgå denne begrænsning ved hjælp af en afledt tabel, fælles tabeludtryk, funktion eller visning. Ved denne lejlighed vil vi bruge et almindeligt tabeludtryk (aka en in-line visning):

MED Rangerede Transaktioner AS( SELECT th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.Quantity, rnk =RANK() OVER (ORDER BY th.TransactionDate DESC) FRA Production.TransactionHistory AS th WHERE th.ProductID =878 )SELECT TransactionID, ReferenceOrderID, TransactionDate, QuantityFROM RangedTransactionsWHERE rnk =1;

Udførelsesplanen er den samme som før, med et ekstra filter for kun at returnere rækker rangeret #1:

Forespørgslen returnerer de seks lige rangordnede rækker, vi forventer:

Generalisering af forespørgslen

Det viser sig, at vores forespørgsel er meget nyttig, så det besluttes at generalisere det og gemme definitionen i en visning. For at dette skal fungere for ethvert produkt, skal vi gøre to ting:returnere ProductID fra visningen, og opdel rangeringsfunktionen efter produkt:

OPRET VISNING dbo.MostRecentTransactionsPerProductWITH SCHEMABINDINGASSELECT sq1.ProductID, sq1.TransactionID, sq1.ReferenceOrderID, sq1.TransactionDate, sq1.QuantityFROM ( SELECT th.ProductID, th.ReferenceD.Orth., th.TransactionID, th.Reference.D., rnk =RANK() OVER ( PARTITION BY th.ProductID ORDER BY th.TransactionDate DESC) FROM Production.TransactionHistory AS th) AS sq1WHERE sq1.rnk =1;

Valg af alle rækker fra visningen resulterer i følgende udførelsesplan og korrekte resultater:

Vi kan nu finde de seneste transaktioner for produkt 878 med en meget enklere forespørgsel på visningen:

SELECT mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =878;

Vores forventning er, at eksekveringsplanen for denne nye forespørgsel vil være nøjagtig den samme, som før vi oprettede visningen. Forespørgselsoptimeringsværktøjet bør være i stand til at skubbe det filter, der er angivet i WHERE klausul ned i visningen, hvilket resulterer i en indekssøgning.

Vi skal dog stoppe op og tænke lidt på dette tidspunkt. Forespørgselsoptimeringsværktøjet kan kun producere eksekveringsplaner, der med garanti giver de samme resultater som den logiske forespørgselsspecifikation – er det sikkert at skubbe vores WHERE klausul ind i visningen?PARTITION BY klausul af vinduesfunktionen i visningen. Begrundelsen er, at eliminering af komplette grupper (partitioner) fra vinduesfunktionen ikke vil påvirke rangeringen af ​​rækker, der returneres af forespørgslen. Spørgsmålet er, om SQL Server-forespørgselsoptimeringsværktøjet ved dette? Svaret afhænger af, hvilken version af SQL Server vi kører.

SQL Server 2005 eksekveringsplan

Et kig på filteregenskaberne i denne plan viser, at den anvender to prædikater:

ProductID = 878 prædikatet er ikke blevet skubbet ned i visningen, hvilket resulterer i en plan, der scanner vores indeks, rangerer hver række i tabellen før filtrering efter produkt #878 og rækker rangeret som #1.

SQL Server 2005-forespørgselsoptimeringsværktøjet kan ikke skubbe passende prædikater forbi en vinduesfunktion i et lavere forespørgselsomfang (visning, fælles tabeludtryk, in-line funktion eller afledt tabel). Denne begrænsning gælder for alle SQL Server 2005-builds.

SQL Server 2008+ eksekveringsplan

Dette er udførelsesplanen for den samme forespørgsel på SQL Server 2008 eller nyere:

ProductID prædikatet er blevet skubbet forbi de rangerende operatører og erstatter indeksscanningen med den effektive indekssøgning.

2008-forespørgselsoptimeringsværktøjet indeholder en ny forenklingsregel SelOnSeqPrj (vælg på sekvensprojekt), der er i stand til at skubbe sikre ydre-scope-prædikater tidligere vinduesfunktioner. For at lave den mindre effektive plan for denne forespørgsel i SQL Server 2008 eller nyere, skal vi midlertidigt deaktivere denne forespørgselsoptimeringsfunktion:

SELECT mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =878OPTION (QUERYRULEOFF SelOnSeqPrj);

Desværre er SelOnSeqPrj forenklingsregel virker kun når prædikatet udfører en sammenligning med en konstant . Af den grund producerer følgende forespørgsel den suboptimale plan på SQL Server 2008 og nyere:

DECLARE @ProductID INT =878; VÆLG mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =@ProductID;

Problemet kan stadig opstå, selv hvor prædikatet bruger en konstant værdi. SQL Server kan beslutte at auto-parameterisere trivielle forespørgsler (en for hvilken der findes en åbenlys bedste plan). Hvis automatisk parametrering er vellykket, ser optimeringsværktøjet en parameter i stedet for en konstant, og SelOnSeqPrj reglen anvendes ikke.

For forespørgsler, hvor auto-parameterisering ikke er forsøgt (eller hvor det er fastslået at være usikkert), kan optimeringen stadig mislykkes, hvis databaseindstillingen for FORCED PARAMETERIZATION er tændt. Vores testforespørgsel (med den konstante værdi 878) er ikke sikker for auto-parameterisering, men den tvungne parameteriseringsindstilling tilsidesætter dette, hvilket resulterer i den ineffektive plan:

ALTER DATABASE AdventureWorksSET PARAMETERIZATION FORCED;GOSELECT mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE HVOR mrt.> 

SQL Server 2008+ løsning

For at tillade optimeringsværktøjet at 'se' en konstant værdi for forespørgsel, der refererer til en lokal variabel eller parameter, kan vi tilføje en OPTION (RECOMPILE) forespørgselstip:

DECLARE @ProductID INT =878; VÆLG mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =@ProductIDOPTION (RECOMPILE);

Bemærk: Pre-execution ('estimeret') eksekveringsplan viser stadig en indeksscanning, fordi værdien af ​​variablen faktisk ikke er indstillet endnu. Når forespørgslen er udført , dog viser udførelsesplanen den ønskede indekssøgningsplan:

SelOnSeqPrj regel findes ikke i SQL Server 2005, så OPTION (RECOMPILE) kan ikke hjælpe der. Hvis du undrer dig, er OPTION (RECOMPILE) workaround resulterer i en søgning, selvom databaseindstillingen for tvungen parameterisering er slået til.

Alle versioner omgåelse #1

I nogle tilfælde er det muligt at erstatte den problematiske visning, det almindelige tabeludtryk eller den afledte tabel med en parametriseret in-line tabelværdi-funktion:

CREATE FUNCTION dbo.MostRecentTransactionsForProduct( @ProductID integer) RETURNER TABELWITH SCHEMABINDING ASRETURN SELECT sq1.ProductID, sq1.TransactionID, sq1.ReferenceOrderID, sq1.TransactionDate, sq1.THE SELECTID. ReferenceOrderID, th.TransactionDate, th.Quantity, rnk =RANK() OVER ( PARTITION BY th.ProductID ORDER BY th.TransactionDate DESC) FRA Production.TransactionHistory AS th WHERE th.ProductID =@ProductID ) AS sq1 WHERE =sq1. WHERE 1;

Denne funktion placerer eksplicit ProductID prædikat i samme omfang som vinduesfunktionen, hvorved optimeringsbegrænsningen undgås. Skrevet til at bruge in-line-funktionen, bliver vores eksempelforespørgsel:

SELECT mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsForProduct(878) AS mrt;

Dette producerer den ønskede indekssøgningsplan på alle versioner af SQL Server, der understøtter vinduesfunktioner. Denne løsning producerer en søgning, selv hvor prædikatet refererer til en parameter eller lokal variabel – OPTION (RECOMPILE) er ikke påkrævet.PARTITION BY klausul og for ikke længere at returnere ProductID kolonne. Jeg lod definitionen være den samme som den visning, den erstattede for mere tydeligt at illustrere årsagen til forskellene i udførelsesplanen.

Alle versioner omgåelse #2

Den anden løsning gælder kun for rangeringsvinduefunktioner, der filtreres for at returnere rækker nummereret eller rangeret #1 (ved hjælp af ROW_NUMBER , RANK , eller DENSE_RANK ). Dette er dog en meget almindelig brug, så det er værd at nævne.

En yderligere fordel er, at denne løsning kan producere planer, der er endnu mere effektive end de tidligere set indekssøgningsplaner. Som en påmindelse så den tidligere bedste plan således ud:

Denne udførelsesplan rangerer 1.918 rækker, selvom det i sidste ende kun returnerer 6 . Vi kan forbedre denne eksekveringsplan ved at bruge vinduesfunktionen i en ORDER BY klausul i stedet for at rangordne rækker og derefter filtrere efter rang #1:

VÆLG TOP (1) MED BINDELSER th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.QuantityFROM Production.TransactionHistory AS thWHERE th.ProductID =878ORDER BY RANK() OVER (ORDER BY th. 

Denne forespørgsel illustrerer fint brugen af ​​en vinduesfunktion i ORDER BY klausul, men vi kan gøre det endnu bedre ved at eliminere vinduesfunktionen fuldstændigt:

VÆLG TOP (1) MED BINDELSER th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.QuantityFROM Production.TransactionHistory AS thWHERE th.ProductID =878ORDER BY th.TransactionDate DESC;

Denne plan læser kun 7 rækker fra tabellen for at returnere det samme 6-rækkers resultatsæt. Hvorfor 7 rækker? Topoperatøren kører i WITH TIES tilstand:

Den fortsætter med at anmode om en række ad gangen fra dens undertræ, indtil TransactionDate ændres. Den syvende række er påkrævet for at toppen skal være sikker på, at der ikke er flere rækker med bundet værdi, der kvalificerer sig.

Vi kan udvide logikken i forespørgslen ovenfor for at erstatte den problematiske visningsdefinition:

ALTER VIEW dbo.MostRecentTransactionsPerProductWITH SCHEMABINDINGASSELECT p.ProductID, Ranged1.TransactionID, Ranking1.ReferenceOrderID, Rang1.TransactionDate, Ranged1.QuantityFROM -- Liste over produkt-ID'er (SELECT ProductID FROM Production.Product) SOM pCROSS GÆLDER( -- Returnerer GÆLDER( #1 resultater for hvert produkt-ID VÆLG TOP (1) MED BINDELSER th.TransactionID, th.ReferenceOrderID, th.TransactionDate, th.Quantity FROM Production.TransactionHistory AS th WHERE th.ProductID =p.ProductID ORDER BY th.TransactionDate DESC) AS rangeret 1;

Visningen bruger nu en CROSS APPLY at kombinere resultaterne af vores optimerede ORDER BY forespørgsel for hvert produkt. Vores testforespørgsel er uændret:

DECLARE @ProductID heltal;SET @ProductID =878; VÆLG mrt.ProductID, mrt.TransactionID, mrt.ReferenceOrderID, mrt.TransactionDate, mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =@ProductID;

Både før- og efterudførelsesplaner viser en indekssøgning uden behov for en OPTION (RECOMPILE) forespørgselstip. Følgende er en efterudførelse ('faktisk') plan:

Hvis visningen havde brugt ROW_NUMBER i stedet for RANK , ville erstatningsvisningen simpelthen have udeladt WITH TIES klausul på TOP (1) . Den nye visning kunne selvfølgelig også skrives som en parametriseret in-line tabel-vurderet funktion.

Man kan argumentere for, at den oprindelige indekssøgningsplan med rnk = 1 prædikat kunne også optimeres til kun at teste 7 rækker. Når alt kommer til alt, bør optimeringsværktøjet vide, at rangeringer produceres af Sequence Project-operatøren i strengt stigende rækkefølge, så udførelsen kan ende, så snart en række med en rang, der er større end én, ses. Optimizeren indeholder dog ikke denne logik i dag.

Sidste tanker

Folk er ofte skuffede over udførelsen af ​​visninger, der inkorporerer vinduesfunktioner. Årsagen kan ofte spores tilbage til optimeringsbegrænsningen beskrevet i dette indlæg (eller måske fordi visningsdesigneren ikke satte pris på, at prædikater anvendt på visningen skal vises i PARTITION BY klausul skal skubbes sikkert ned).

Jeg vil gerne understrege, at denne begrænsning ikke kun gælder for visninger, og den er heller ikke begrænset til ROW_NUMBER , RANK og DENSE_RANK . Du skal være opmærksom på denne begrænsning, når du bruger en funktion med en OVER klausul i en visning, almindeligt tabeludtryk, afledt tabel eller in-line-tabelvurderet funktion.

SQL Server 2005-brugere, der støder på dette problem, står over for valget mellem at omskrive visningen som en parametriseret in-line tabelværdi-funktion eller bruge APPLY teknik (hvor relevant).

SQL Server 2008-brugere har den ekstra mulighed for at bruge en OPTION (RECOMPILE) forespørgselstip, hvis problemet kan løses ved at lade optimeringsværktøjet se en konstant i stedet for en variabel eller parameterreference. Husk dog at tjekke efterudførelsesplaner, når du bruger dette tip:præudførelsesplanen kan generelt ikke vise den optimale plan.


  1. Kontrollerer oracle-side og databasenavn

  2. 3 måder at få jobtrinene for et SQL Server Agent Job (T-SQL)

  3. Tilslut Java til en MySQL-database

  4. Brug af LIKE i en Oracle IN-klausul