SQL Server има базиран на разходите оптимизатор, който използва знания за различните таблици, включени в заявка, за да произведе това, което реши, че е най-оптималният план за времето, с което разполага по време на компилацията. Това знание включва всички съществуващи индекси и техните размери и каквато и статистика на колоните да съществува. Част от това, което влиза в намирането на оптимален план за заявка, се опитва да сведе до минимум броя на физическите четения, необходими по време на изпълнението на плана.
Едно нещо, което ме питаха няколко пъти, е защо оптимизаторът не взема предвид какво има в буферния пул на SQL Server, когато компилира план за заявка, тъй като със сигурност това може да направи заявката да се изпълнява по-бързо. В тази публикация ще обясня защо.
Определяне на съдържанието на буферния пул
Първата причина, поради която оптимизаторът игнорира буферния пул е, че е нетривиален проблем да се разбере какво има в буферния пул поради начина, по който е организиран буферният пул. Страниците с файлове с данни се контролират в буферния пул от малки структури от данни, наречени буфери, които проследяват неща като (неизчерпателен списък):
- Идентификационният номер на страницата (номер на файла:номер на страница във файл)
- Последният път, когато страницата е била препращана (използвана от мързеливия писател, за да помогне за внедряването на най-малко наскоро използвания алгоритъм, който създава свободно пространство, когато е необходимо)
- Местоположението на паметта на 8KB страницата в буферния пул
- Независимо дали страницата е мръсна или не (мръсна страница има промени на нея, които все още не са записани обратно в трайно хранилище)
- Разпределителната единица, към която принадлежи страницата (обяснено тук) и идентификаторът на разпределителната единица може да се използва, за да се разбере каква таблица и индекс е част от страницата
За всяка база данни, която има страници в буферния пул, има хеш списък със страници, в ред на идентификатора на страницата, който може бързо да се търси, за да се определи дали дадена страница вече е в паметта или трябва да се извърши физическо четене. Нищо обаче не позволява лесно на SQL Server да определи какъв процент от нивото на листа за всеки индекс на таблица вече е в паметта. Кодът ще трябва да сканира целия списък с буфери за базата данни, търсейки буфери, които картографират страниците за въпросната единица за разпределение. И колкото повече страници са в паметта на база данни, толкова по-дълго ще отнеме сканирането. Би било твърде скъпо да се направи като част от компилацията на заявка.
Ако се интересувате, написах публикация преди известно време с някакъв T-SQL код, който сканира буферния пул и дава някои показатели, използвайки DMV sys.dm_os_buffer_descriptors .
Защо използването на съдържанието на буферния пул би било опасно
Нека се преструваме, че *има* високоефективен механизъм за определяне на съдържанието на буферния пул, който оптимизаторът може да използва, за да му помогне да избере кой индекс да използва в план за заявка. Хипотезата, която ще проуча е, че ако оптимизаторът знае достатъчно, че по-малко ефективен (по-голям) индекс вече е в паметта, в сравнение с най-ефективния (по-малък) индекс за използване, той трябва да избере индекса в паметта, защото ще намалете броя на необходимите физически четения и заявката ще работи по-бързо.
Сценарият, който ще използвам, е следният:таблица BigTable има два неклъстерирани индекса, Index_A и Index_B, като и двата напълно покриват конкретна заявка. Заявката изисква пълно сканиране на нивото на листа на индекса, за да се извлекат резултатите от заявката. Таблицата има 1 милион реда. Index_A има 200 000 страници на ниво лист, а Index_B има 1 милион страници на ниво лист, така че пълното сканиране на Index_B изисква обработка пет пъти повече страници.
Създадох този измислен пример на лаптоп, работещ със SQL Server 2019 с 8 процесорни ядра, 32 GB памет и твърдотелни дискове. Кодът е както следва:
СЪЗДАДЕТЕ ТАБЛИЦА BigTable ( c1 BIGINT IDENTITY, c2 AS (c1 * 2), c3 CHAR (1500) DEFAULT 'a', c4 CHAR (5000) DEFAULT 'b');GO INSERT INTO BigTable DEFAULT STONS;GO 1000000 CREATE НЕКЛУСТРИРАН ИНДЕКС Индекс_A НА BigTable (c2) ВКЛЮЧВА (c3);-- 5 записа на страница =200 000 странициGO СЪЗДАЙТЕ НЕКЛУСТРИРАН ИНДЕКС Индекс_B НА BigTable (c2) ВКЛЮЧВАТЕ (c4);-- 1 запис на страница =1 милион CHECKPO INT; /предварително>И тогава засегнах измислените заявки:
DBCC DROPCLEANBUFFERS;GO -- Индекс_A не е в паметтаSELECT SUM (c2) FROM BigTable WITH (INDEX (Index_A));GO-- CPU време =796 ms, изминало време =764 ms -- Index_A в памет SELECT SUM (c2) ОТ BigTable WITH (ИНДЕКС (Индекс_A));GO-- Време на процесора =312 ms, изминало време =52 ms DBCC DROPCLEANBUFFERS;GO -- Индекс_B не е в паметта SELECT SUM (c2) ОТ BigTable WITH (ИНДЕКС (Индекс_B));GO- - Време на процесора =2952 ms, изминало време =2761 ms -- Индекс_B в паметИЗБИРАНЕ СУМ (c2) ОТ BigTable WITH (ИНДЕКС (Индекс_B));GO-- Време на процесора =1219 ms, изминало време =149 msМожете да видите, когато нито един от индексите не е в паметта, Index_A е лесно най-ефективният индекс за използване, с изминало време на заявка от 764ms срещу 2761ms при използване на Index_B, и същото е вярно, когато и двата индекса са в паметта. Ако обаче Index_B е в паметта, а Index_A не е, ако заявката използва Index_B (149ms), тя ще работи по-бързо, отколкото ако използва Index_A (764ms).
Сега нека позволим на оптимизатора да базира избора на план върху това, което е в буферния пул...
Ако Index_A не е предимно в паметта, а Index_B е предимно в паметта, би било по-ефективно да се компилира планът на заявката, за да се използва Index_B, за заявка, изпълнявана в този момент. Въпреки че Index_B е по-голям и ще има нужда от повече цикли на процесора за сканиране, физическите четения са много по-бавни от допълнителните цикли на процесора, така че по-ефективният план за заявка минимизира броя на физическите четения.
Този аргумент е валиден само и планът за заявка „използване на Index_B“ е само по-ефективен от план за заявка „използване на Index_A“, ако Index_B остава предимно в паметта, а Index_A остава предимно не в паметта. Веднага щом по-голямата част от Index_A е в паметта, планът за заявка „use Index_A“ ще бъде по-ефективен, а планът за заявка „use Index_B“ е грешен избор.
Ситуациите, в които компилираният план „използване на индекс_B“ е по-малко ефективен от базирания на разходите план „използване на индекс_A“ са (обобщаващи):
- Index_A и Index_B са в паметта:компилираният план ще отнеме почти три пъти повече време
- Нито един индекс не е резидентен в паметта:компилираният план отнема 3,5 пъти повече време
- Index_A е резидентен в паметта, а Index_B не е:всички физически четения, извършени от плана, са външни и това ще отнеме огромни 53 пъти повече време
Резюме
Въпреки че в нашето мисловно упражнение оптимизаторът може да използва знанията за буферния пул, за да компилира най-ефективната заявка в един момент, това би било опасен начин за стимулиране на компилацията на план поради потенциалната нестабилност на съдържанието на буферния пул, което прави бъдещата ефективност на кешираният план е много ненадежден.
Не забравяйте, че работата на оптимизатора е бързо да намери добър план, а не непременно най-добрия план за 100% от всички ситуации. Според мен оптимизаторът на SQL Server прави правилното нещо, като игнорира действителното съдържание на буферния пул на SQL Server и вместо това разчита на различните правила за изчисляване на разходите, вместо да създаде план за заявка, който вероятно ще бъде най-ефективен през повечето време .