Archiwa blogu

Statystyki w SQL Server: GEEK LEVEL BONUS

Jak zapewne pamiętacie kilka dni temu prowadziłem prelekcję na spotkaniu Trójmiejskiej Zawodowej Grupy .Net. W ramach tego spotkania pokazałem demo, które jest odpowiedzią na bardzo specyficzny problem 🙂

Definicja problemu

Wyobraźmy sobie sytuację, w której jesteście osobami z działu developmentu i dostajecie zgłoszenie odnośnie problemów wydajnościowych związanych z działaniem waszej aplikacji. Standardowym sposobem byłoby podłączenie się do instancji i stworzenie odpowiedniego trace-a. Tym razem jest niestety nieco inaczej – dane klienta są tajne i nie możecie w żaden sposób ich podejrzeć. Oczywiście możecie prosić o różnego rodzaju zrzuty wydajnościowe, jednak działanie „po omacku” i przy pomocy osób trzecich jest czasem problematyczne …

Co możemy w takim razie zrobić?

I tutaj do akcji wchodzi mój GEEK LEVEL BONUS :). Może by oszukać optymalizator i sprawić by „myślał”, że jest u naszego klienta? Tylko jak to zrobić …

Na początek imitacja bazy klienta.

Załóżmy, że naszą bazą klienta jest baza AdventureWorks2012, którą można pobrać z witryny Codeplex, a problematyczna instrukcja SELECT wygląda następująco:

SELECT *
fROM AdventureWorks2012.Sales.SalesOrderDetail sod
JOIN AdventureWorks2012.[Sales].[SalesOrderHeader] soh
ON soh.SalesOrderID = sod.SalesOrderID
WHERE sod.CarrierTrackingNumber = '4911-403C-98'

A teraz nasza baza developerska

Jako bazę developerską użyjemy również bazy AdventureWorks2012. Jedyne co, to odzyskamy ją z kopii zapasowej pod inną nazwą (np. AdventureGeek) i usuniemy informacje z tabel , tak aby zasymulować brak danych na serwerze developerskim:

DELETE AdventureGeek.Sales.SalesOrderDetail

DELETE AdventureGeek.Sales.SalesOrderHeader

Gdy odpalimy nasz selekt na bazie developerskiej zobaczymy następujący plan zapytania:

SELECT *
fROM AdventureGeek.Sales.SalesOrderDetail sod
JOIN AdventureGeek.[Sales].[SalesOrderHeader] soh
ON soh.SalesOrderID = sod.SalesOrderID
WHERE sod.CarrierTrackingNumber = '4911-403C-98'

GEEK_LEVEL_BONUS_01

No tak … ale co w tym planie jest nie tak? Oczywiście mamy pewne ostrzeżenie optymailizatora, ale mimo wszystko plan zapytania na pierwszy rzut oka wygląda w miarę OK (no, może cześć z was zauważy niuans naprowadzający na rozwiązanie). Dlaczego tak się stało – po prostu mamy inne dane niż nasz klient (a dokładnie nie mamy danych). Diagnostyka, co może być nie tak przeprowadzona w ten sposób może okazać się dość kłopotliwa.

No dobrze, ale wspomniałem coś o oszukiwaniu optymalizatora 🙂

Na początek użyjemy instrukcji DBCC SHOW_STATISTICS, która służy do pokazania informacji na temat rozkładu danych w tabeli. Instrukcja ta zawiera nie do końca udokumentowaną opcję o nazwie STATS_STREAM:

DBCC SHOW_STATISTICS ('AdventureWorks2012.Sales.SalesOrderDetail', 'PK_SalesOrderDetail_SalesOrderID_SalesOrderDetailID')
WITH STATS_STREAM

GEEK_LEVEL_BONUS_02

Gdy zajrzymy w SQL Server Books Online możemy o tej opcji przeczytać następujące zdanie:

STATS_STREAM is Identified for informational purposes only. Not supported. Future compatibility is not guaranteed.

Jeżeli namówimy osobę po stronie klienta by skopiowała i udostępniła nam jej wyniki, to może być tylko lepiej 🙂

W drugim kroku użyjemy instrukcji UPDATE STATISTICS (już na naszej bazie), przepisując wartości ROWCOUNT, PAGECOUNT I STATS_STREAM na zgodne z wypisanymi we wcześniejszym kroku:

UPDATE STATISTICS AdventureGeek.Sales.SalesOrderDetail PK_SalesOrderDetail_SalesOrderID_SalesOrderDetailID
WITH ROWCOUNT = 121317, PAGECOUNT = 1237,
STATS_STREAM = -- tutaj wklejamy dane STATS_STREAM z poprzedniego kroku :)

No i oczywiście odpalmy nasze zapytanie w bazie AdventureGeek. Dla łatwiejszego porównania wyników zamieszczam obie instrukcje SELECT:

-- baza developerska
SELECT *
FROM AdventureGeek.Sales.SalesOrderDetail sod
JOIN AdventureGeek.[Sales].[SalesOrderHeader] soh
ON soh.SalesOrderID = sod.SalesOrderID
WHERE sod.CarrierTrackingNumber = '4911-403C-98'

-- baza klienta
SELECT *
FROM AdventureWorks2012.Sales.SalesOrderDetail sod
JOIN AdventureWorks2012.[Sales].[SalesOrderHeader] soh
ON soh.SalesOrderID = sod.SalesOrderID
WHERE sod.CarrierTrackingNumber = '4911-403C-98'

GEEK_LEVEL_BONUS_03

Jak widać podpowiedź optymalizatora w obu przypadkach jest taka sama – utworzyć indeks na kolumnie CarrierTrackingNumber. Oczywiście oba zaprezentowane plany są nieco odmienne. Na bazie developerskiej obie tabele nie są wypełnione wartościami, a my podmieniliśmy tylko jedną ze statystyk. Pozostałe kolumny nadal pozostają dla SQL Server-a puste.  

Mimo wszystko, na powyższym przykładzie widać, że nasza idea jak działa. Podmieniając kolejne statystyki dla obu tabel w bazie AdventureGeek moglibyśmy doprowadzić do wygenerowania identycznych planów.

Podsumowanie

Mam nadzieję, że moje rozwiązanie związane z podmianą statystyk wam się podoba. Jedynie co, to proszę – nie róbcie tego typu „sztuczek” na serwerach „u klienta”. Oczywiście chcielibyśmy, aby działały ładnie jak developerskie u nas, lecz nie jestem przekonany, czy ta sztuczka będzie działać w obie strony 🙂

PS. Pamiętajcie, że te rozwiązanie jest totalnie niewspierane !!!! (nawet przez mnie :))