Skip to content

Create Sortable GUIDs With UUID Version 7 in .NET

GUIDs are great for generating unique identifiers across distributed systems, since they need no centralised coordination and are globally unique. But their randomness comes with a price tag when we use them in relational databases: fragmented indexes, slower inserts, and no sense of order.

Since .NET 9 we get an out of the box solution that fixes this problem: UUID version 7. We can keep the upsides of a GUID while we can get rid of the downsides - by just using a different method to create our identifiers. Let us explore how that works.

UUID version 4 as we know it

When we use Guid.NewGuid() in our application, we create a Universally Unique Identifier (UUID) in version 4. If we create 10 identifiers, we can see that they are completely random and we have no idea if they are created in sequence or if months passed between their creation:

1
2
3
4
Console.WriteLine("UUID Version 4");
for(int i = 0; i < 10; i++)
    Console.WriteLine(Guid.NewGuid());
    Thread.Sleep(100);
UUID Version 4
e42f6305-2e74-4ea1-bd9b-34778ed9fbc2
9e60ebc9-7102-42e4-b8a7-00c8ed65df68
80913969-57f4-427b-8166-871f1d7b9672
9842b819-4f00-4a1d-9872-af3153b6ed1f
6faade63-5e93-49ed-93fb-b0b886418328
159d5d61-a6f8-4dd5-b73e-4dec7265e654
14bb0401-a6fa-4df9-aaca-652ec858c225
434fd7f5-99aa-42fe-8ea2-711329edff17
2fbd33b8-35bf-4c43-b528-9551f8aad839
b1801ef2-3b0f-483b-a6d0-3a3193c942e4

This randomness makes it slow to insert the objects into a database where we use a clustered index for the primary key. The first entry goes into one block; the next entry may need to go at the other end of the index while the third one may be somewhere in between. That requires a lot of jumping around while inserting our data. The more data you need to insert at once, the more you will feel the pain.

UUID version 7 offers a timestamp

In UUID version 7 the identifier we create combines a Unix timestamp with random bits, producing UUIDs that are time-sortable and still unique. We end up with a globally unique identifier that is sortable and can tell us when it was created. Even better, since .Net 9 we can use the same GUID class and only switch to the Guid.CreateVersion7() method:

1
2
3
4
Console.WriteLine("UUID Version 7");
for (int i = 0; i < 10; i++)
    Console.WriteLine(Guid.CreateVersion7());
    Thread.Sleep(100);
UUID Version 7
019a1aba-e98d-7f96-834e-b9943f6a76ac
019a1aba-e98d-72c8-8ac6-34e4a980ecff
019a1aba-e98d-7f27-8c27-fe8f0d53c8f4
019a1aba-e98d-77c9-ac54-7230027ade07
019a1aba-e98d-7a80-8ef1-6084c021effd
019a1aba-e98d-73a4-950e-56e5c7640a5f
019a1aba-e98d-74b4-ad7b-f05ca12e2902
019a1aba-e98d-71b0-a755-e1500e34414c
019a1aba-e98d-7863-bf80-95631cce694d
019a1aba-e98d-76d5-a1b0-2afc0252de66

We see that the 10 generated identifiers were created closely together. While we have less bytes for the random number, we still have enough to create more numbers than we ever going to need.

Since we now can create the identifiers in a sortable sequence, we can insert them faster into our database, while still be able to create them in a distributed manner.

Create version 7 GUIDs for a specific date

If we do not specify a specific date, Guid.CreateVersion7() uses the current time to create the identifier. If we want to change that, we can use the method overload that accepts a DateTimeOffset object:

1
2
3
Console.WriteLine("\n\nSpecific date");
var lastYear = DateTime.Now;
Console.WriteLine(Guid.CreateVersion7(new DateTimeOffset(lastYear)));
Specific date
0193358b-4d30-762b-b7e8-342f2055b72a

Extract the date from the identifier

Should we be interested in the date part of the version 7 identifier, we can use a code snipped like this here to extract it:

public DateTimeOffset ExtractDate(Guid uuid)
{
    // Get the UUID bytes in big-endian order
    var bytes = uuid.ToByteArray();

    // Convert to big-endian layout (UUID is stored in mixed-endian in .NET)
    var reordered = new byte[16];
    reordered[0] = bytes[3];
    reordered[1] = bytes[2];
    reordered[2] = bytes[1];
    reordered[3] = bytes[0];
    reordered[4] = bytes[5];
    reordered[5] = bytes[4];
    reordered[6] = bytes[7];
    reordered[7] = bytes[6];
    Array.Copy(bytes, 8, reordered, 8, 8);

    // Extract the first 6 bytes (48 bits) as the timestamp in milliseconds
    ulong timestamp = 0;
    for (int i = 0; i < 6; i++)
    {
        timestamp = (timestamp << 8) | reordered[i];
    }

    // Convert Unix milliseconds to DateTime (UTC)
    var dateTime = DateTimeOffset.FromUnixTimeMilliseconds((long)timestamp).UtcDateTime;
    return dateTime;
}

...
var id = Guid.CreateVersion7(new DateTimeOffset(2025, 11, 16, 16, 15, 42));
Console.WriteLine(ExtractDate(id));
16.11.2025 15:15:42 +00:00

This gives us the date and time in UTC from our identifier.

Should we switch?

The GUID class creates the same system.Guid type with both methods. We only need to change the way we create the GUID, the rest of the application can stay the same. Everywhere where we used a version 4 GUID can we use a version 7 GUID.

There is only one case where we need to be careful. If we used the randomness of version 4 to shard our data, we need to find a new way to distribute the entries between the shards. Since Version 7 creates its identifiers in a sortable order, they are no longer distributed over the whole range of possible numbers

The code change is done quickly, changing the existing data not so much. Changing primary keys is messy and a lot of work, but when you want to profit from the sequential starting part of the identifiers to sort them by date, then that may be something worth considering. Doing that would be the topic of another post.

Conclusion

Starting with .NET 9 we get an easy way to create UUID version 7 identifiers that contain a timestamp and thus bring order in the chaos of GUID identifiers. Since the data type stays the same, we can switch the identifier creation and keep the rest of our application the same. That makes it a good choice for all new applications that need to create their identifiers in a distributed way.