Database
 sql >> база данни >  >> RDS >> Database

Решения за предизвикателство за генератор на числови серии – част 3

Това е третата част от поредица за решения на предизвикателството за генериране на числа. В част 1 разгледах решения, които генерират редовете в движение. В част 2 разгледах решения, които отправят заявка към физическа базова таблица, която предварително попълвате с редове. Този месец ще се съсредоточа върху една завладяваща техника, която може да се използва за справяне с нашето предизвикателство, но която също има интересни приложения далеч отвъд него. Не знам официално име на техниката, но тя е донякъде подобна по концепция на елиминирането на хоризонтални дялове, така че ще я наричам неофициално като хоризонтално елиминиране на единици техника. Техниката може да има интересни положителни ползи за производителността, но има и предупреждения, които трябва да сте наясно, когато при определени условия може да доведе до неустойка за ефективност.

Благодаря отново на Алън Бърщайн, Джо Оббиш, Адам Мачаник, Кристофър Форд, Джеф Модън, Чарли, НоамГр, Камил Косно, Дейв Мейсън, Джон Нелсън #2, Ед Вагнер, Майкъл Бърбеа и Пол Уайт за споделянето на вашите идеи и коментари.

Ще направя тестовете си в tempdb, като активирам статистика за времето:

SET NOCOUNT ON;
 
USE tempdb;
 
SET STATISTICS TIME ON;

По-ранни идеи

Техниката за хоризонтално елиминиране на единици може да се използва като алтернатива на логиката за елиминиране на колони или елиминиране на вертикални единици техника, на която разчитах в няколко от решенията, които разгледах по-рано. Можете да прочетете за основите на логиката за елиминиране на колони с таблични изрази в Основи на табличните изрази, част 3 – Производни таблици, съображения за оптимизация под „Проекция на колона и дума за SELECT *.

Основната идея на техниката за елиминиране на вертикални единици е, че ако имате израз на вложена таблица, който връща колони x и y и вашата външна заявка се позовава само на колона x, процесът на компилиране на заявката елиминира y от първоначалното дърво на заявката и следователно плана няма нужда да го оценява. Това има няколко положителни последици, свързани с оптимизацията, като например постигане на покритие на индекса само с x и ако y е резултат от изчисление, изобщо не е необходимо да се оценява основният израз на y. Тази идея беше в основата на решението на Алън Бърщайн. Разчитах на него и в няколко от другите решения, които обхванах, като например с функцията dbo.GetNumsAlanCharlieItzikBatch (от част 1), функциите dbo.GetNumsJohn2DaveObbishAlanCharlieItzik и dbo.GetNumsJohnAlanCharlieItzikBatch (от част 1), функциите dbo.GetNumsJohn2DaveObbishAlanCharlieItzik и dbo.GetNumsJohnAlanCharlieItzik и други, Part. Като пример ще използвам dbo.GetNumsAlanCharlieItzikBatch като базово решение с логиката за вертикално елиминиране.

Като напомняне, това решение използва присъединяване с фиктивна таблица, която има индекс на columnstore, за да получи пакетна обработка. Ето кода за създаване на фиктивната таблица:

DROP TABLE IF EXISTS dbo.BatchMe;
GO
 
CREATE TABLE dbo.BatchMe(col1 INT NOT NULL, INDEX idx_cs CLUSTERED COLUMNSTORE);

А ето и кода с дефиницията на функцията dbo.GetNumsAlanCharlieItzikBatch:

CREATE OR ALTER FUNCTION dbo.GetNumsAlanCharlieItzikBatch(@low AS BIGINT = 1, @high AS BIGINT)
  RETURNS TABLE
AS
RETURN
  WITH
    L0 AS ( SELECT 1 AS c
            FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),
                        (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ),
    L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
    L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
    L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ),
    Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
              FROM L3 )
  SELECT TOP(@high - @low + 1)
     rownum AS rn,
     @high + 1 - rownum AS op,
     @low - 1 + rownum AS n
  FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0
  ORDER BY rownum;
GO

Използвах следния код, за да тествам производителността на функцията със 100M реда, връщайки колоната с изчисления резултат n (манипулация на резултата от функцията ROW_NUMBER), подредена по n:

