Porównanie ntext i nvarchar(max)
Informacje na temat ntext i nvarchar(MAX)
W dokumentacji MSDN obok typów ntext, text oraz image pojawiła się następująca notka:
ntext, text, and image data types will be removed in a future version of Microsoft SQL Server. Avoid using these data types in new development work, and plan to modify applications that currently use them. Use nvarchar(max), varchar(max), and varbinary(max) instead.
Przekładając na nasze dowiadujemy się, że typy ntext, text i image zostaną w którejś z kolejnych wersji SQL Server usunięte. Należy zatem unikać tych typów i zaplanować zmiany w aplikacjach, które z tych typów korzystają. Zamiast wspomnianych należy używać nvarchar(max), varchar(max), and varbinary(max).
Dowiadujemy się tam również, że ntext to łańcuchy znaków zmiennej długości i maksymalnym rozmiarze 2^30 - 1 (tj. 1 073 741 823) znaków. Zajmowany obszar w bazie jest oczywiście dwa razy większy niż ilość znaków.
Korzystając z typu nvarchar(max) możemy wstawić łańcuchy znaków takiej samej długości, jakie mogliśmy umieszczać w typie ntext. Dokumentacja podpowiada nam także, że dane tekstowe korzystają z formatu UNICODE UCS-2.
Obie definicje wydają się bardzo podobne, wręcz identyczne. Co zatem sprawiło, że Microsoft odradza stosowanie pierwszego typu, a zaleca stosowanie nvarchar(max)? Przekonamy się o tym czytając dalszą część artykułu.
Wydajność operacji INSERT
Sróbujmy stworzyć dwie tabele, a następnie wrzućmy do tych tabel, powiedzmy, 50000 rekordów.
CREATE TABLE NTxt
(
txt ntext
)
--Tabela nvarchar(max)
CREATE TABLE NVar
(
txt NVARCHAR(MAX)
)
Teraz wykonajmy naszą procedurę do badania wydajności operacji INSERT:
DECLARE @i int=1
--operacja na tabeli ntext
DECLARE @t DATETIME = GETDATE()
WHILE @i<=50000
BEGIN
INSERT INTO NTxt values('WWW')
SET @i=@i+1
END
SELECT DATEDIFF(millisecond,@t,GETDATE())
SET @i=1
--operacja na tabeli nvarchar(max)
SELECT @t = GETDATE()
WHILE @i<=50000
BEGIN
INSERT INTO NVar values('WWW')
SET @i=@i+1
END
SELECT DATEDIFF(millisecond,@t,GETDATE())
Rezultaty są następujące:
ntext | 20220 | 18556 | 20163 | 21740 | 21393 | 21610 |
nvarchar(MAX) | 19656 | 19343 | 19436 | 20720 | 21393 | 18156 |
Średnia | Mediana | |
ntext | 20613,7 | 20806,5 |
nvarchar(MAX) | 20194,0 | 20136,0 |
Pomiar 2:
ntext | 18013 | 18166 | 18523 | 19200 | 18910 |
nvarchar(MAX) | 18163 | 17926 | 18493 | 17946 | 18156 |
Średnia | Mediana | |
ntext | 18562,4 | 18523 |
nvarchar(MAX) | 18136,8 | 18156 |
Pomiar 3, tym razem 150000 rekordów:
ntext | 55760 | 56436 | 58663 | 55800 | 55840 |
nvarchar(MAX) | 54633 | 56063 | 55336 | 53833 | 55600 |
Średnia | Mediana | |
ntext | 56499,8 | 55840 |
nvarchar(MAX) | 55093 | 55336 |
Wzrostu wydajności rzędu 1-2% nie należy traktować jako coś nadzwyczajnego, wszystko mieści się w granicach błędu. To nie tutaj należy szukać poprawy wydajności, choć różnica jest już zauważalna i powtarzalna. Z czego to wynika? Zapewne z tego, że SQL Server musi modyfikować większą ilość stron pamięci masowej. Więcej szczegółów dalej. Jako ciekawostkę podam fakt, że te same operacje wykonywane w jednej transakcji są w sposób oczywisty szybsze. Co więcej, wtedy dopiero wychodzi przewaga nvarchar(MAX) nad ntext. Przy jednej transakcji większy wpływ na wydajność zapisu ma rzeczywisty zapis danych, a mniejszy modyfikacja indeksu stosowego, nieklastrowanego i logowanie poszczególnych operacji. Wyniki są następujące (wstawianie 150000 rekordów):
ntext | 6860 | 6220 | 6400 | 6250 | 6170 |
nvarchar(MAX) | 4970 | 4620 | 4540 | 4560 | 4600 |
Średnia | Mediana | |
ntext | 6380 | 6250 |
nvarchar(MAX) | 4658 | 4600 |
Jak widać, tym razem różnica jest znaczna. Może nie jest to przekonujące zwycięstwo, ale możemy spokojnie napisać 1:0 dla nvarchar(max).
Rozmiary tabel ntext i nvarchar(MAX)
Pierwszy test być może nie wszystkich przekonał i nie wykazał jakiejś nadzwyczajnej poprawy, choć przy dużej ilości danych ta poprawa jest zauważalna. Spróbujmy zatem przyjrzeć się rozmiarom obu tabel. Na początek skorzystamy z porcedury sp_spaceused. Przypomnijmy, że w każdej z tabel zapisanych jest 150000 rekordów.
EXEC sp_spaceused 'NVar'
Wyniki z tabeli NTxt przedstawiają się następująco:
name | rows | reserved | data | index_size | unused |
NTxt | 150000 | 18000 KB | 17920 KB | 8 KB | 72 KB |
Poniżej wyniki tabeli NVar:
name | rows | reserved | data | index_size | unused |
NVar | 150000 | 3080 KB | 3032 KB | 8 KB | 40 KB |
Co tutaj widać? Po pierwsze to, że dla tabeli z typem ntext zarezerwowane jest 18000 KB (używane 17920 KB), podczas gdy dla tabeli NVar tylko 3080 KB. Poprawa jest tutaj dość znaczna. W tym przypadku ponad pięciokrotna. Czy to nie jest wystarczający powód, aby zmienić w istniejących bazach kolumny typu ntext na nvarchar(max)? Być może jest, ale nie jest to koniec testów porównawczych obu typów. Przyjrzyjmy się jeszcze dokładnej strukturze obu tabel. Tym razym skorzystamy z widoku systemowego sys.dm_db_partition_stats.
lob_used_page_count,used_page_count, row_count
FROM sys.dm_db_partition_stats WHERE [object_id]=OBJECT_ID('NTxt')
SELECT 'NVar' [Table], in_row_data_page_count,
lob_used_page_count, used_page_count, row_count
FROM sys.dm_db_partition_stats WHERE [object_id]=OBJECT_ID('NVar')
Popatrzmy teraz na wyniki zwrócone przez te dwa zapytania:
Table | in_row_data_page_count | lob_used_page_count | used_page_count | row_count |
NTxt | 572 | 1668 | 2241 | 150000 |
Table | in_row_data_page_count | lob_used_page_count | used_page_count | row_count |
NVar | 379 | 0 | 380 | 150000 |
Tutaj znacznie lepiej widać co się stało i jaki jest powód różnic wydajnościowych. Dane w kolumnie typu ntext przechowywane są w obszarze pamięci przeznaczonym na duże obiekty (LOB_DATA). Dane typu nvarchar(max) przechowywane są w obszarze dużych obiektów tylko wtedy, gdy nie mieszczą się na jednej stronie pamięci. Nie ma niepotrzebnych wskaźników, nie ma struktur pośrednich. Jak się później przekonamy - nie zawsze. Nie uprzedzajmy jednak faktów.
Wydajność operacji SELECT
Przed każdą operacją SELECT należy wyczyścić wewnętrzny bufor SQL Server. SQL Server może zapamiętywać sobie wyniki poprzednich operacji. Sprawia to, że zamiast wykonywać fizyczny dostęp do danych na dysku, wykonuje fizyczny dostęp do danych bufora. Jak nietrudno się domyślić, dostęp do bufora jest znacznie szybszy niż do danych z dysku, a nasze pomiary okażą się niemiarodajne i przekłamane. Do czyszczenia pamięci cache serwera służy instrukcja DBCC DROPCLEANBUFFERS.
Czas na pomiar numer 1. Procedura pomiarowa jest prosta i polega na wykonaniu operacji SELECT * FROM Tabela. Popatrzmy na wyniki tych operacji. Dla pewności zostały one powtórzone 10 razy.
NTxt | 1650 | 1800 | 1686 | 1610 | 1526 | 1660 | 1843 | 1743 | 1653 | 1606 |
NVar | 803 | 820 | 773 | 926 | 746 | 783 | 780 | 750 | 730 | 760 |
Średnia | Mediana | |
ntext | 1677,7 | 1656,5 |
nvarchar(MAX) | 787,1 | 776,5 |
Co się stało, że wyniki są takie różne? Po pierwsze, jak już mogliśmy się przekonać, dane typu nvarchar(max) zapisane są na mniejszej liczbe stron. Konsekwencja tego jest taka, że SQL Server odczytuje z dysku mniejszą ilość danych. Mniej operacji odczytu to szybsze działanie, bo głównym kosztem są operacje dyskowe, o kilka rzędów wielkości wolniejsze niż operacje na pamięci operacyjnej. Druga rzecz - podczas operacji odczytu nie są wykonywane dodatkowe skoki do obszaru dużych obiektów - to też ma swój wpływ na wydajność kwerend.
Porównajmy jeszcze plan wykonania.
Rys. 1. Porównanie instrukcji SELECT dla ntext i nvarchar(MAX) na planie wykonania.
Jak widać na załączonym rysunku, także plany wykonania zakładają, że operacja odczytu tej samej ilości rekordów będzie trwała dłużej dla kolumn typu ntext. Różnica w testach jest znacznie większa, ale pamiętajmy, że w naszym przypadku wszystkie rekordy mieszczą się na stronie i nie ma żadnych odwołań do obszaru LOB_DATA. Przypomnijmy, że nvarchar(max) też wędruje do obszaru dużych obiektów, ale tylko wtedy, gdy rozmiar przekracza 8KB. Doświadczenie pokazuje, że większość danych w takich kolumnach ma rozmiar mniejszy niż 8KB. Jeżeli nawet niewielka część danych, powiedzmy 30%, mieści się na stronie razem z pozostałymi danymi, to osiągniemy wymierne korzyści - poprawiając wydajnośći nieznacznie zmniejszając obszar zajmowany przez te dane.
Wnioski
Dość niespodziewanie wyszedł z tego wpisu dość spory artykuł. Nie oznacza to, że temat został wyczerpany. Zachęcam do dzielenia się własnymi spostrzeżeniami w komentarzach i używania nvarchar(MAX). Skrypt, który zamienia wszystkie kolumny typu ntext na nvarchar(MAX) można znaleźć w tym wpisie: Zamiana wszystkich kolumn typu ntext na nvarchar(MAX).
Kategoria:Optymalizacja SQLSQL Server
Komentarze: