Sqlserver
 sql >> Database >  >> RDS >> Sqlserver

Comprensione delle dimensioni di archiviazione "datetime2" in SQL Server

In questo articolo condivido alcune osservazioni che ho avuto riguardo a datetime2 dimensione di archiviazione del tipo di dati in SQL Server. Forse chiarirò alcuni punti sulla dimensione di archiviazione effettiva utilizzata da questo tipo di dati quando archiviato in un database.

In particolare, guardo quanto segue:

  • Documentazione di Microsoft
  • Dati memorizzati in una variabile
    • Lunghezza in byte utilizzando DATALENGTH()
    • Lunghezza in byte utilizzando DATALENGTH() dopo la conversione in varbinary
  • Dati archiviati in un database
    • Lunghezza in byte utilizzando COL_LENGTH()
    • Lunghezza in byte utilizzando DBCC PAGE()

Alcuni di questi sembrano contraddirsi a vicenda e vedrai due quantità di spazio di archiviazione diverse per lo stesso valore, a seconda di dove guardi.

Un dataora2 value può mostrare una dimensione di archiviazione diversa, a seconda che sia archiviato in un database, come datetime2 variabile o convertito in varbinary .

Ma c'è una spiegazione plausibile per questo:dipende da dove si trova la precisione è in fase di archiviazione.

Durante la mia ricerca su questo problema, ho trovato l'articolo approfondito di Ronen Ariely su come datetime2 è memorizzato nel file di dati in modo molto informativo e mi ha chiesto di eseguire alcuni test simili nel mio ambiente di sviluppo e di presentarli qui.

Documentazione Microsoft

Per prima cosa, diamo un'occhiata a cosa dice la documentazione ufficiale.

La documentazione di Microsoft su datetime2 tipo di dati afferma che la sua dimensione di archiviazione è la seguente:

6 byte per una precisione inferiore a 3.
7 byte per una precisione 3 o 4.
Tutte le altre precisioni richiedono 8 byte.

Ma qualifica la tabella sopra con la seguente affermazione:

Il primo byte di un datetime2 value memorizza la precisione del valore, il che significa lo spazio di archiviazione effettivo richiesto per un datetime2 value è la dimensione di archiviazione indicata nella tabella sopra più 1 byte aggiuntivo per memorizzare la precisione. Ciò rende la dimensione massima di un datetime2 valore 9 byte – 1 byte memorizza la precisione più 8 byte per la memorizzazione dei dati alla massima precisione.

Quindi, date le informazioni di cui sopra, la conclusione ovvia da trarre sarebbe che la tabella potrebbe/(dovrebbe?) essere scritta come segue:

7 byte per una precisione inferiore a 3.
8 byte per una precisione 3 o 4.
Tutte le altre precisioni richiedono 9 byte.

In questo modo, non avrebbero bisogno di qualificarlo con le informazioni extra sulla precisione.

Ma non è così semplice.

Dati archiviati in una variabile

Innanzitutto, memorizziamo un datetime2 valore in una variabile e verificarne la dimensione di archiviazione. Quindi convertirò quel valore in varbinary e controllalo di nuovo.

Lunghezza in byte utilizzando DATALENGTH

Ecco cosa succede se utilizziamo DATALENGTH() funzione per restituire il numero di byte utilizzati per un datetime2(7) valore:

DECLARE @d datetime2(7);
SET @d = '2025-05-21 10:15:30.1234567';
SELECT 
  @d AS 'Value',
  DATALENGTH(@d) AS 'Length in Bytes';

Risultato

+-----------------------------+-------------------+
| Value                       | Length in Bytes   |
|-----------------------------+-------------------|
| 2025-05-21 10:15:30.1234567 | 8                 |
+-----------------------------+-------------------+

Il valore in questo esempio ha la scala massima di 7 (perché dichiaro la variabile come datetime2(7) ), e restituisce una lunghezza di 8 byte.

Questo sembra contraddire ciò che Microsoft afferma sulla necessità di un byte aggiuntivo per memorizzare la precisione. Per citare Microsoft, Questo rende la dimensione massima di un datetime2 valore 9 byte – 1 byte memorizza la precisione più 8 byte per l'archiviazione dei dati alla massima precisione. .

Anche se è vero che sembra che otteniamo 8 byte per l'archiviazione dei dati , sembra che manchi 1 byte utilizzato per memorizzare la precisione.