DECLARE @n AS BIGINT;
 
SELECT @n = n FROM dbo.GetNumsAlanCharlieItzikBatch(1, 100000000) ORDER BY n OPTION(MAXDOP 1);

Ето статистическите данни за времето, които получих за този тест:

Процесорно време =9328 ms, изминало време =9330 ms.

Използвах следния код, за да тествам производителността на функцията със 100 милиона реда, връщайки колоната rn (директен, неманипулиран, резултат от функцията ROW_NUMBER), подредена от rn:

DECLARE @n AS BIGINT;
 
SELECT @n = rn FROM dbo.GetNumsAlanCharlieItzikBatch(1, 100000000) ORDER BY rn OPTION(MAXDOP 1);

Ето статистическите данни за времето, които получих за този тест:

Процесорно време =7296 ms, изминало време =7291 ms.

Нека прегледаме важните идеи, които са вградени в това решение.

Разчитайки на логиката за елиминиране на колони, на Алън хрумва идеята да върне не само една колона с числови серии, а три:

  • Колона rn представлява неманипулиран резултат от функцията ROW_NUMBER, която започва с 1. Изчисляването е евтино. Това е запазване на реда както когато предоставяте константи, така и когато предоставяте неконстанти (променливи, колони) като входни данни за функцията. Това означава, че когато външната ви заявка използва ORDER BY rn, вие не получавате оператор за сортиране в плана.
  • Колона n представлява изчисление, базирано на @low, константа и rownum (резултат от функцията ROW_NUMBER). Това е запазване на реда по отношение на rownum, когато предоставяте константи като входни данни за функцията. Това е благодарение на прозрението на Чарли относно постоянното сгъване (вижте част 1 за подробности). Въпреки това, това не е запазване на реда, когато предоставяте неконстанти като входни данни, тъй като не получавате постоянно сгъване. Ще демонстрирам това по-късно в раздела за предупрежденията.
  • Колона op представлява n в обратен ред. Това е резултат от изчисление и не е запазване на реда.

Разчитайки на логиката за елиминиране на колони, ако трябва да върнете числови серии, започващи с 1, вие заявявате колона rn, което е по-евтино от заявката n. Ако имате нужда от серия от числа, започваща със стойност, различна от 1, вие заявявате n и заплащате допълнителната цена. Ако имате нужда от резултата, подреден по колоната с числа, с константи като входни данни, можете да използвате ORDER BY rn или ORDER BY n. Но с неконстанти като входни данни, вие искате да сте сигурни, че използвате ORDER BY rn. Може да е добра идея винаги да се придържате към използването на ORDER BY rn, когато се нуждаете от поръчания резултат, за да бъде сигурен.

Идеята за елиминиране на хоризонтални единици е подобна на идеята за елиминиране на вертикални единици, само че се прилага за набори от редове вместо набори от колони. Всъщност Джо Оббиш разчита на тази идея в своята функция dbo.GetNumsObbish (от част 2) и ние ще я направим още една стъпка. В своето решение Джо обединява множество заявки, представляващи несвързани поддиапазони от числа, като използва филтър в клаузата WHERE на всяка заявка, за да определи приложимостта на поддиапазон. Когато извиквате функцията и предавате постоянни входове, представляващи ограничителите на желания от вас диапазон, SQL Server елиминира неприложимите заявки по време на компилиране, така че планът дори не ги отразява.

Хоризонтално елиминиране на единици, време за компилиране спрямо време за изпълнение

Може би би било добра идея да започнем с демонстриране на концепцията за хоризонтално елиминиране на единици в по-общ случай и също така да обсъдим важно разграничение между елиминирането по време на компилиране и елиминиране по време на изпълнение. След това можем да обсъдим как да приложим идеята към нашето предизвикателство за серия от числа.

Ще използвам три таблици, наречени dbo.T1, dbo.T2 и dbo.T3 в моя пример. Използвайте следния DDL и DML код, за да създадете и попълните тези таблици:

DROP TABLE IF EXISTS dbo.T1, dbo.T2, dbo.T3;
GO
 
