Typy danych parametrów
Jak wspomniano w pierwszej części tej serii, jednym z powodów, dla których lepiej jest jawnie sparametryzować, jest pełna kontrola nad typami danych parametrów. Prosta parametryzacja ma wiele dziwactw w tym obszarze, które mogą skutkować większą liczbą sparametryzowanych planów zapisanych w pamięci podręcznej niż oczekiwano lub znalezieniem innych wyników w porównaniu z wersją niesparametryzowaną.
Kiedy SQL Server stosuje prostą parametryzację do instrukcji ad-hoc, zgaduje typ danych parametru zastępczego. Powody zgadywania omówię w dalszej części serii.
Na razie spójrzmy na kilka przykładów korzystających z bazy danych Stack Overflow 2010 na SQL Server 2019 CU 14. Kompatybilność bazy danych jest ustawiona na 150, a próg kosztów dla równoległości jest ustawiony na 50, aby na razie uniknąć równoległości:
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; GO SELECT U.DisplayName FROM dbo.Users AS U WHERE U.Reputation = 252; GO SELECT U.DisplayName FROM dbo.Users AS U WHERE U.Reputation = 25221; GO SELECT U.DisplayName FROM dbo.Users AS U WHERE U.Reputation = 252552;
Wynikiem tych stwierdzeń jest sześć planów w pamięci podręcznej, trzy Adhoc i trzy Przygotowane :
Różne odgadnięte typy
Zwróć uwagę na różne typy danych parametrów w Przygotowane plany.
Wnioskowanie o typie danych
Szczegóły dotyczące odgadywania każdego typu danych są złożone i niekompletnie udokumentowane. Jako punkt wyjścia, SQL Server wywnioskuje typ podstawowy z tekstowej reprezentacji wartości, a następnie używa najmniejszego zgodnego podtypu.
Dla ciągu liczb bez cudzysłowów lub kropki dziesiętnej, SQL Server wybiera z tinyint
, smallint
i integer
. Dla takich liczb poza zakresem integer
, SQL Server używa numeric
z najmniejszą możliwą precyzją. Na przykład liczba 2147483648 jest wpisywana jako numeric(10,0)
. bigint
typ nie jest używany do parametryzacji po stronie serwera. Ten akapit wyjaśnia typy danych wybrane we wcześniejszych przykładach.
Ciągi liczb z kropka dziesiętna jest interpretowana jako numeric
, z dokładnością i skalą wystarczająco dużą, aby pomieścić podaną wartość. Ciągi znaków poprzedzone symbolem waluty są interpretowane jako money
. Łańcuchy w notacji naukowej tłumaczone są na float
. smallmoney
i real
typy nie są stosowane.
datetime
i uniqueidentifer
nie można wywnioskować typów z naturalnych formatów ciągów. Aby uzyskać datetime
lub uniqueidentifer
typ parametru, wartość literału musi być podana w formacie ucieczki ODBC. Na przykład {d '1901-01-01'}
, {ts '1900-01-01 12:34:56.790'}
lub {guid 'F85C72AB-15F7-49E9-A949-273C55A6C393'}
. W przeciwnym razie zamierzona data lub literał UUID jest wpisywany jako ciąg. Typy daty i godziny inne niż datetime
nie są używane.
Ogólne napisy i literały binarne są wpisywane jako varchar(8000)
, nvarchar(4000)
lub varbinary(8000)
odpowiednio, chyba że literał przekracza 8000 bajtów, w którym to przypadku max
używany jest wariant. Ten schemat pomaga uniknąć zanieczyszczenia pamięci podręcznej i niskiego poziomu ponownego wykorzystania, który wynikałby z używania określonych długości.
Nie można użyć CAST
lub CONVERT
aby ustawić typ danych dla parametrów z powodów, które opiszę w dalszej części tej serii. Przykład tego znajduje się w następnej sekcji.
Nie będę omawiać parametryzacji wymuszonej w tej serii, ale chcę wspomnieć o regułach wnioskowania o typach danych w takim przypadku mają pewne istotne różnice w porównaniu z prostą parametryzacją . Wymuszona parametryzacja została dodana dopiero w SQL Server 2005, więc firma Microsoft miała możliwość włączenia kilku lekcji z prostej parametryzacji doświadczenie i nie musiałem się martwić o problemy ze zgodnością wsteczną.
Typy numeryczne
Dla liczb z kropką dziesiętną i liczb całkowitych poza zakresem integer
, reguły wywnioskowanego typu stwarzają szczególne problemy związane z ponownym wykorzystaniem planów i zanieczyszczeniem pamięci podręcznej.
Rozważ następujące zapytanie z użyciem ułamków dziesiętnych:
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; GO DROP TABLE IF EXISTS dbo.Test; GO CREATE TABLE dbo.Test ( SomeValue decimal(19,8) NOT NULL ); GO SELECT T.SomeValue FROM dbo.Test AS T WHERE T.SomeValue >= 987.65432 AND T.SomeValue < 123456.789;
To zapytanie kwalifikuje się do prostej parametryzacji . SQL Server wybiera najmniejszą precyzję i skalę dla parametrów, które mogą zawierać podane wartości. Oznacza to, że wybiera numeric(8,5)
dla 987.65432
i numeric(9,3)
dla 123456.789
:
Wnioskowane typy danych liczbowych
Te wywnioskowane typy nie pasują do decimal(19,8)
typ kolumny, więc w planie wykonania pojawia się konwersja wokół parametru:
Konwersja na typ kolumny
Te konwersje reprezentują tylko niewielką nieefektywność w czasie wykonywania w tym konkretnym przypadku. W innych sytuacjach niezgodność między typem danych kolumny a wywnioskowanym typem parametru może uniemożliwić wyszukiwanie indeksu lub wymagać od SQL Server wykonania dodatkowej pracy w celu wytworzenia dynamicznego wyszukiwania.
Nawet jeśli wynikowy plan wykonania wydaje się rozsądny, niezgodność typu może łatwo wpłynąć na jakość planu ze względu na wpływ niezgodności typu na oszacowanie kardynalności. Zawsze najlepiej jest używać pasujących typów danych i zwracać szczególną uwagę na typy pochodne wynikające z wyrażeń.
Ponowne wykorzystanie planu
Głównym problemem z bieżącym planem są określone typy wywnioskowane wpływające na dopasowanie planu w pamięci podręcznej, a zatem ponowne użycie. Uruchommy jeszcze kilka zapytań o tej samej formie ogólnej:
SELECT T.SomeValue FROM dbo.Test AS T WHERE T.SomeValue >= 98.76 AND T.SomeValue < 123.4567; GO SELECT T.SomeValue FROM dbo.Test AS T WHERE T.SomeValue >= 1.2 AND T.SomeValue < 1234.56789; GO
Teraz spójrz na pamięć podręczną planów:
SELECT CP.usecounts, CP.objtype, ST.[text] FROM sys.dm_exec_cached_plans AS CP CROSS APPLY sys.dm_exec_sql_text (CP.plan_handle) AS ST WHERE ST.[text] NOT LIKE '%dm_exec_cached_plans%' AND ST.[text] LIKE '%SomeValue%Test%' ORDER BY CP.objtype ASC;
Pokazuje AdHoc i Przygotowane oświadczenie dla każdego przesłanego zapytania:
Oddzielne przygotowane wyciągi
Sparametryzowany tekst jest taki sam, ale typy danych parametrów są różne, więc oddzielne plany są buforowane i nie następuje ponowne użycie planu.
Jeśli nadal będziemy przesyłać zapytania z różnymi kombinacjami skali lub precyzji, nowy Przygotowany plan będzie tworzony i buforowany za każdym razem. Pamiętaj, że wywnioskowany typ każdego parametru nie jest ograniczony przez typ danych kolumny, więc możemy skończyć z ogromną liczbą buforowanych planów, w zależności od przesłanych literałów liczbowych. Liczba kombinacji z numeric(1,0)
na numeric(38,38)
jest już duża, zanim pomyślimy o wielu parametrach.
Jasna parametryzacja
Ten problem nie pojawia się, gdy używamy jawnej parametryzacji, najlepiej wybierając ten sam typ danych, co kolumna, z którą porównywany jest parametr:
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; GO DECLARE @stmt nvarchar(4000) = N'SELECT T.SomeValue FROM dbo.Test AS T WHERE T.SomeValue >= @P1 AND T.SomeValue < @P2;', @params nvarchar(4000) = N'@P1 numeric(19,8), @P2 numeric(19,8)'; EXECUTE sys.sp_executesql @stmt, @params, @P1 = 987.65432, @P2 = 123456.789; EXECUTE sys.sp_executesql @stmt, @params, @P1 = 98.76, @P2 = 123.4567; EXECUTE sys.sp_executesql @stmt, @params, @P1 = 1.2, @P2 = 1234.56789;
W przypadku jawnej parametryzacji zapytanie dotyczące pamięci podręcznej planu pokazuje tylko jeden plan w pamięci podręcznej, używany trzy razy i nie wymaga konwersji typu:
Jawna parametryzacja
Jako ostatnia uwaga, użyłem decimal
i numeric
zamiennie w tej sekcji. Są technicznie różne typy, choć udokumentowane jako synonimy i zachowujące się równoważnie. Zwykle tak jest, ale nie zawsze:
-- Raises error 8120: -- Column 'dbo.Test.SomeValue' is invalid in the select list -- because it is not contained in either an aggregate function -- or the GROUP BY clause. SELECT CONVERT(decimal(19,8), T.SomeValue) FROM dbo.Test AS T GROUP BY CONVERT(numeric(19,8), T.SomeValue);
Prawdopodobnie jest to mały błąd parsera, ale nadal opłaca się być spójnym (chyba że piszesz artykuł i chcesz wskazać interesujący wyjątek).
Operatory arytmetyczne
Jest jeszcze jeden przypadek brzegowy, którym chcę się zająć, na podstawie przykładu podanego w dokumentacji, ale bardziej szczegółowo (i być może z dokładnością):
-- The dbo.LinkTypes table contains two rows -- Uses simple parameterization SELECT r = CONVERT(float, 1./ 7) FROM dbo.LinkTypes AS LT; -- No simple parameterization due to -- constant-constant comparison SELECT r = CONVERT(float, 1./ 7) FROM dbo.LinkTypes AS LT WHERE 1 = 1;
Wyniki są różne, zgodnie z dokumentacją:
Różne wyniki
Z prostą parametryzacją
Gdy prosta parametryzacja wystąpi, SQL Server sparametryzuje obie wartości literalne. 1.
wartość jest wpisywana jako numeric(1,0)
zgodnie z oczekiwaniami. Nieco niespójnie, 7
jest wpisywany jako integer
(nie tinyint
). Reguły wnioskowania o typach były budowane z biegiem czasu przez różne zespoły. Zachowania są utrzymywane, aby uniknąć złamania starszego kodu.
Następny krok obejmuje /
operator arytmetyczny. SQL Server wymaga zgodnych typów przed wykonaniem podziału. Podano numeric
(decimal
) ma wyższy priorytet typu danych niż integer
, integer
zostanie przekonwertowany na numeric
.
SQL Server musi niejawnie przekonwertować integer
do numeric
. Ale jakiej precyzji i skali użyć? Odpowiedź może być oparta na oryginalnym literale, tak jak SQL Server w innych okolicznościach, ale zawsze używa numeric(10)
tutaj.
Typ danych wyniku dzielenia numeric(1,0)
za pomocą numeric(10,0)
jest określany przez inny zestaw reguł, podanych w dokumentacji, dotyczących dokładności, skali i długości. Wstawiając liczby do podanych tam wzorów na dokładność wyników i skalę, otrzymujemy:
- Dokładność wyniku:
- p1 – s1 + s2 + max(6, s1 + p2 + 1)
- =1 – 0 + 0 + maks(6, 0 + 10 + 1)
- =1 + maks.(6, 11)
- =1 + 11
- =12
- Skala wyniku:
- maks(6, s1 + p2 + 1)
- =maks(6, 0 + 10 + 1)
- =maks(6, 11)
- =11
Typ danych 1. / 7
jest zatem numeric(12, 11)
. Ta wartość jest następnie konwertowana na float
na żądanie i wyświetlane jako 0.14285714285
(z 11 cyframi po przecinku).
Bez prostej parametryzacji
Gdy prosta parametryzacja nie jest wykonywana, 1.
literał jest wpisywany jako numeric(1,0)
jak wcześniej. 7
jest początkowo wpisywany jako integer
także jak widzieliśmy wcześniej. Kluczową różnicą jest integer
jest konwertowany na numeric(1,0)
, więc operator dzielenia ma wspólne typy do pracy. Jest to najmniejsza precyzja i skala, która może zawierać wartość 7
. Pamiętaj, że użyto prostej parametryzacji numeric(10,0)
tutaj.
Formuły precyzji i skali do dzielenia numeric(1,0)
przez numeric(1,0)
podaj typ danych wyniku numeric(7,6)
:
- Dokładność wyniku:
- p1 – s1 + s2 + max(6, s1 + p2 + 1)
- =1 – 0 + 0 + maks(6, 0 + 1 + 1)
- =1 + maks.(6, 2)
- =1 + 6
- =7
- Skala wyniku:
- maks(6, s1 + p2 + 1)
- =maks(6, 0 + 1 + 1)
- =maks(6, 2)
- =6
Po ostatecznej konwersji do float
, wyświetlany wynik to 0.142857
(sześć cyfr po przecinku).
Zaobserwowana różnica w wynikach jest zatem spowodowana wyprowadzeniem typu tymczasowego (numeric(12,11)
w porównaniu z numeric(7,6)
) zamiast ostatecznej konwersji do float
.
Jeśli potrzebujesz dalszych dowodów, konwersja do float
nie jest odpowiedzialny, rozważ:
-- Simple parameterization SELECT r = CONVERT(decimal(13,12), 1. / 7) FROM dbo.LinkTypes AS LT; -- No simple parameterization SELECT r = CONVERT(decimal(13,12), 1. / 7) FROM dbo.LinkTypes AS LT OPTION (MAXDOP 1);
Wynik z liczbą dziesiętną
Wyniki różnią się wartością i skalą jak poprzednio.
Ta sekcja nie obejmuje wszystkich dziwactw wnioskowania o typach danych i konwersji z prostą parametryzacją dowolnymi środkami. Jak wspomniano wcześniej, lepiej jest używać jawnych parametrów ze znanymi typami danych, gdy tylko jest to możliwe.
Koniec części 2
W następnej części tej serii opisano, jak prosta parametryzacja wpływa na plany wykonania.