Database
 sql >> Baza danych >  >> RDS >> Database

Filtrowane indeksy i wymuszona parametryzacja (redux)

Po blogowaniu o tym, jak filtrowane indeksy mogą być bardziej wydajne, a ostatnio o tym, jak można je uczynić bezużytecznymi przez wymuszoną parametryzację, wracam do tematu filtrowanych indeksów/parametryzacji. Pozornie zbyt proste rozwiązanie pojawiło się ostatnio w pracy i musiałem się podzielić.

Weźmy następujący przykład, gdzie mamy bazę danych sprzedaży zawierającą tabelę zamówień. Czasami potrzebujemy tylko listy (lub liczby) tylko zamówień, które nie zostały jeszcze wysłane — które z czasem (miejmy nadzieję!) stanowią coraz mniejszy procent całej tabeli:

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;

W tym scenariuszu sensowne może być utworzenie filtrowanego indeksu w ten sposób (co ułatwia pracę z dowolnymi zapytaniami, które próbują uzyskać w tych niewysłanych zamówieniach):

CREATE INDEX ix_OrdersNotShipped 
  ON dbo.Orders(IsShipped, OrderDate) 
  WHERE IsShipped = 0;

Możemy uruchomić szybkie zapytanie, takie jak to, aby zobaczyć, jak wykorzystuje filtrowany indeks:

SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;

Plan wykonania jest dość prosty, ale jest ostrzeżenie o UnmatchedIndexes:

Nazwa ostrzeżenia jest nieco myląca — optymalizator mógł ostatecznie użyć indeksu, ale sugeruje, że byłoby „lepiej” bez parametrów (których wprost nie użyliśmy), mimo że oświadczenie wygląda tak, jakby było sparametryzowane:

Jeśli naprawdę chcesz, możesz wyeliminować ostrzeżenie, bez różnicy w rzeczywistej wydajności (byłoby to tylko kosmetyczne). Jednym ze sposobów jest dodanie predykatu o zerowym wpływie, takiego jak AND (1 > 0) :

SELECT wadd = OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 AND (1 > 0);

Innym (prawdopodobnie bardziej powszechnym) jest dodanie OPTION (RECOMPILE) :

SELECT wrecomp = OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 OPTION (RECOMPILE);

Obie te opcje dają ten sam plan (poszukiwanie bez ostrzeżeń):

Jak na razie dobrze; nasz filtrowany indeks jest używany (zgodnie z oczekiwaniami). To oczywiście nie jedyne sztuczki; zobacz poniższe komentarze dla innych, które czytelnicy już przesłali.

W takim razie komplikacja

Ponieważ baza danych podlega dużej liczbie zapytań ad hoc, ktoś włącza wymuszoną parametryzację, próbując zmniejszyć kompilację i wyeliminować plany o niskim i jednorazowym użyciu z zaśmiecania pamięci podręcznej planów:

ALTER DATABASE Sales SET PARAMETERIZATION FORCED;

Teraz nasze oryginalne zapytanie nie może używać filtrowanego indeksu; jest zmuszony do skanowania indeksu klastrowego:

SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;

Powraca ostrzeżenie o niedopasowanych indeksach i otrzymujemy nowe ostrzeżenia o szczątkowych I/O. Zauważ, że instrukcja jest sparametryzowana, ale wygląda trochę inaczej:

Jest to zgodne z projektem, ponieważ jedynym celem wymuszonej parametryzacji jest parametryzacja zapytań takich jak ta. Ale jest to sprzeczne z celem naszego filtrowanego indeksu, ponieważ ma on wspierać pojedynczą wartość w predykacie, a nie parametr, który może się zmienić.

Wygłup

Nasze „podstępne” zapytanie, które używa dodatkowego predykatu, również nie może użyć filtrowanego indeksu i kończy się nieco bardziej skomplikowanym planem uruchamiania:

SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 AND (1 > 0);

OPCJA (REKOMPILACJA)

Typową reakcją w tym przypadku, podobnie jak w przypadku wcześniejszego usunięcia ostrzeżenia, jest dodanie OPTION (RECOMPILE) do oświadczenia. To działa i pozwala wybrać filtrowany indeks w celu wydajnego wyszukiwania…

SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 OPTION (RECOMPILE);

…ale dodając OPTION (RECOMPILE) i branie tego dodatkowego trafienia kompilacji na każde wykonanie zapytania nie zawsze będzie akceptowalne w środowiskach o dużym natężeniu ruchu (zwłaszcza jeśli są one już powiązane z procesorem).

Wskazówki

Ktoś zasugerował, aby wyraźnie zasugerować filtrowany indeks, aby uniknąć kosztów ponownej kompilacji. Ogólnie jest to dość kruche, ponieważ opiera się na indeksie, który przetrwał kod; Zwykle używam tego w ostateczności. W tym przypadku i tak jest nieważne. Gdy reguły parametryzacji uniemożliwiają optymalizatorowi automatyczne wybranie przefiltrowanego indeksu, uniemożliwiają również wybranie go ręcznie. Ten sam problem z ogólnym FORCESEEK wskazówka:

SELECT OrderID, OrderDate FROM dbo.Orders WITH (INDEX (ix_OrdersNotShipped)) WHERE IsShipped = 0;
 
SELECT OrderID, OrderDate FROM dbo.Orders WITH (FORCESEEK) WHERE IsShipped = 0;

Oba powodują ten błąd:

Msg 8622, poziom 16, stan 1
Procesor kwerend nie może utworzyć planu kwerendy z powodu wskazówek zdefiniowanych w tej kwerendzie. Ponownie prześlij zapytanie bez określania żadnych wskazówek i bez użycia SET FORCEPLAN.

A to ma sens, ponieważ nie ma sposobu, aby dowiedzieć się, że nieznana wartość dla IsShipped parametr będzie pasował do filtrowanego indeksu (lub będzie obsługiwał operację wyszukiwania na dowolnym indeksie).

Dynamiczny SQL?

Zasugerowałem, że możesz użyć dynamicznego SQL, aby przynajmniej zapłacić za trafienie rekompilacji tylko wtedy, gdy wiesz, że chcesz trafić na mniejszy indeks:

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;

Prowadzi to do tego samego efektywnego planu, co powyżej. Jeśli zmieniłeś zmienną na @IsShipped = 1 , otrzymasz droższe skanowanie indeksu klastrowego, jakiego powinieneś się spodziewać:

Ale nikt nie lubi używać dynamicznego SQL w tak skrajnym przypadku — sprawia to, że kod jest trudniejszy do odczytania i utrzymania, a nawet gdyby ten kod znajdował się w aplikacji, nadal należałoby tam dodać dodatkową logikę, co czyni ją mniej niż pożądaną .

Coś prostszego

Rozmawialiśmy krótko o zaimplementowaniu przewodnika po planie, co z pewnością nie jest prostsze, ale potem kolega zasugerował, że można oszukać optymalizator, „ukrywając” sparametryzowaną instrukcję wewnątrz procedury składowanej, widoku lub wbudowanej funkcji z wartościami tabelarycznymi. To było tak proste, że nie wierzyłem, że to zadziała.

Ale potem spróbowałem:

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

Wszystkie trzy z tych zapytań wykonują efektywne wyszukiwanie według filtrowanego indeksu:

EXEC dbo.GetUnshippedOrders;
GO
SELECT OrderID, OrderDate FROM dbo.vUnshippedOrders;
GO
SELECT OrderID, OrderDate FROM dbo.fnUnshippedOrders();

Wniosek

Byłem zaskoczony, że to było tak skuteczne. Oczywiście wymaga to zmiany aplikacji; jeśli nie możesz zmienić kodu aplikacji, aby wywołać procedurę składowaną lub odwołać się do widoku lub funkcji (lub nawet dodać OPTION (RECOMPILE) ), będziesz musiał szukać innych opcji. Ale jeśli możesz zmienić kod aplikacji, umieszczenie predykatu w innym module może być po prostu dobrym rozwiązaniem.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Co to jest T-SQL?

  2. Twój kompletny przewodnik po SQL Join:CROSS JOIN – część 3

  3. Jak zmienić nazwę kolumny w SQL?

  4. Jak łączyć ciągi w SQL

  5. Zabezpiecz swoje klastry Mongo za pomocą SSL