CREATE TABLE dbo.T1(col1 INT); INSERT INTO dbo.T1(col1) VALUES(1);
CREATE TABLE dbo.T2(col1 INT); INSERT INTO dbo.T2(col1) VALUES(2);
CREATE TABLE dbo.T3(col1 INT); INSERT INTO dbo.T3(col1) VALUES(3);

Да предположим, че искате да внедрите вграден TVF, наречен dbo.OneTable, който приема едно от горните три имена на таблици като вход и връща данните от исканата таблица. Въз основа на концепцията за хоризонтално елиминиране на единици, можете да приложите функцията така:

CREATE OR ALTER FUNCTION dbo.OneTable(@WhichTable AS NVARCHAR(257))
  RETURNS TABLE
AS
RETURN
  SELECT col1 FROM dbo.T1 WHERE @WhichTable = N'dbo.T1'
  UNION ALL
  SELECT col1 FROM dbo.T2 WHERE @WhichTable = N'dbo.T2'
  UNION ALL
  SELECT col1 FROM dbo.T3 WHERE @WhichTable = N'dbo.T3';
GO

Не забравяйте, че вграденият TVF прилага вграждане на параметри. Това означава, че когато подадете константа като N'dbo.T2' като вход, процесът на вграждане замества всички препратки към @WhichTable с константата преди оптимизация . След това процесът на елиминиране може да премахне препратките към T1 и T3 от първоначалното дърво на заявката и по този начин оптимизацията на заявката води до план, който препраща само към T2. Нека тестваме тази идея със следната заявка:

SELECT * FROM dbo.OneTable(N'dbo.T2');

Планът за тази заявка е показан на фигура 1.

Фигура 1:План за dbo.OneTable с постоянно въвеждане

Както можете да видите, в плана се появява само таблица T2.

Нещата са малко по-трудни, когато подавате неконстанта като вход. Това може да се случи при използване на променлива, параметър на процедура или предаване на колона чрез APPLY. Входната стойност е или неизвестна по време на компилиране, или трябва да се вземе предвид потенциалът за повторно използване на параметризиран план.

Оптимизаторът не може да премахне нито една от таблиците от плана, но все пак има трик. Той може да използва оператори за стартиране на филтър над поддърветата, които имат достъп до таблиците, и да изпълнява само съответното поддърво въз основа на стойността по време на изпълнение на @WhichTable. Използвайте следния код, за да тествате тази стратегия:

DECLARE @T AS NVARCHAR(257) = N'dbo.T2';
 
SELECT * FROM dbo.OneTable(@T);

Планът за това изпълнение е показан на Фигура 2:

Фигура 2:План за dbo.OneTable с непостоянен вход

Plan Explorer прави чудесно очевидно да се види, че е изпълнено само приложимото поддърво (Изпълнения =1) и оцветява поддърветата, които не са били изпълнени (Изпълнения =0). Също така, STATISTICS IO показва I/O информация само за таблицата, до която е бил достъпен:

Таблица „T2“. Брой на сканиране 1, логическо четене 1, физическо четене 0, сървър на страницата чете 0, четене напред 0, сървър за страница чете напред 0, лобно логическо четене 0, физическо четене на лоб 0, сървър на лоб страница чете 0, лобно четене- напред чете 0, сървърът на лоб страница чете напред чете 0.

Прилагане на логиката за елиминиране на хоризонтални единици към предизвикателството на серия от числа

Както споменахме, можете да приложите концепцията за хоризонтално елиминиране на единици, като модифицирате някое от по-ранните решения, които в момента използват логика за вертикално елиминиране. Ще използвам функцията dbo.GetNumsAlanCharlieItzikBatch като отправна точка за моя пример.

Припомнете си, че Джо Оббиш използва елиминиране на хоризонтални единици, за да извлече съответните непреходни поддиапазони на числовите серии. Ще използваме концепцията, за да разделим хоризонтално по-евтиното изчисление (rn), където @low =1 от по-скъпото изчисление (n), където @low <> 1.

Докато сме на това, можем да експериментираме, като добавим идеята на Джеф Модън в неговата функция fnTally, където той използва ред за наблюдение със стойност 0 за случаите, когато диапазонът започва с @low =0.

