W tym artykule dzielę się kilkoma obserwacjami, jakie miałem dotyczące datetime2 wielkość magazynu typu danych w SQL Server. Być może wyjaśnię kilka punktów dotyczących rzeczywistego rozmiaru pamięci używanej przez ten typ danych podczas przechowywania w bazie danych.
W szczególności patrzę na następujące:
- Dokumentacja Microsoft
- Dane przechowywane w zmiennej
- Długość w bajtach przy użyciu
DATALENGTH()
- Długość w bajtach przy użyciu
DATALENGTH()
po konwersji na warbinę
- Długość w bajtach przy użyciu
- Dane przechowywane w bazie danych
- Długość w bajtach przy użyciu
COL_LENGTH()
- Długość w bajtach przy użyciu
DBCC PAGE()
- Długość w bajtach przy użyciu
Niektóre z nich wydają się być ze sobą sprzeczne i zobaczysz dwie różne wielkości pamięci dla tej samej wartości, w zależności od tego, gdzie spojrzysz.
data/godzina2 wartość może pokazywać inny rozmiar pamięci, w zależności od tego, czy jest przechowywana w bazie danych, jako datetime2 zmienna lub przekonwertowana na zmienną .
Ale jest na to wiarygodne wytłumaczenie – zależy to od tego, gdzie precyzja jest przechowywany.
Podczas moich badań nad tym problemem znalazłem szczegółowy artykuł Ronena Ariely na temat tego, jak datetime2 jest przechowywany w pliku danych bardzo pouczający i skłonił mnie do przeprowadzenia podobnych testów w moim własnym środowisku programistycznym i zaprezentowania ich tutaj.
Dokumentacja firmy Microsoft
Najpierw spójrzmy, co mówi oficjalna dokumentacja.
Dokumentacja Microsoft dotycząca datetime2 typ danych określa, że jego rozmiar pamięci jest następujący:
6 bajtów dla precyzji mniejszej niż 3.
7 bajtów dla precyzji 3 lub 4.
Wszystkie inne precyzje wymagają 8 bajtów.
Ale kwalifikuje powyższą tabelę następującym stwierdzeniem:
Pierwszy bajt datetime2 wartość przechowuje precyzję wartości, co oznacza rzeczywistą pamięć wymaganą dla datetime2 value to rozmiar pamięci wskazany w powyższej tabeli plus 1 dodatkowy bajt do przechowywania precyzji. To sprawia, że maksymalny rozmiar datetime2 wartość 9 bajtów – 1 bajt przechowuje precyzję plus 8 bajtów do przechowywania danych z maksymalną precyzją.
Biorąc więc pod uwagę powyższe informacje, oczywistym wnioskiem do wyciągnięcia byłoby to, że tabela mogłaby/(powinna?) być napisana w następujący sposób:
7 bajtów dla precyzji mniejszej niż 3.
8 bajtów dla precyzji 3 lub 4.
Cała inna precyzja wymaga 9 bajtów.
W ten sposób nie musieliby określać tego dodatkowymi informacjami o precyzji.
Ale to nie jest takie proste.
Dane przechowywane w zmiennej
Najpierw zapiszmy datetime2 wartość w zmiennej i sprawdź jej rozmiar pamięci. Następnie przekonwertuję tę wartość na varbinary i sprawdź ponownie.
Długość w bajtach przy użyciu DATALENGTH
Oto, co się stanie, jeśli użyjemy DATALENGTH()
funkcja zwracająca liczbę bajtów użytych dla datetime2(7) wartość:
DECLARE @d datetime2(7); SET @d = '2025-05-21 10:15:30.1234567'; SELECT @d AS 'Value', DATALENGTH(@d) AS 'Length in Bytes';
Wynik
+-----------------------------+-------------------+ | Value | Length in Bytes | |-----------------------------+-------------------| | 2025-05-21 10:15:30.1234567 | 8 | +-----------------------------+-------------------+
Wartość w tym przykładzie ma maksymalną skalę 7 (ponieważ deklaruję zmienną jako datetime2(7) ) i zwraca długość 8 bajtów.
Wydaje się to zaprzeczać temu, co Microsoft stwierdza o potrzebie dodatkowego bajtu do przechowywania precyzji. Cytując Microsoft, To sprawia, że maksymalny rozmiar datetime2 wartość 9 bajtów – 1 bajt przechowuje precyzję plus 8 bajtów do przechowywania danych z maksymalną precyzją.
.
Chociaż prawdą jest, że wydaje się, że mamy 8 bajtów do przechowywania danych
, wygląda na to, że brakuje 1 bajtu używanego do przechowywania precyzji.
Jeśli jednak przekonwertujemy wartość na varbinary otrzymujemy inną historię.
Długość w bajtach po konwersji na „zmienną”
Oto, co się stanie, jeśli przekonwertujemy naszą datetime2 wartość do zmiennej :
DECLARE @d datetime2(7); SET @d = '2025-05-21 10:15:30.1234567'; SELECT CONVERT(VARBINARY(10), @d) AS 'Value', DATALENGTH(CONVERT(VARBINARY(10), @d)) AS 'Length in Bytes';
Wynik
+----------------------+-------------------+ | Value | Length in Bytes | |----------------------+-------------------| | 0x0787A311FC553F480B | 9 | +----------------------+-------------------+
W tym przypadku otrzymujemy 9 bajtów.
To jest szesnastkowa reprezentacja datetime2 wartość. Rzeczywista wartość daty i czasu (i jej precyzja) to wszystko po 0x
. Każda para znaków szesnastkowych to bajt. Jest 9 par, a więc 9 bajtów. Potwierdzamy to, gdy używamy DATALENGTH()
aby zwrócić długość w bajtach.
W tym przykładzie widzimy, że pierwszy bajt to 07
. To reprezentuje precyzję (użyłem skali 7, więc to jest tutaj wyświetlane).
Jeśli zmienię skalę, zobaczymy, że pierwszy bajt zmienia się, aby dopasować skalę:
DECLARE @d datetime2(3); SET @d = '2025-05-21 10:15:30.1234567'; SELECT CONVERT(VARBINARY(10), @d) AS 'Value', DATALENGTH(CONVERT(VARBINARY(10), @d)) AS 'Length in Bytes';
Wynik
+--------------------+-------------------+ | Value | Length in Bytes | |--------------------+-------------------| | 0x034B8233023F480B | 8 | +--------------------+-------------------+
Widzimy również, że długość jest odpowiednio zmniejszona.
Tak więc w tym przypadku nasze wyniki idealnie pasują do dokumentacji Microsoft – dodano dodatkowy bajt dla precyzji.
Wielu programistów zakłada, że w ten sposób SQL Server przechowuje swój datetime2 wartości w bazie danych. Jednak to założenie wydaje się być błędne.
Dane przechowywane w bazie danych
W tym przykładzie tworzę bazę danych zawierającą tabelę z różnymi datetime2(n) kolumny. Następnie używam COL_LENGTH()
aby zwrócić długość każdej kolumny w bajtach. Następnie wstawiam do niego wartości, przed użyciem DBCC PAGE
aby sprawdzić rozmiar pamięci, który każdy datetime2 wartość zajmuje plik strony.
Utwórz bazę danych:
CREATE DATABASE Test;
Utwórz tabelę:
USE Test; CREATE TABLE Datetime2Test ( d0 datetime2(0), d1 datetime2(1), d2 datetime2(2), d3 datetime2(3), d4 datetime2(4), d5 datetime2(5), d6 datetime2(6), d7 datetime2(7) );
W tym przypadku tworzę osiem kolumn – po jednej dla każdej zdefiniowanej przez użytkownika skali, której możemy użyć z datetime2(n) .
Teraz możemy sprawdzić rozmiar pamięci każdej kolumny.
Długość w bajtach przy użyciu COL_LENGTH()
Użyj COL_LENGTH()
aby sprawdzić długość (w bajtach) każdej kolumny:
SELECT COL_LENGTH ( 'Datetime2Test' , 'd0' ) AS 'd0', COL_LENGTH ( 'Datetime2Test' , 'd1' ) AS 'd1', COL_LENGTH ( 'Datetime2Test' , 'd2' ) AS 'd2', COL_LENGTH ( 'Datetime2Test' , 'd3' ) AS 'd3', COL_LENGTH ( 'Datetime2Test' , 'd4' ) AS 'd4', COL_LENGTH ( 'Datetime2Test' , 'd5' ) AS 'd5', COL_LENGTH ( 'Datetime2Test' , 'd6' ) AS 'd6', COL_LENGTH ( 'Datetime2Test' , 'd7' ) AS 'd7';
Wynik:
+------+------+------+------+------+------+------+------+ | d0 | d1 | d2 | d3 | d4 | d5 | d6 | d7 | |------+------+------+------+------+------+------+------| | 6 | 6 | 6 | 7 | 7 | 8 | 8 | 8 | +------+------+------+------+------+------+------+------+
Więc po raz kolejny wydaje się, że nie otrzymujemy dodatkowego bajtu używanego do przechowywania precyzji.
Użyj strony DBCC PAGE do sprawdzenia zapisanych danych
Teraz użyjmy DBCC PAGE
aby znaleźć rzeczywisty rozmiar danych, które przechowujemy w tej tabeli.
Najpierw wstawmy trochę danych:
DECLARE @d datetime2(7) = '2025-05-21 10:15:30.1234567'; INSERT INTO Datetime2Test ( d0, d1, d2, d3, d4, d5, d6, d7 ) SELECT @d, @d, @d, @d, @d, @d, @d, @d;
Teraz wybierz dane (tylko po to, aby je sprawdzić):
SELECT * FROM Datetime2Test;
Wynik (przy użyciu wyjścia pionowego):
d0 | 2025-05-21 10:15:30 d1 | 2025-05-21 10:15:30.1 d2 | 2025-05-21 10:15:30.12 d3 | 2025-05-21 10:15:30.123 d4 | 2025-05-21 10:15:30.1235 d5 | 2025-05-21 10:15:30.12346 d6 | 2025-05-21 10:15:30.123457 d7 | 2025-05-21 10:15:30.1234567
Zgodnie z oczekiwaniami wartości wykorzystują precyzję, która została wcześniej określona na poziomie kolumny.
Teraz, zanim użyjemy DBCC PAGE()
, musimy wiedzieć, który PagePID przekazać do niego. Możemy użyć DBCC IND()
aby to znaleźć.
Znajdź identyfikator strony:
DBCC IND('Test', 'dbo.Datetime2Test', 0);
Wynik (przy użyciu wyjścia pionowego):
-[ RECORD 1 ]------------------------- PageFID | 1 PagePID | 306 IAMFID | NULL IAMPID | NULL ObjectID | 1205579333 IndexID | 0 PartitionNumber | 1 PartitionID | 72057594043039744 iam_chain_type | In-row data PageType | 10 IndexLevel | NULL NextPageFID | 0 NextPagePID | 0 PrevPageFID | 0 PrevPagePID | 0 -[ RECORD 2 ]------------------------- PageFID | 1 PagePID | 360 IAMFID | 1 IAMPID | 306 ObjectID | 1205579333 IndexID | 0 PartitionNumber | 1 PartitionID | 72057594043039744 iam_chain_type | In-row data PageType | 1 IndexLevel | 0 NextPageFID | 0 NextPagePID | 0 PrevPageFID | 0 PrevPagePID | 0
Zwraca to dwa rekordy. Interesuje nas PageType 1 (drugi rekord). Chcemy mieć PagePID z tego rekordu. W tym przypadku PagePID to 360 .
Teraz możemy wziąć ten PagePID i użyć go w następujący sposób:
DBCC TRACEON(3604, -1); DBCC PAGE(Test, 1, 360, 3);
Daje to dużo danych, ale interesuje nas głównie następująca część:
Slot 0 Column 1 Offset 0x4 Length 6 Length (physical) 6 d0 = 2025-05-21 10:15:30 Slot 0 Column 2 Offset 0xa Length 6 Length (physical) 6 d1 = 2025-05-21 10:15:30.1 Slot 0 Column 3 Offset 0x10 Length 6 Length (physical) 6 d2 = 2025-05-21 10:15:30.12 Slot 0 Column 4 Offset 0x16 Length 7 Length (physical) 7 d3 = 2025-05-21 10:15:30.123 Slot 0 Column 5 Offset 0x1d Length 7 Length (physical) 7 d4 = 2025-05-21 10:15:30.1235 Slot 0 Column 6 Offset 0x24 Length 8 Length (physical) 8 d5 = 2025-05-21 10:15:30.12346 Slot 0 Column 7 Offset 0x2c Length 8 Length (physical) 8 d6 = 2025-05-21 10:15:30.123457 Slot 0 Column 8 Offset 0x34 Length 8 Length (physical) 8 d7 = 2025-05-21 10:15:30.1234567
Wygląda więc na to, że nie używa dodatkowego bajtu do precyzji.
Ale przyjrzyjmy się rzeczywistym danym, zanim wyciągniemy jakiekolwiek wnioski.
Rzeczywiste dane są przechowywane w tej części pliku strony:
Memory Dump @0x000000041883A060 0000000000000000: 10003c00 4290003f 480b95a2 053f480b d459383f ..<.B..?H.¢.?H.ÔY8? 0000000000000014: 480b4b82 33023f48 0bf31603 163f480b 7ae51edc H.K3.?H.ó...?H.zå.Ü 0000000000000028: 003f480b c1f63499 083f480b 87a311fc 553f480b .?H.Áö4..?H.£.üU?H. 000000000000003C: 080000 ... ...
Jak widać, żaden z tych wyników nie przypomina wyników, które uzyskalibyśmy, konwertując datetime2 wartość do zmiennej . Ale jest całkiem blisko.
Oto, jak to wygląda, jeśli usunę kilka rzeczy:
4290003f 480b95a2 053f480b d459383f 480b4b82 33023f48 0bf31603 163f480b 7ae51edc 003f480b c1f63499 083f480b 87a311fc 553f480b
Pozostałe cyfry szesnastkowe zawierają wszystkie nasze dane daty i godziny, ale nie precyzję . Jednak musimy zmienić kolejność spacji, aby uzyskać rzeczywiste wartości dla każdego wiersza.
Oto efekt końcowy. Dla lepszej czytelności umieściłem każdą wartość daty/czasu w nowej linii.
4290003f480b 95a2053f480b d459383f480b 4b8233023f480b f31603163f480b 7ae51edc003f480b c1f63499083f480b 87a311fc553f480b
To są rzeczywiste wartości szesnastkowe (minus precyzja ), które otrzymalibyśmy, gdybyśmy przekonwertowali datetime2 wartość do zmiennej . Aby mieć pewność, chodźmy od razu i zróbmy właśnie to:
SELECT CONVERT(VARBINARY(10), d0) AS 'd0', CONVERT(VARBINARY(10), d1) AS 'd1', CONVERT(VARBINARY(10), d2) AS 'd2', CONVERT(VARBINARY(10), d3) AS 'd3', CONVERT(VARBINARY(10), d4) AS 'd4', CONVERT(VARBINARY(10), d5) AS 'd5', CONVERT(VARBINARY(10), d6) AS 'd6', CONVERT(VARBINARY(10), d7) AS 'd7' FROM Datetime2Test;
Wynik (przy użyciu wyjścia pionowego):
d0 | 0x004290003F480B d1 | 0x0195A2053F480B d2 | 0x02D459383F480B d3 | 0x034B8233023F480B d4 | 0x04F31603163F480B d5 | 0x057AE51EDC003F480B d6 | 0x06C1F63499083F480B d7 | 0x0787A311FC553F480B
Otrzymujemy więc ten sam wynik – z tą różnicą, że został on poprzedzony precyzją.
Ale żeby wszystko było jasne, oto tabela, która porównuje rzeczywiste dane pliku strony z wynikami CONVERT()
operacja.
Dane pliku strony | CONVERT() Dane |
---|---|
4290003f480b | 004290003F480B |
95a2053f480b | 0195A2053F480B |
d459383f480b | 02D459383F480B |
4b8233023f480b | 034B8233023F480B |
f31603163f480b | 04F31603163F480B |
7ae51edc003f480b | 057AE51EDC003F480B |
c1f63499083f480b | 06C1F63499083F480B |
87a311fc553f480b | 0787A311FC553F480B |
Możemy więc wyraźnie zobaczyć, że plik strony nie przechowuje precyzji, ale przekonwertowany wynik tak.
Podkreśliłem rzeczywistą datę i godzinę na czerwono. Usunąłem również 0x
prefiks z przekonwertowanych wyników, dzięki czemu wyświetlane są tylko rzeczywiste dane daty/godziny (wraz z precyzją).
Pamiętaj też, że w systemie szesnastkowym wielkość liter nie jest rozróżniana, więc to, że jedna używa małych liter, a druga wielkich, nie stanowi problemu.
Wniosek
Konwertując data/godzinę2 wartość do zmiennej , potrzebuje dodatkowego bajtu do przechowywania precyzji. Potrzebuje precyzji, aby zinterpretować część czasu (ponieważ jest ona przechowywana jako przedział czasu, którego dokładna wartość będzie zależeć od precyzji).
W przypadku przechowywania w bazie danych precyzja jest określana raz na poziomie kolumny. Wydaje się to logiczne, ponieważ nie ma potrzeby przechowywania dodatkowego bajtu w każdym wierszu, jeśli można go określić na poziomie kolumny. Więc jeśli określisz powiedz, datetime2(7) na poziomie kolumny każdy wiersz będzie miał wartość datetime2(7) . Nie ma potrzeby powtarzania tego w każdym rzędzie.
Ronen Ariely doszedł do tego samego wniosku we wspomnianym powyżej artykule.
Jeśli masz milion wierszy z datetime2(7) wartości, przechowywanie precyzji w każdym wierszu wymagałoby 9 000 000 bajtów, w porównaniu do zaledwie 8 000 001, jeśli precyzja jest przechowywana raz dla całej kolumny.
To również wzmacnia datetime2 Przypadek w porównaniu z datetime . Nawet przy użyciu tej samej liczby miejsc dziesiętnych co data i godzina (tj. 3), datetime2 typ danych zajmuje mniej miejsca (przynajmniej w przypadku przechowywania w tabeli z więcej niż jednym wierszem). I robi to z większą dokładnością. data i godzina wartość używa 8 bajtów, podczas gdy datetime2(3) używa 7 bajtów (plus 1 „precyzyjny” bajt współdzielony we wszystkich wierszach).