Recently while talking about the SQL Server Storage engine at one of the customer workshops, I was asked about how data is actually stored in the tables and indexes. With the exception of Column Store Indexes (*introduced in SQL 2012), data in SQL Server is stored in rows.
A data row in SQL Server can be stored in one of 3 formats, depending on what SQL Server feature is enabled in the database.
- Regular Data Record: The regular Data row structure which have been in use since the early days of SQL Server.
- Compression Row Structure : When Row/Page compression is enabled on the Table.
- Sparse Column Row Structure: When Sparse Columns are defined in the table.
Index Records are in two formats
- Leaf record structure
- Non-Leaf record structure
Additionally SQL Server has a separate type of record structure to store LOB data. LOB data can either be stored as
- Off Row Data
- Row-overflow data
In addition to the above mentioned data structures, SQL also has a Versioning Records (for all data, Index, text types) when row versioning is enabled or as used by some internal SQL features.
In the first part of the blog series I would be talking about the regular data row structure as has been used since the early days of SQL Server, the other record structures for compression and or sparse columns would be discussed in later posts. The Data records is part of a Heap Table (tables with no clustered index) or the leaf level of a clustered index. A Data Record is composed from the values of all the columns in the table. Another important point to keep in mind is that in SQL Server, the terms Row/Record/Slots are all synonymous and can be used interchangeably.
The record can be divided into 8 sections as depicted in the picture above.
- 2 Bytes Record Status (Record header) : Currently only 9 bits of this is being used. The header contains information about what kind of record it is. Whether it is versioned or ghosted record and so on. For more information on these 2 bytes refer, Microsoft® SQL Server® 2008 Internals by Kalen Delaney, Paul S. Randal, Kimberly L. Tripp , Conor Cunningham , Adam Machanic
- The second 2 bytes (Length of Fixed Length Columns) indicate the total length of fixed length columns (int, date, float, money char etc.)
- The 3rd section of N bytes is the actual storage for the Fixed length columns. Yes, you got it right, SQL Server stores all the fixed length columns together (irrespective of where they are defined in the table).
- 2 Bytes for Number of Columns – 2 bytes are used to store the number of columns in the record. In the default scenario (no sparse columns) a table in SQL 2008 can have up to 1024 column, which means at least 10 bits would be required to store this information.
- Null Bitmap (1 bit per column in the table): 1 bit would be used per column, to indicate whether the column can have a null value or not. This is done for all the columns in the table.
- 2 bytes – Number of variable length columns in the table: A count of the variable length columns in the table.
- Variable length Column offset (2 bytes per variable length column): This offset stores the ending offset for each variable length column in the table. This allows SQL Server to efficiently calculate the start,end and length of each variable length columns. 2 bytes is required because the column offset can be anywhere on the 8KB page.
- N bytes – For the variable length columns storage: Storage for the column values.
To understand this further, lets take few examples. In all the examples below, the following DBCC PAGE/DBCC TRACEON commands would be used to display the page/record structure.
Example 1: Table With All fixed length columns
Consider a SQL Table with the following structure
create table FixedLenghtColumns
-- Insert a record into the table
Insert into FixedLenghtColumns values (1, 1.0001, getdate(), 'Fixed Length Columns')
-- DBCC IND Command to display the allocations for this table
DBCC IND('Database Name','FixedLenghtColumns',-1)
-- DBCC PAGE Command to display the Page structure
DBCC TRACEON (3604,-1)
DBCC PAGE (10, 1, 276, 3)
Lets look at the page and record structure from the DBCC Page Output.
The fields marked in Red boxes are important.
pminlen – Indicates the minimum length of a record in the table. This table has 4 columns with a total length of 45 bytes. Add to it the Row header and 2 bytes for the length of the fixed length columns.
m_slotCnt: This field indicate the number of slots or records on the page.
Record Size/Record Attributes: This indicate the actual size of the this record and what are the attributes that this record has. For example this record contains a NULL_BITMAP field.
The “Slot 0 Offset 0x60 Length 52” section indicate that this is the first record on the page, and the record starts at offset 0x60 (decimal 96). Which is valid since the Page Header occupies the first 96 bytes of a SQL Server Page. Please note all offsets start at 0.
Now lets see why the record occupies 52 bytes. The first 4 bytes of the record are fixed for the record header and the length of fixed length columns. Next the total length of the fixed length columns is 45 bytes. Then we need 2 bytes for the number of columns and 1 byte for the Null Bitmap (4 bits for the four columns, but since it all byte allocation, we need 1 byte). This is visible in the record details we see in the DBCC PAGE output.
As can be seen, the first column value starts at offset 0x4 (decimal-4). Which is after the first 4 bytes for the Row header and the length of the fixed length columns. The other column values follow the similar pattern.
Example 2: Tables with mix of variable and fixed length columns
Lets consider another example, where the table contains a mix of fixed and variable length data types.
CREATE TABLE MixDataTypeColumns
Col1 int NOT NULL,
Col2 char(25) NOT NULL,
Col3 varchar(60) NULL,
Col4 money NOT NULL,
Col5 varchar(20) NOT NULL
Insert into MixDataTypeColumns values (10, 'Sourabh Kumar Agarwal', 'This is a test', 3764.846, 'last column')
The output of the DBCC PAGE indicates the following…
The Record length here is 75 which can be summed up as follows
2 bytes Record Header+ 2 bytes for the length of fixed length data types columns + 37 bytes for the fixed length data types columns + 2 Bytes of No of Columns + 1 bytes (*5 bits) for NULL_BITMAP + 2 bytes for number of Variable length data type columns + 2*2 bytes for the Variable length column offset + 25 bytes used for the variable length column values.
Also as visible from the column offset information from the snapshot above, we can see that the fixed length columns are stored first (notice the column offsets) and then the variable length columns are stored, Which is
- Col1 (first fixed length Column) – 0x4 – decimal 8
- Col2 (second fixed length Column) – 0x8 – decimal 8
- Col4 (last fixed length Column) – 0x21 – decimal 33
- Col3 (first variable length Column) – 0x32 – decimal 50
- Col5 (first fixed length Column) – 0x40 – decimal 64
In the next post, I will talk about 3 special cases involving versioning, ghost records and row forwarding.