Така че имаме четири хоризонтални единици:

  • Сентинен ред с 0, където @low =0, с n =0
  • TOP (@high) редове, където @low =0, с евтин n =rownum и op =@high – rownum
  • TOP (@high) редове, където @low =1, с евтин n =rownum и op =@high + 1 – rownum
  • TOP(@high – @low + 1) редове, където @low <> 0 И @low <> 1, с по-скъпо n =@low – 1 + rownum и op =@high + 1 – rownum

Това решение съчетава идеи от Алън, Чарли, Джо, Джеф и мен, така че ще наречем пакетната версия на функцията dbo.GetNumsAlanCharlieJoeJeffItzikBatch.

Първо, не забравяйте да се уверите, че все още имате фиктивната таблица dbo.BatchMe, за да получите пакетна обработка в нашето решение, или използвайте следния код, ако не го направите:

DROP TABLE IF EXISTS dbo.BatchMe;
GO
 
CREATE TABLE dbo.BatchMe(col1 INT NOT NULL, INDEX idx_cs CLUSTERED COLUMNSTORE);

Ето кода с дефиницията на функцията dbo.GetNumsAlanCharlieJoeJeffItzikBatch:

CREATE OR ALTER FUNCTION dbo.GetNumsAlanCharlieJoeJeffItzikBatch(@low AS BIGINT = 1, @high AS BIGINT)
  RETURNS TABLE
AS
RETURN
  WITH
    L0 AS ( SELECT 1 AS c
            FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),
                        (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ),
    L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
    L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
    L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ),
    Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
              FROM L3 )
  SELECT @low AS n, @high AS op WHERE @low = 0 AND @high > @low
  UNION ALL
  SELECT TOP(@high)
     rownum AS n,
     @high - rownum AS op
  FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0
  WHERE @low = 0
  ORDER BY rownum
  UNION ALL
  SELECT TOP(@high)
     rownum AS n,
     @high + 1 - rownum AS op
  FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0
  WHERE @low = 1
  ORDER BY rownum
  UNION ALL
  SELECT TOP(@high - @low + 1)
     @low - 1 + rownum AS n,
     @high + 1 - rownum AS op
  FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0
  WHERE @low <> 0 AND @low <> 1
  ORDER BY rownum;
GO

Важно:Концепцията за хоризонтално елиминиране на единици несъмнено е по-сложна за изпълнение от вертикалната, така че защо да се притеснявате? Защото премахва отговорността за избор на правилната колона от потребителя. Потребителят трябва да се тревожи само за запитване на колона, наречена n, вместо да помни да използва rn, когато диапазонът започва с 1, и n в противен случай.

Нека започнем с тестване на решението с постоянни входове 1 и 100 000 000, като поискаме резултатът да бъде подреден:

DECLARE @n AS BIGINT;
 
SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeJeffItzikBatch(1, 100000000) ORDER BY n OPTION(MAXDOP 1);

Планът за това изпълнение е показан на Фигура 3.

Фигура 3:План за dbo.GetNumsAlanCharlieJoeJeffItzik>01, 01, 10

Обърнете внимание, че единствената върната колона се основава на директния, неманипулиран израз ROW_NUMBER (Expr1313). Също така имайте предвид, че няма нужда от сортиране в плана.

Получих следната статистика за времето за това изпълнение:

Процесорно време =7359 ms, изминало време =7354 ms.

Времето за изпълнение отразява адекватно факта, че планът използва пакетен режим, неманипулирания израз ROW_NUMBER и няма сортиране.

След това тествайте функцията с константния диапазон от 0 до 99 999 999:

DECLARE @n AS BIGINT;
 
SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeJeffItzikBatch(0, 99999999) ORDER BY n OPTION(MAXDOP 1);

Планът за това изпълнение е показан на Фигура 4.

Фигура 4:План за dbo.GetNumsAlanCharlieJoeJeffItzik9)

Планът използва Merge Join (Concatenation) оператор за сливане на реда за наблюдение със стойността 0 и останалата част. Въпреки че втората част е толкова ефективна, колкото и преди, логиката на сливането отнема доста голяма тежест от около 26% върху времето за изпълнение, което води до следните статистически данни за времето:

Процесорно време =9265 ms, изминало време =9298 ms.

Нека тестваме функцията с константен диапазон от 2 до 100 000 001:

