•
4 min read
UUID v7 for SQL Server Indexes: Still a Bad Idea

With the release of .NET 9, we finally got native UUID v7 support via Guid.CreateVersion7(). My first thought was: Great, this should finally make GUIDs sane for indexes in SQL Server. Well…

nah

What is the problem with GUIDs?

Generally, using random GUIDs (UUID v4) as clustered (or nonclustered) indexes in SQL Server is a terrible idea. SQL Server stores indexes as a sorted B-trees. When your key is completely random, every insert lands somewhere random too.

That means page splits, fragmentation, and plenty of extra I/O. Databases are fast, but once your table grows large and your insert rate climbs, this randomness quickly converts into a real performance problem.

b-tree

The general solution is straightforward: use sequentially generated GUIDs. Entity Framework has shipped SequentialGuidValueGenerator for years. It generates GUIDs that are still random, while ensuring each new value sorts after the previous one (at least on the local machine).

SQL Server also provides NEWSEQUENTIALID(), but its algorithm differs slightly.

UUID v7 is (practically) sequential

Based on the UUID specification, the text format is as follows where v is the version and w is the variant:

xxxxxxxx-xxxx-vxxx-wxxx-xxxxxxxxxxxx

Unlike UUID v4, where every x is random, UUID v7 stores the Unix timestamp (in milliseconds) in the first 12 x as hexadecimal digits. The remaining x are random. For example:

019ddfb1-8527-7b7e-93ea-6cefea2c3b90
019ddfb1-8527-7c34-b4fb-189eb2249446
019ddfb1-8527-7a5c-8627-af24f40e4b0f
019ddfb1-8527-79b4-ac9a-1be6cf8f58ef

Compared to UUID v4:

9836356a-2a23-4dd7-bdd3-7665633d1a4a
b85d8546-bcb0-4974-8239-d592b25af77c
dd2e0dbd-4f60-41ee-a102-c025e908afde
d4a45075-782f-4dd1-8a16-ec9fcd154153

At first glance, UUID v7 looks like exactly what we want. Sequential and standardized.

SQL Server sorting is… special

Let’s look at how EF Core’s SequentialGuidValueGenerator works. If we check the code carefully, It only replaces the last 8 bytes of the GUID with sequential data.

...
var guidBytes = MemoryMarshal.AsBytes(new Span<Guid>(ref guid));

guidBytes[09] = counterBytes[0];
guidBytes[08] = counterBytes[1];
guidBytes[10] = counterBytes[7];
guidBytes[11] = counterBytes[6];
guidBytes[12] = counterBytes[5];
guidBytes[13] = counterBytes[4];
guidBytes[14] = counterBytes[3];
guidBytes[15] = counterBytes[2];
...

In other words, only the last 16 x change:

ea53f1f2-398b-40a7-7656-08dea6e9ba6d
7389f15d-d6fd-4d73-7657-08dea6e9ba6d
eb341e7a-e01a-4271-7658-08dea6e9ba6d
21ea3533-9194-41c9-7659-08dea6e9ba6d
04702e13-abe5-401d-765a-08dea6e9ba6d

This looks almost backwards compared to UUID v7. That is because of how the SQL Server sort algorithm works for uniqueidentifier columns.

SQL Server sorts uniqueidentifier values differently than pretty much everyone else. Instead of starting from the first byte, it gives highest priority to the last bytes.

brain explosion

Based on. Ordering of uniqueidentifier in sql server:

we look at bytes {10 to 15} first, then {8-9}, then {6-7}, then {4-5}, and lastly {0 to 3}.

sql-order

That is due to some historical reason and tied to RFC 4122, In the UUID v1 the last 6 bytes are node id, and the node id was based on a machine’s MAC address so probably the sorting was designed to be most sensitive to the parts of the GUID that changed the least frequently or likely to group GUIDs by originating machine.

Why UUID v7 is a poor index key in SQL Server

UUID v7 places its sequential timestamp in the first six bytes. Because of how SQL Server sorts uniqueidentifier values, UUID v7 behaves almost like UUID v4 in a SQL Server index. You still end up with random inserts, page splits, and fragmentation.

In our case, this simple change turned a production insert operation from 20 minutes into hours and created a fun afternoon for a colleague, who got to replace every CreateVersion7() call in the codebase.