Sqlserver
 sql >> Baza danych >  >> RDS >> Sqlserver

Zrozumienie rozmiaru pamięci „datetime2” w SQL Server

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ę
  • Dane przechowywane w bazie danych
    • Długość w bajtach przy użyciu COL_LENGTH()
    • Długość w bajtach przy użyciu DBCC PAGE()

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.K‚3.?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).


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Jak ustawić wartość zmiennej za pomocą „wykonaj” w t-sql?

  2. Jak zmienić nazwę kolumny lub nazwę tabeli w SQL Server — samouczek SQL Server / T-SQL, część 36

  3. Jak wstawić rekord z tylko wartościami domyślnymi?

  4. Znajdź jednostki odniesienia w SQL Server:sys.dm_sql_referenced_entities

  5. SSMS 2016 Błąd podczas importowania Azure SQL v12 bacpac:klucze główne bez hasła nie są obsługiwane