DECLARE @n AS BIGINT;
 
SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeJeffItzikBatch(2, 100000001) ORDER BY n OPTION(MAXDOP 1);

Планът за това изпълнение е показан на Фигура 5.

Фигура 5:План за dbo.GetNumsAlanCharlieJoeJeffItzikBatch(2,0000)

Този път няма скъпа логика за сливане, тъй като частта за контролния ред е без значение. Обърнете внимание обаче, че върнатата колона е манипулираният израз @low – 1 + rownum, който след вграждане/вграждане на параметри и постоянно сгъване става 1 + rownum.

Ето статистическите данни за времето, които получих за това изпълнение:

Време на процесора =9000 ms, изминало време =9015 ms.

Както се очаква, това не е толкова бързо, колкото при диапазон, който започва с 1, но е интересно, по-бързо, отколкото при диапазон, който започва с 0.

Премахване на 0 сигнален ред

Като се има предвид, че техниката със сентинелния ред със стойност 0 изглежда по-бавна от прилагането на манипулация към rownum, има смисъл просто да се избягва. Това ни води до опростено решение, базирано на хоризонтално елиминиране, което смесва идеите на Алън, Чарли, Джо и мен. Ще извикам функцията с това решение dbo.GetNumsAlanCharlieJoeItzikBatch. Ето дефиницията на функцията:

CREATE OR ALTER FUNCTION dbo.GetNumsAlanCharlieJoeItzikBatch(@low AS BIGINT = 1, @high AS BIGINT)
  RETURNS TABLE
AS
RETURN
  WITH
    L0 AS ( SELECT 1 AS c
            FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),
                        (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ),
    L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
    L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
    L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ),
    Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
              FROM L3 )
  SELECT TOP(@high)
     rownum AS n,
     @high + 1 - rownum AS op
  FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0
  WHERE @low = 1
  ORDER BY rownum
  UNION ALL
  SELECT TOP(@high - @low + 1)
     @low - 1 + rownum AS n,
     @high + 1 - rownum AS op
  FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0
  WHERE @low <> 1
  ORDER BY rownum;
GO

Нека го тестваме с диапазона от 1 до 100M:

DECLARE @n AS BIGINT;
 
SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(1, 100000000) ORDER BY n OPTION(MAXDOP 1);

Планът е същият като този, показан по-рано на фигура 3, както се очаква.

Съответно получих следната статистика за времето:

Процесорно време =7219 ms, изминало време =7243 ms.

Тествайте го с диапазона от 0 до 99 999 999:

DECLARE @n AS BIGINT;
 
SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(0, 99999999) ORDER BY n OPTION(MAXDOP 1);

Този път получавате същия план като този, показан по-рано на Фигура 5, а не на Фигура 4.

Ето статистическите данни за времето, които получих за това изпълнение:

Време на процесора =9313 ms, изминало време =9334 ms.

Тествайте го с диапазона от 2 до 100 000 001:

DECLARE @n AS BIGINT;
 
SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(2, 100000001) ORDER BY n OPTION(MAXDOP 1);

Отново получавате същия план като този, показан по-рано на Фигура 5.

Получих следната статистика за времето за това изпълнение:

Време на процесора =9125 ms, изминало време =9148 ms.

Предупреждения при използване на непостоянни входове

И с техниките за елиминиране на вертикални и хоризонтални единици нещата работят идеално, стига да предавате константи като входни данни. Въпреки това, трябва да сте наясно с предупрежденията, които могат да доведат до неустойки в производителността, когато предавате непостоянни входове. Техниката за елиминиране на вертикални единици има по-малко проблеми, а проблемите, които съществуват, са по-лесни за справяне, така че нека започнем с нея.

Не забравяйте, че в тази статия използвахме функцията dbo.GetNumsAlanCharlieItzikBatch като наш пример, който разчита на концепцията за елиминиране на вертикални единици. Нека изпълним серия от тестове с непостоянни входове, като променливи.

Като наш първи тест ще върнем rn и ще поискаме данните, подредени от rn:

DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 100000000;
 
DECLARE @n AS BIGINT;
 
SELECT @n = rn FROM dbo.GetNumsAlanCharlieItzikBatch(@mylow, @myhigh) ORDER BY rn OPTION(MAXDOP 1);

