How SQL Server Packs BIT Columns Into a Single Byte

A common worry about bit columns is that a table full of yes/no flags will waste a byte on every flag. It will not. SQL Server packs bit columns together, up to eight of them sharing a single byte, and the catalog view that reports each bit as “1 byte” hides that fact rather than revealing it. This is a short, satisfying one to see on the page: we will store ten bit columns, watch eight of them collapse into one byte, and watch the ninth open a second.
This continues the byte-level storage series alongside the NULL bitmap, decimal, and where each column lives in a record. If DBCC PAGE is new to you, this older post covers the mechanics. Every byte below is from a SQL Server 2019 instance, and the script is included so you can reproduce it. Randolph West wrote his own excellent post on how SQL Server stores bit columns over at bornsql.ca, and it is well worth reading alongside this one.
What the documentation promises
Microsoft is direct about it: “If there are 8 or fewer bit columns in a table, the columns are stored as 1 byte. If there are from 9 up to 16 bit columns, the columns are stored as 2 bytes, and so on.”[1] In other words, each bit column is a single bit in a shared byte, and the byte count is CEILING(bit_columns / 8). Let us confirm it, because the catalog does not.
Ten bit columns, one row
An int key and ten bit columns, with an alternating pattern in the first eight and both of the last two set:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
CREATE TABLE [dbo].[bit_demo] ( [id] int NOT NULL , [b1] bit NOT NULL, [b2] bit NOT NULL, [b3] bit NOT NULL, [b4] bit NOT NULL , [b5] bit NOT NULL, [b6] bit NOT NULL, [b7] bit NOT NULL, [b8] bit NOT NULL , [b9] bit NOT NULL, [b10] bit NOT NULL ); INSERT INTO [dbo].[bit_demo] ( [id], [b1],[b2],[b3],[b4],[b5],[b6],[b7],[b8],[b9],[b10] ) VALUES ( 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1 ); |
First, notice what the catalog says. Every bit column reports a max_length of 1:
|
1 2 3 4 5 6 7 |
column_name max_length ----------- ---------- id 4 b1 1 b2 1 ... b10 1 |
Ten columns at “1” each looks like ten bytes. It is not. The max_length is a logical figure; the physical packing is another matter entirely.
The packing on the page
Here is the record from DBCC PAGE:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
Slot 0 Offset 0x60 Length 14 Record Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP Record Size = 14 0000000000000000: 10000a00 01000000 554f0b00 0000 Slot 0 Column 2 Offset 0x8 Length 1 (Bit position 0) b1 = 1 Slot 0 Column 3 Offset 0x8 Length 1 (Bit position 1) b2 = 0 Slot 0 Column 4 Offset 0x8 Length 1 (Bit position 2) b3 = 1 ... Slot 0 Column 9 Offset 0x8 Length 1 (Bit position 7) b8 = 0 Slot 0 Column 10 Offset 0x9 Length 1 (Bit position 0) b9 = 1 Slot 0 Column 11 Offset 0x9 Length 1 (Bit position 1) b10 = 1 |
Look at the offsets. Columns b1 through b8 all report Offset 0x8, the same byte, differentiated only by “Bit position 0” through “Bit position 7”. Eight columns, one byte. Then b9 and b10 jump to Offset 0x9, the next byte, at bit positions 0 and 1. The ninth bit column is what opened the second byte, exactly as the documentation says.
Now read the bytes. After the four-byte id (01 00 00 00) comes 55 at offset 8. In binary 0x55 is 0101 0101, and reading it from bit 0 upward gives 1, 0, 1, 0, 1, 0, 1, 0, which is precisely the pattern we inserted into b1 through b8. The byte at offset 9 is where b9 and b10 live, only its lowest two bits carrying meaning. Ten Boolean values, two bytes.
What this means in practice
- Flags are cheap; do not pack them by hand. There is no storage reason to combine several yes/no attributes into a single integer with bit-masking. SQL Server already stores eight
bitcolumns in the space of one, and separatebitcolumns are far clearer to query thanflags & 4 = 4. - The savings step in eights. Columns 1 through 8 are free after the first byte; the 9th costs a second byte that then covers columns 9 through 16. If you are counting bytes on an enormous table, the boundary to watch is every multiple of eight.
- A nullable bit still needs the NULL bitmap. The packed byte records the true/false value; whether the column is
NULLis tracked separately in the NULL bitmap, like every other column. - Column order does not change the packing. SQL Server groups the
bitcolumns together regardless of where they sit in yourCREATE TABLE. You do not need to cluster them in the definition to get the byte-sharing.
It is a small, tidy optimization, and one of the few places where the physical storage is more generous than the logical description suggests. Ten flags really do cost two bytes.
Have you ever hand-rolled a bit-masked integer to “save space” that bit columns would have saved for you? I would like to hear it. Find me on Bluesky or LinkedIn.
References
- bit (Transact-SQL) – Microsoft Learn. The storage optimization: 8 or fewer bit columns in 1 byte, 9 to 16 in 2 bytes, and so on. ↩