Tuttavia, se convertiamo il valore in varbinary abbiamo una storia diversa.

Lunghezza in byte dopo la conversione in 'varbinary'

Ecco cosa succede se convertiamo il nostro datetime2 valore a variabile :

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';

Risultato

+----------------------+-------------------+
| Value                | Length in Bytes   |
|----------------------+-------------------|
| 0x0787A311FC553F480B | 9                 |
+----------------------+-------------------+

In questo caso otteniamo 9 byte.

Questa è una rappresentazione esadecimale di datetime2 valore. Il valore della data e dell'ora effettivo (e la sua precisione) è tutto dopo il 0x . Ogni coppia di caratteri esadecimali è un byte. Ci sono 9 coppie, e quindi 9 byte. Ciò è confermato quando utilizziamo DATALENGTH() per restituire la lunghezza in byte.

In questo esempio possiamo vedere che il primo byte è 07 . Questo rappresenta la precisione (ho usato una scala di 7 e quindi questo è ciò che viene visualizzato qui).

Se cambio la scala, possiamo vedere che il primo byte cambia per corrispondere alla scala:

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';

Risultato

+--------------------+-------------------+
| Value              | Length in Bytes   |
|--------------------+-------------------|
| 0x034B8233023F480B | 8                 |
+--------------------+-------------------+

Possiamo anche vedere che la lunghezza si riduce di conseguenza.

Quindi in questo caso i nostri risultati corrispondono perfettamente alla documentazione Microsoft:è stato aggiunto un byte in più per la precisione.

Molti sviluppatori presumono che questo sia il modo in cui SQL Server archivia il suo datetime2 valori nel database. Tuttavia, tale ipotesi sembra non essere corretta.

Dati archiviati in un database

In questo esempio creo un database che contiene una tabella con vari datetime2(n) colonne. Quindi uso COL_LENGTH() per restituire la lunghezza di ciascuna colonna, in byte. Successivamente, inserisco i valori al suo interno, prima di utilizzare DBCC PAGE per controllare la dimensione di archiviazione di ogni datetime2 il valore occupa il file di paging.

Crea un database:

CREATE DATABASE Test;

Crea una tabella:

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)
    );

In questo caso creo otto colonne, una per ogni scala definita dall'utente che possiamo utilizzare con datetime2(n) .

Ora possiamo controllare la dimensione di archiviazione di ciascuna colonna.

Lunghezza in byte utilizzando COL_LENGTH()

Usa COL_LENGTH() per controllare la lunghezza (in byte) di ogni colonna:

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';  

Risultato:

+------+------+------+------+------+------+------+------+
| d0   | d1   | d2   | d3   | d4   | d5   | d6   | d7   |
|------+------+------+------+------+------+------+------|
| 6    | 6    | 6    | 7    | 7    | 8    | 8    | 8    |
+------+------+------+------+------+------+------+------+

Quindi, ancora una volta, non sembra che otteniamo il byte aggiuntivo utilizzato per memorizzare la precisione.

Utilizza DBCC PAGE per controllare i dati archiviati

Ora usiamo DBCC PAGE per trovare la dimensione di archiviazione effettiva dei dati che memorizziamo in questa tabella.

Per prima cosa, inseriamo alcuni dati:

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;

Ora seleziona i dati (solo per verificarli):

SELECT * FROM Datetime2Test;

