Типове данни за параметри
Както беше споменато в първата част на тази серия, една от причините да е по-добре да се параметризира изрично е, за да имате пълен контрол върху типовете данни за параметри. Простата параметризация има редица странности в тази област, които могат да доведат до кеширане на повече параметризирани планове от очакваното или до намиране на различни резултати в сравнение с непараметризираната версия.
Когато SQL Server прилага проста параметризация към ad-hoc изявление, той прави предположение за типа данни на параметъра за заместване. Ще разгледам причините за предположението по-късно в поредицата.
За момента нека разгледаме някои примери, използващи базата данни Stack Overflow 2010 на SQL Server 2019 CU 14. Съвместимостта на базата данни е настроена на 150, а прагът на разходите за паралелизъм е настроен на 50, за да се избегне паралелизъм засега:
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;
Тези изявления водят до шест кеширани плана, три Adhoc и три Подготвени :
Различни предполагаеми типове
Обърнете внимание на различните типове данни за параметри в Подготвени планове.
Извод за тип данни
Подробностите за това как се отгатва всеки тип данни са сложни и непълно документирани. Като отправна точка SQL Server извежда основен тип от текстовото представяне на стойността, след което използва най-малкия съвместим подтип.
За низ от числа без кавички или десетична запетая, SQL Server избира от tinyint
, smallint
и integer
. За такива числа извън обхвата на integer
, SQL Server използва numeric
с възможно най-малка прецизност. Например числото 2,147,483,648 се въвежда като numeric(10,0)
. bigint
типът не се използва за параметризиране от страна на сървъра. Този параграф обяснява типовете данни, избрани в предишните примери.
Низове от числа с десетичната запетая се интерпретира като numeric
, с точност и мащаб, достатъчно голям, за да съдържа предоставената стойност. Низовете с префикс с валутен символ се интерпретират като money
. Низовете в научна нотация се превеждат на float
. smallmoney
и real
типове не се използват.
datetime
и uniqueidentifer
типове не могат да бъдат изведени от естествените низови формати. За да получите datetime
или uniqueidentifier
тип параметър, стойността на литерала трябва да бъде предоставена в ODBC escape формат. Например {d '1901-01-01'}
, {ts '1900-01-01 12:34:56.790'}
, или {guid 'F85C72AB-15F7-49E9-A949-273C55A6C393'}
. В противен случай предвидената дата или UUID литерал се въвежда като низ. Типове дата и час, различни от datetime
не се използват.
Общият низ и двоичните литерали се въвеждат като varchar(8000)
, nvarchar(4000)
, или varbinary(8000)
според случая, освен ако литералът не надвишава 8000 байта, в който случай max
се използва вариант. Тази схема помага да се избегне замърсяването на кеша и ниското ниво на повторна употреба, които биха били резултат от използването на специфични дължини.
Не е възможно да се използва CAST
или CONVERT
за да зададете типа данни за параметри по причини, които ще обясня подробно по-късно в тази серия. Има пример за това в следващия раздел.
Няма да разглеждам принудителното параметризиране в тази серия, но искам да спомена правилата за извеждане на тип данни в този случай имат някои важни разлики в сравнение с простата параметризация . Принудителното параметризиране не беше добавено до SQL Server 2005, така че Microsoft имаше възможността да включи някои уроци от простата параметризация опит и не трябваше да се притеснявате много за проблеми с обратната съвместимост.
Числови типове
За числа с десетична запетая и цели числа извън обхвата на integer
, правилата за изведен тип представляват специални проблеми за повторно използване на планове и замърсяване на кеша.
Помислете за следната заявка с десетични знаци:
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;
Тази заявка отговаря на условията за проста параметризация . SQL Server избира най-малката точност и мащаб за параметрите, които могат да съдържат предоставените стойности. Това означава, че избира numeric(8,5)
за 987.65432
и numeric(9,3)
за 123456.789
:
Изведени числови типове данни
Тези изведени типове не съответстват на decimal(19,8)
тип на колоната, така че в плана за изпълнение се появява преобразуване около параметъра:
Преобразуване в тип колона
Тези преобразувания представляват само малка неефективност по време на изпълнение в този конкретен случай. В други ситуации несъответствието между типа данни на колоната и изведения тип на параметър може да попречи на търсене на индекс или да наложи SQL Server да извърши допълнителна работа за създаване на динамично търсене.
Дори когато полученият план за изпълнение изглежда разумен, несъответствието на типа може лесно да повлияе на качеството на плана поради ефекта на несъответствието на типа върху оценката на мощността. Винаги е най-добре да използвате съвпадащи типове данни и да обръщате внимание на извлечените типове, произтичащи от изрази.
Планирайте повторно използване
Основният проблем с текущия план са специфичните изведени типове, засягащи съвпадението на кеширания план и следователно повторното им използване. Нека изпълним още няколко заявки от същата обща форма:
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
Сега погледнете кеша на плана:
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;
Показва AdHoc и Подготвени изявление за всяка заявка, която изпратихме:
Отделни подготвени изявления
Параметризираният текст е същият, но типовете данни за параметрите са различни, така че отделните планове се кешират и не се използва повторно планиране.
Ако продължим да изпращаме заявки с различни комбинации от мащаб или прецизност, нов Подготвен планът ще бъде създаден и кеширан всеки път. Не забравяйте, че изведеният тип на всеки параметър не е ограничен от типа данни на колоната, така че можем да получим огромен брой кеширани планове, в зависимост от представените числови литерали. Броят на комбинациите от numeric(1,0)
към numeric(38,38)
е вече голям, преди да помислим за множество параметри.
Изрична параметризация
Този проблем не възниква, когато използваме изрично параметризиране, като в идеалния случай избираме същия тип данни като колоната, с която се сравнява параметърът:
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;
С изрична параметризация, заявката за кеширане на плана показва само един кеширан план, използван три пъти и не са необходими преобразувания на типове:
Изрична параметризация
Като последна странична бележка използвах decimal
и numeric
взаимозаменяеми в този раздел. Те са технически различни типове, въпреки че е документирано, че са синоними и се държат еквивалентно. Това обикновено е така, но не винаги:
-- 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);
Вероятно това е малка грешка в анализатора, но все пак си струва да бъдете последователни (освен ако не пишете статия и искате да посочите интересно изключение).
Аритметични оператори
Има още един краен случай, който искам да разгледам въз основа на пример, даден в документацията, но малко по-подробно (и може би точност):
-- 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;
Резултатите са различни, както е документирано:
Различни резултати
С проста параметризация
Когато просто параметризиране възниква, SQL Server параметризира и двете литерални стойности. 1.
стойността се въвежда като numeric(1,0)
както се очаква. Донякъде непоследователно, 7
се въвежда като integer
(не tinyint
). Правилата за извод на типа са изградени с течение на времето от различни екипи. Поведението се поддържа, за да се избегне нарушаване на наследения код.
Следващата стъпка включва /
аритметичен оператор. SQL Server изисква съвместими типове преди да извърши разделянето. Даден е numeric
(decimal
) има по-висок приоритет на типа данни от integer
, integer
ще бъде преобразуван в numeric
.
SQL Server трябва имплицитно да преобразува integer
към numeric
. Но каква прецизност и мащаб да използвате? Отговорът може да се основава на оригиналния литерал, както прави SQL Server при други обстоятелства, но винаги използва numeric(10)
тук.
Типът данни на резултата от разделянето на numeric(1,0)
чрез numeric(10,0)
се определя от друг набор от правила, дадени в документацията за прецизност, мащаб и дължина. Вмъквайки числата във формулите за прецизност на резултата и мащаб, дадени там, имаме:
- Прецизност на резултата:
- p1 – s1 + s2 + max(6, s1 + p2 + 1)
- =1 – 0 + 0 + макс(6, 0 + 10 + 1)
- =1 + макс(6, 11)
- =1 + 11
- =12
- Скала на резултата:
- max(6, s1 + p2 + 1)
- =макс(6, 0 + 10 + 1)
- =макс(6, 11)
- =11
Типът данни на 1. / 7
следователно е numeric(12, 11)
. След това тази стойност се преобразува в float
както е поискано и се показва като 0.14285714285
(с 11 цифри след десетичната запетая).
Без проста параметризация
Когато простата параметризация не е извършена, 1.
литералът се въвежда като numeric(1,0)
по старому. 7
първоначално се въвежда като integer
също както се вижда по-рано. Основната разлика е integer
се преобразува в numeric(1,0)
, така че операторът на разделяне има общи типове, с които да работи. Това е най-малката точност и мащаб, който може да съдържа стойността 7
. Запомнете простата параметризация, използвана numeric(10,0)
тук.
Формулите за прецизност и мащаб за разделяне на numeric(1,0)
от numeric(1,0)
дайте резултатен тип данни numeric(7,6)
:
- Прецизност на резултата:
- p1 – s1 + s2 + max(6, s1 + p2 + 1)
- =1 – 0 + 0 + макс(6, 0 + 1 + 1)
- =1 + макс(6, 2)
- =1 + 6
- =7
- Скала на резултата:
- max(6, s1 + p2 + 1)
- =макс(6, 0 + 1 + 1)
- =макс(6, 2)
- =6
След окончателното преобразуване в float
, показаният резултат е 0.142857
(с шест цифри след десетичната запетая).
Следователно наблюдаваната разлика в резултатите се дължи на извличане на междинен тип (numeric(12,11)
спрямо numeric(7,6)
) вместо крайното преобразуване в float
.
Ако имате нужда от допълнителни доказателства, преобразуването в float
не носи отговорност, имайте предвид:
-- 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);
Резултат с десетичен знак
Резултатите се различават по стойност и мащаб, както преди.
Този раздел не обхваща всяка странност на извеждането и преобразуването на тип данни с просто параметризиране по всякакъв начин. Както беше казано по-рано, по-добре е да използвате изрични параметри с известни типове данни, където е възможно.
Край на част 2
Следващата част от тази серия описва как проста параметризация засяга плановете за изпълнение.