В тази статия ще засегнем темата за производителността на променливите в таблицата. В SQL Server можем да създадем променливи, които ще работят като пълни таблици. Може би други бази данни имат същите възможности, но аз използвах такива променливи само в MS SQL Server.
По този начин можете да напишете следното:
declare @t as table (int value)
Тук ние декларираме променливата @t като таблица, която ще съдържа една колона Value от типа Integer. Възможно е да се създават по-сложни таблици, но в нашия пример една колона е достатъчна за изследване на оптимизацията.
Сега можем да използваме тази променлива в нашите заявки. Можем да добавим много данни към него и да извършим извличане на данни от тази променлива:
insert into @t select UserID from User or select * from @t
Забелязах, че променливите на таблицата се използват, когато е необходимо да се извлекат данни за голям избор. Например в кода има заявка, която връща потребителите на сайта. Сега събирате идентификатори на всички потребители, добавяте ги към променливата на таблицата и можете да търсите адреси за тези потребители. Може би някой може да попита защо не изпълним една заявка към базата данни и не получим всичко веднага? Имам прост пример.
Да приемем, че потребителите идват от уеб услугата, докато техните адреси се съхраняват във вашата база данни. В този случай няма изход. Получихме куп потребителски идентификатори от услугата и за да избегнем запитване към базата данни, някой решава, че е по-лесно да добави всички идентификатори към параметъра на заявката като променлива на таблица и заявката ще изглежда спретнато:
select * from @t as users join Address a on a.UserID = users.UserID os
Всичко това работи правилно. В кода на C# можете бързо да комбинирате резултатите от двата масива от данни в един обект с помощта на LINQ. Въпреки това, ефективността на заявката може да се влоши.
Факт е, че променливите в таблицата не са предназначени за обработка на големи обеми данни. Ако не се лъжа, оптимизаторът на заявки винаги ще използва метода за изпълнение LOOP. По този начин за всеки идентификатор от @t ще се извърши търсене в таблицата с адреси. Ако има 1000 записа в @t, сървърът ще сканира адреса 1000 пъти.
По отношение на изпълнението, поради безумния брой сканирания, сървърът просто престава да се опитва да намери данни.
Много по-ефективно е да сканирате цялата таблица с адреси и да намерите всички потребители наведнъж. Този метод се нарича MERGE. Въпреки това SQL Server го избира, когато има много сортирани данни. В този случай оптимизаторът не знае колко и какви данни ще бъдат добавени към променливата и дали има сортиране, тъй като такава променлива не включва индекси.
Ако има малко данни в променливата на таблицата и не вмъквате хиляди редове в нея, всичко е наред. Въпреки това, ако искате да използвате такива променливи и да добавяте огромно количество данни към тях, трябва да продължите да четете.
Дори ако замените променливата на таблицата с SQL, това значително ще ускори изпълнението на заявката:
select * from ( Select 10377 as UserID Union all Select 73736 Union all Select 7474748 …. ) as users join Address a on a.UserID = users.UserID
Може да има хиляди такива изрази SELECT и текстът на заявката ще бъде огромен, но ще се изпълнява хиляди пъти по-бързо за голям обем данни, тъй като SQL Server може да избере ефективен план за изпълнение.
Тази заявка не изглежда страхотно. Неговият план за изпълнение обаче не може да бъде кеширан, тъй като промяната само на един идентификатор ще промени и целия текст на заявката и параметрите не могат да се използват.
Мисля, че Microsoft не очакваха потребителите да използват таблични променливи по този начин, но има хубаво решение.
Има няколко начина за решаване на този проблем. Въпреки това, според мен, най-ефективното по отношение на производителността е да добавите OPTION (RECOMPILE) в края на заявката:
select * from @t as users join Address a on a.UserID = users.UserID OPTION (RECOMPILE)
Тази опция се добавя веднъж в самия край на заявката след дори ORDER BY. Целта на тази опция е да накара SQL Server да прекомпилира заявката при всяко изпълнение.
Ако след това измерим ефективността на заявката, времето най-вероятно ще бъде намалено за извършване на търсенето. При големи данни подобрението на производителността може да бъде значително, от десетки минути до секунди. Сега сървърът компилира кода си преди да изпълни всяка заявка и не използва плана за изпълнение от кеша, а генерира нов, в зависимост от количеството данни в променливата, и това обикновено помага много.
Недостатъкът е, че планът за изпълнение не се съхранява и сървърът трябва да компилира заявката и да търси ефективен план за изпълнение всеки път. Не съм виждал обаче заявките, при които този процес е отнел повече от 100 ms.
Лоша идея ли е да се използват променливи в таблицата? Не, не е. Само не забравяйте, че те не са създадени за големи данни. Понякога е по-добре да създадете временна таблица, ако има много данни, и да вмъкнете данни в тази таблица или дори да създадете индекс в движение. Трябваше да направя това с доклади, макар и само веднъж. Тогава намалих времето за генериране на един отчет от 3 часа на 20 минути.
Предпочитам да използвам една голяма заявка, вместо да я разделя на няколко заявки и съхраняването води до променливи. Позволете на SQL Server да настрои производителността на голяма заявка и тя няма да ви разочарова. Моля, имайте предвид, че трябва да прибягвате до таблични променливи само в екстремни случаи, когато наистина виждате предимствата им.