Не забравяйте, че rn представлява неманипулирания израз ROW_NUMBER, така че фактът, че използваме непостоянни входни данни, няма специално значение в този случай. Няма нужда от изрично сортиране в плана.

Получих следната статистика за времето за това изпълнение:

Време на процесора =7390 ms, изминало време =7386 ms.

Тези числа представляват идеалния случай.

В следващия тест подредете редовете с резултатите по n:

DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 100000000;
 
DECLARE @n AS BIGINT;
 
SELECT @n = n FROM dbo.GetNumsAlanCharlieItzikBatch(@mylow, @myhigh) ORDER BY n OPTION(MAXDOP 1);

Планът за това изпълнение е показан на фигура 6.

Фигура 6:План за dbo.GetNumsAlanCharlieItzikBatch(@mylow) поръчка, @myhigh н

Виждате ли проблема? След вграждането, @low беше заменен с @mylow — не със стойността в @mylow, която е 1. Следователно, постоянното сгъване не се осъществи и следователно n не е запазване на реда по отношение на rownum. Това доведе до изрично сортиране в плана.

Ето статистическите данни за времето, които получих за това изпълнение:

Време на процесора =25141 ms, изминало време =25628 ms.

Времето за изпълнение почти се е утроило в сравнение с времето, когато не е било необходимо изрично сортиране.

Лесно заобиколно решение е да използвате оригиналната идея на Алън Бърщайн да подреждате винаги по rn, когато имате нужда от подреден резултат, както при връщане на rn, така и при връщане на n, така:

DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 100000000;
 
DECLARE @n AS BIGINT;
 
SELECT @n = n FROM dbo.GetNumsAlanCharlieItzikBatch(@mylow, @myhigh) ORDER BY rn OPTION(MAXDOP 1);

Този път в плана няма изрично сортиране.

Получих следната статистика за времето за това изпълнение:

Време на процесора =9156 ms, изминало време =9184 ms.

Числата отразяват адекватно факта, че връщате манипулирания израз, но нямате изрично сортиране.

С решения, които се основават на техниката за елиминиране на хоризонтални единици, като нашата функция dbo.GetNumsAlanCharlieJoeItzikBatch, ситуацията е по-сложна при използване на непостоянни входове.

Нека първо тестваме функцията с много малък диапазон от 10 числа:

DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 10;
 
DECLARE @n AS BIGINT;
 
SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(@mylow, @myhigh) ORDER BY n OPTION(MAXDOP 1);

Планът за това изпълнение е показан на Фигура 7.

Фигура 7:План за dbo.GetNumsAlanCharlieJoeItzikBatch(@mylow) @mylow, em>

Този план има много тревожна страна. Обърнете внимание, че филтърните оператори се появяват по-долу най-добрите оператори! При всяко дадено извикване на функцията с непостоянни входове, естествено един от клоновете под оператора за конкатенация винаги ще има фалшиво условие за филтър. Въпреки това и двата оператора Top изискват ненулев брой редове. Така че операторът Top над оператора с фалшиво филтърно условие ще поиска редове и никога няма да бъде удовлетворен, тъй като филтърният оператор ще продължи да отхвърля всички редове, които ще получи от своя дъщерен възел. Работата в поддървото под оператора Filter ще трябва да завърши до завършване. В нашия случай това означава, че поддървото ще премине през работата по генериране на 4B реда, които операторът Filter ще изхвърли. Чудите се защо филтърният оператор се притеснява да изисква редове от своя дъщерен възел, но изглежда, че така работи в момента. Трудно е да се види това със статичен план. По-лесно е да видите това на живо, например с опцията за изпълнение на заявка на живо в SentryOne Plan Explorer, както е показано на фигура 8. Опитайте.

Фигура 8:Статистика на заявките на живо за dbo.GetNumsAlanCharlieJoeItzik, @myhhItzikBatch

Този тест отне 9:15 минути, за да завърши на моята машина, и не забравяйте, че заявката беше да върне диапазон от 10 числа.

