Na te hebben geblogd over hoe gefilterde indexen krachtiger zouden kunnen zijn, en meer recentelijk over hoe ze onbruikbaar kunnen worden gemaakt door geforceerde parametrering, ga ik terug naar het onderwerp gefilterde indexen/parameterisatie. Onlangs kwam er een schijnbaar te eenvoudige oplossing op het werk en die moest ik delen.
Neem het volgende voorbeeld, waar we een verkoopdatabase hebben met een tabel met bestellingen. Soms willen we alleen een lijst (of een telling) van alleen de nog te verzenden bestellingen - die na verloop van tijd (hopelijk!) een steeds kleiner percentage van de totale tabel vertegenwoordigen:
CREATE DATABASE Sales; GO USE Sales; GO -- simplified, obviously: CREATE TABLE dbo.Orders ( OrderID int IDENTITY(1,1) PRIMARY KEY, OrderDate datetime NOT NULL, filler char(500) NOT NULL DEFAULT '', IsShipped bit NOT NULL DEFAULT 0 ); GO -- let's put some data in there; 7,000 shipped orders, and 50 unshipped: INSERT dbo.Orders(OrderDate, IsShipped) -- random dates over two years SELECT TOP (7000) DATEADD(DAY, ABS(object_id % 730), '20171101'), 1 FROM sys.all_columns UNION ALL -- random dates from this month SELECT TOP (50) DATEADD(DAY, ABS(object_id % 30), '20191201'), 0 FROM sys.all_columns;
In dit scenario kan het zinvol zijn om een gefilterde index zoals deze te maken (die snel werk maakt van alle vragen die proberen om die niet-verzonden bestellingen te krijgen):
CREATE INDEX ix_OrdersNotShipped ON dbo.Orders(IsShipped, OrderDate) WHERE IsShipped = 0;
We kunnen een snelle query als deze uitvoeren om te zien hoe deze de gefilterde index gebruikt:
SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;
Het uitvoeringsplan is vrij eenvoudig, maar er is een waarschuwing over UnmatchedIndexes:
De naam van de waarschuwing is enigszins misleidend - de optimizer was uiteindelijk in staat om de index te gebruiken, maar suggereert dat het "beter" zou zijn zonder parameters (die we niet expliciet hebben gebruikt), hoewel de verklaring eruitziet alsof deze is geparametriseerd:
Als je echt wilt, kun je de waarschuwing verwijderen, zonder verschil in werkelijke prestaties (het zou alleen cosmetisch zijn). Een manier is om een predikaat zonder impact toe te voegen, zoals AND (1 > 0)
:
SELECT wadd = OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 AND (1 > 0);
Een andere (waarschijnlijk vaker voorkomende) is het toevoegen van OPTION (RECOMPILE)
:
SELECT wrecomp = OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 OPTION (RECOMPILE);
Beide opties leveren hetzelfde plan op (een zoektocht zonder waarschuwingen):
Tot nu toe, zo goed; onze gefilterde index wordt gebruikt (zoals verwacht). Dit zijn natuurlijk niet de enige trucs; zie de opmerkingen hieronder voor anderen die lezers al hebben ingediend.
Dan de complicatie
Omdat de database onderhevig is aan een groot aantal ad-hocquery's, schakelt iemand geforceerde parametrering in, in een poging de compilatie te verminderen en te voorkomen dat plannen voor laag en eenmalig gebruik de plancache vervuilen:
ALTER DATABASE Sales SET PARAMETERIZATION FORCED;
Nu kan onze oorspronkelijke query de gefilterde index niet gebruiken; het wordt gedwongen om de geclusterde index te scannen:
SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;
De waarschuwing over ongeëvenaarde indexen keert terug en we krijgen nieuwe waarschuwingen over resterende I/O. Merk op dat het statement geparametriseerd is, maar het ziet er een beetje anders uit:
Dit is zo ontworpen, aangezien het hele doel van geforceerde parametrering is om query's als deze te parametriseren. Maar het verslaat het doel van onze gefilterde index, aangezien die bedoeld is om een enkele waarde in het predikaat te ondersteunen, niet een parameter die kan veranderen.
Tomfoolery
Onze "truc"-query die het extra predikaat gebruikt, kan ook de gefilterde index niet gebruiken en eindigt met een iets gecompliceerder plan om op te starten:
SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 AND (1 > 0);
OPTIE (HERCOMPILEREN)
De typische reactie in dit geval, net als bij het verwijderen van de waarschuwing eerder, is het toevoegen van OPTION (RECOMPILE)
naar de verklaring. Dit werkt en maakt het mogelijk om de gefilterde index te kiezen voor een efficiënte zoekactie...
SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 OPTION (RECOMPILE);
…maar toevoegen van OPTION (RECOMPILE)
en het is niet altijd acceptabel om deze extra compilatiehit tegen elke uitvoering van de query te nemen in omgevingen met een hoog volume (vooral als ze al CPU-gebonden zijn).
Hints
Iemand stelde voor om expliciet de gefilterde index te gebruiken om de kosten van het opnieuw compileren te vermijden. Over het algemeen is dit nogal broos, omdat het erop vertrouwt dat de index de code overleeft; Ik heb de neiging om dit als laatste redmiddel te gebruiken. In dit geval is het sowieso niet geldig. Wanneer parametreringsregels voorkomen dat de optimizer de gefilterde index automatisch kiest, voorkomen ze ook dat u deze handmatig kiest. Hetzelfde probleem met een generieke FORCESEEK
hint:
SELECT OrderID, OrderDate FROM dbo.Orders WITH (INDEX (ix_OrdersNotShipped)) WHERE IsShipped = 0; SELECT OrderID, OrderDate FROM dbo.Orders WITH (FORCESEEK) WHERE IsShipped = 0;
Beide leveren deze fout op:
Msg 8622, Level 16, State 1Queryprocessor kon geen queryplan produceren vanwege de hints die in deze query zijn gedefinieerd. Dien de query opnieuw in zonder hints op te geven en zonder SET FORCEPLAN te gebruiken.
En dit is logisch, want er is geen manier om te weten dat de onbekende waarde voor de IsShipped
parameter komt overeen met de gefilterde index (of ondersteunt een zoekbewerking op elke index).
Dynamische SQL?
Ik stelde voor dat je dynamische SQL zou kunnen gebruiken, om in ieder geval alleen die hercompileringshit te betalen als je weet dat je de kleinere index wilt halen:
DECLARE @IsShipped bit = 0; DECLARE @sql nvarchar(max) = N'SELECT dynsql = OrderID, OrderDate FROM dbo.Orders' + CASE WHEN @IsShipped IS NOT NULL THEN N' WHERE IsShipped = @IsShipped' ELSE N'' END + CASE WHEN @IsShipped = 0 THEN N' OPTION (RECOMPILE)' ELSE N'' END; EXEC sys.sp_executesql @sql, N'@IsShipped bit', @IsShipped;
Dit leidt tot hetzelfde efficiënte plan als hierboven. Als u de variabele hebt gewijzigd in @IsShipped = 1
, dan krijgt u de duurdere geclusterde indexscan die u mag verwachten:
Maar niemand vindt het leuk om dynamische SQL te gebruiken in een edge-case als deze - het maakt code moeilijker te lezen en te onderhouden, en zelfs als deze code in de toepassing zou zijn, is het nog steeds extra logica die daar zou moeten worden toegevoegd, waardoor het minder dan wenselijk is .
Iets eenvoudiger
We hebben kort gesproken over het implementeren van een plangids, wat zeker niet eenvoudiger is, maar toen suggereerde een collega dat je de optimizer voor de gek kon houden door de geparametriseerde instructie te "verbergen" in een opgeslagen procedure, weergave of inline tabelwaardefunctie. Het was zo eenvoudig, ik geloofde niet dat het zou werken.
Maar toen probeerde ik het:
CREATE PROCEDURE dbo.GetUnshippedOrders AS BEGIN SET NOCOUNT ON; SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0; END GO CREATE VIEW dbo.vUnshippedOrders AS SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0; GO CREATE FUNCTION dbo.fnUnshippedOrders() RETURNS TABLE AS RETURN (SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0); GO
Alle drie deze zoekopdrachten voeren de efficiënte zoekactie uit tegen de gefilterde index:
EXEC dbo.GetUnshippedOrders; GO SELECT OrderID, OrderDate FROM dbo.vUnshippedOrders; GO SELECT OrderID, OrderDate FROM dbo.fnUnshippedOrders();
Conclusie
Ik was verrast dat dit zo effectief was. Hiervoor moet u natuurlijk wel de applicatie wijzigen; als u de app-code niet kunt wijzigen om een opgeslagen procedure aan te roepen of naar de weergave of functie te verwijzen (of zelfs OPTION (RECOMPILE)
toe te voegen ), moet je blijven zoeken naar andere opties. Maar als u de toepassingscode kunt wijzigen, is het misschien de beste keuze om het predikaat in een andere module te proppen.