Risultato (usando l'output verticale):

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

Come previsto, i valori utilizzano la precisione precedentemente specificata a livello di colonna.

Ora, prima di usare DBCC PAGE() , abbiamo bisogno di sapere quale PagePID passargli. Possiamo usare DBCC IND() per trovarlo.

Trova il PagePID:

DBCC IND('Test', 'dbo.Datetime2Test', 0);

Risultato (usando l'output verticale):

-[ 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

Questo restituisce due record. Siamo interessati al PageType di 1 (il 2° record). Vogliamo il PagePID da quel record. In questo caso il PagePID è 360 .

Ora possiamo prendere quel PagePID e usarlo nel modo seguente:

DBCC TRACEON(3604, -1);
DBCC PAGE(Test, 1, 360, 3);

Questo produce molti dati, ma siamo principalmente interessati alla parte seguente:

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                                   

Quindi sembra che non utilizzi il byte aggiuntivo per la precisione.

Ma esaminiamo i dati effettivi prima di giungere a qualsiasi conclusione.

I dati effettivi sono memorizzati in questa parte del file di paging:

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                                        ...                                           ...   

Come puoi vedere, niente di tutto ciò assomiglia ai risultati che otterremmo convertendo il datetime2 valore a variabile . Ma è abbastanza vicino.

Ecco come appare se elimino alcune cose:

4290003f 480b95a2 053f480b d459383f
480b4b82 33023f48 0bf31603 163f480b 7ae51edc
003f480b c1f63499 083f480b 87a311fc 553f480b

Le restanti cifre esadecimali contengono tutti i nostri dati di data e ora, ma ​​non la precisione . Tuttavia, dobbiamo riorganizzare gli spazi per ottenere i valori effettivi per ogni riga.

Ecco il risultato finale. Ho inserito ogni valore di data/ora su una nuova riga per una migliore leggibilità.

4290003f480b 
95a2053f480b 
d459383f480b 
4b8233023f480b
f31603163f480b 
7ae51edc003f480b 
c1f63499083f480b 
87a311fc553f480b

Questi sono i valori esadecimali effettivi (meno la precisione ) che otterremmo se convertissimo il datetime2 valore a variabile . Per essere sicuri, andiamo avanti e facciamo proprio questo:

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;

Risultato (usando l'output verticale):

d0 | 0x004290003F480B
d1 | 0x0195A2053F480B
d2 | 0x02D459383F480B
d3 | 0x034B8233023F480B
d4 | 0x04F31603163F480B
d5 | 0x057AE51EDC003F480B
d6 | 0x06C1F63499083F480B
d7 | 0x0787A311FC553F480B

Quindi otteniamo lo stesso risultato, tranne per il fatto che è stato anteposto con la precisione.

Ma per chiarire le cose, ecco una tabella che confronta i dati effettivi del file di paging con i risultati di CONVERT() operazione.

Dati del file di pagina Dati CONVERT()
4290003f480b 004290003F480B
95a2053f480b 0195A2053F480B
d459383f480b 02D459383F480B
4b8233023f480b 034B8233023F480B
f31603163f480b 04F31603163F480B
7ae51edc003f480b 057AE51EDC003F480B
c1f63499083f480b 06C1F63499083F480B
87a311fc553f480b 0787A311FC553F480B

Quindi possiamo vedere chiaramente che il file di paging non memorizza la precisione, ma il risultato convertito lo fa.

Ho evidenziato la data e l'ora effettive in rosso. Ho anche rimosso 0x prefisso dai risultati convertiti, in modo che vengano visualizzati solo i dati di data/ora effettivi (insieme alla precisione).

Tieni inoltre presente che l'esadecimale non fa distinzione tra maiuscole e minuscole, quindi il fatto che uno utilizzi le minuscole e l'altro le maiuscole non è un problema.

Conclusione

Quando si converte un datetime2 valore a variabile , ha bisogno di un byte aggiuntivo per memorizzare la precisione. Ha bisogno della precisione per interpretare la porzione di tempo (perché questa è memorizzata come un intervallo di tempo, il cui valore esatto dipenderà dalla precisione).

Quando viene archiviata in un database, la precisione viene specificata una volta a livello di colonna. Sembrerebbe logico, poiché non è necessario memorizzare un byte aggiuntivo con ogni riga se può essere specificato a livello di colonna. Quindi, se specifichi, ad esempio, datetime2(7) a livello di colonna, ogni singola riga sarà datetime2(7) . Non c'è bisogno di ribadirlo su ogni riga.

Ronen Ariely è giunto alla stessa conclusione nel suo articolo sopra menzionato.

Se hai un milione di righe con datetime2(7) valori, la memorizzazione della precisione con ogni riga richiederebbe 9.000.000 di byte, rispetto ai soli 8.000.001 se la precisione viene archiviata una volta per l'intera colonna.

Questo rafforza anche il datetime2 è il caso quando lo confronti con datetime . Anche utilizzando lo stesso numero di cifre decimali di datetime (ovvero 3), il datetime2 il tipo di dati utilizza meno spazio di archiviazione (almeno se archiviato in una tabella con più di una riga). E lo fa con maggiore precisione. Un data e ora value utilizza 8 byte, mentre datetime2(3) utilizza 7 byte (più 1 byte di "precisione" condiviso su tutte le righe).