Нека помислим дали има начин да избегнем активирането на неподходящото поддърво в неговата цялост. За да постигнете това, бихте искали операторите на филтъра при стартиране да се показват отгоре операторите Топ вместо под тях. Ако прочетете Основи на табличните изрази, част 4 – Извлечени таблици, съображения за оптимизация, продължение, знаете, че ТОП филтър предотвратява разместването на таблични изрази. И така, всичко, което трябва да направите, е да поставите ТОП заявката в производна таблица и да приложите филтъра във външна заявка спрямо получената таблица.

Ето нашата модифицирана функция, която прилага този трик:

CREATE OR ALTER FUNCTION dbo.GetNumsAlanCharlieJoeItzikBatch(@low AS BIGINT = 1, @high AS BIGINT)
  RETURNS TABLE
AS
RETURN
  WITH
    L0 AS ( SELECT 1 AS c
            FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),
                        (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ),
    L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
    L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
    L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ),
    Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
              FROM L3 )
  SELECT *
  FROM ( SELECT TOP(@high)
            rownum AS n,
            @high + 1 - rownum AS op
         FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0
         ORDER BY rownum ) AS D1
  WHERE @low = 1
  UNION ALL
  SELECT *
  FROM ( SELECT TOP(@high - @low + 1)
            @low - 1 + rownum AS n,
            @high + 1 - rownum AS op
         FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0
         ORDER BY rownum ) AS D2
  WHERE @low <> 1;
GO

Както се очакваше, изпълненията с константи продължават да се държат и изпълняват същото като без трика.

Що се отнася до непостоянните входове, сега с малки обхвати е много бързо. Ето тест с диапазон от 10 числа:

DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 10;
 
DECLARE @n AS BIGINT;
 
SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(@mylow, @myhigh) ORDER BY n OPTION(MAXDOP 1);

Планът за това изпълнение е показан на Фигура 9.

Фигура 9:План за подобрен dbo.GetNumsAlanCharlieJoeItzikBatch(@highlow) /em>

Забележете, че желаният ефект от поставянето на операторите Filter над операторите Top е постигнат. Въпреки това колоната за подреждане n се третира като резултат от манипулация и следователно не се счита за колона за запазване на реда по отношение на rownum. Следователно в плана има изрично сортиране.

Тествайте функцията с голям диапазон от 100 милиона числа:

DECLARE @mylow AS BIGINT = 1, @myhigh AS BIGINT = 100000000;
 
DECLARE @n AS BIGINT;
 
SELECT @n = n FROM dbo.GetNumsAlanCharlieJoeItzikBatch(@mylow, @myhigh) ORDER BY n OPTION(MAXDOP 1);

Получих следната статистика за времето:

CPU време =29907 ms, изминало време =29909 ms.

Каква глупост; беше почти перфектно!

Резюме и статистика за ефективността

Фигура 10 съдържа обобщение на статистическите данни за времето за различните решения.

Фигура 10:Резюме на времевата ефективност на решенията

И така, какво научихме от всичко това? Предполагам да не го правя отново! Просто се шегувам. Научихме, че е по-безопасно да използваме концепцията за вертикално елиминиране като в dbo.GetNumsAlanCharlieItzikBatch, която разкрива както неманипулирания резултат ROW_NUMBER (rn), така и манипулирания (n). Просто се уверете, че когато трябва да върнете поръчания резултат, винаги подреждайте по rn, независимо дали връщате rn или n.

Ако сте абсолютно сигурни, че вашето решение винаги ще се използва с константи като входни данни, можете да използвате концепцията за хоризонтално елиминиране на единици. Това ще доведе до по-интуитивно решение за потребителя, тъй като те ще взаимодействат с една колона за нарастващите стойности. Все пак бих предложил да използвате трика с извлечените таблици, за да предотвратите разместването и да поставите операторите Filter над операторите Top, ако функцията някога се използва с непостоянни входове, само за да бъдете в безопасност.

Все още не сме готови. Следващия месец ще продължа да проучвам допълнителни решения.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Как да гарантираме, че базите данни нямат фрагментирани индекси

  2. Въздействие върху производителността на различни техники за обработка на грешки

  3. Как да актуализирате колона въз основа на филтър на друга колона

  4. Разликата между първичен ключ и уникален ключ

  5. Открояване на удари в пълнотекстово търсене