This C# project implements UUID v7 as described in Peabody and Davis. This is the latest IETF draft for UUIDs that are time-sortable and have a random component to guarantee uniqueness.
It is available on NuGet as UuidExtensions.
The internal structure of the UUID uses:
- 36 bits to represent the number of whole seconds since 1 January 1970.
- 24 bits to represent the fractional number of seconds, giving a resolution of up to 50ns.
- 14 bits for a sequence number, in case of multiple uuids being generated
with the same timestamp instant.
- Needed in cases when the machine's physical clock tick resolution is a lot worse than the 50ns storage resolution.
- Also needed when generating multiple uuids as of a fixed timestamp in the past.
- 48 bits of randomness.
- 6 "version" and "variant" bits to indicate this is a UUID v7 rather than v4 or one of the other UUID formats.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix ts (32 + 4 = 36 bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|unix ts| msec (12 bits) | ver | usec (12 bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| seq (14 bits) | rand (16 bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| rand (32 bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Here is a pair of Uuid7s, one as a Guid and the other as a string, illustrating how their string representations are time-ordered:
var uuid7 = new Uuid7();
Guid g1 = uuid7.Guid(); // e.g. 06338364-8305-788f-8000-9ada942eb663
string s2 = uuid7.String(); // 06338364-8305-7b74-8000-de4963503139
Assert.IsTrue(String.Compare(g1.ToString(), s2) < 0);Note that internally, Microsoft stores Guids using big endian for the first three
components and little endian for the last two. So UUID v7 Guids
are only time-ordered using guid.ToString(), not using guid.ToByteArray().
This package also supports an alternative string representation that I call Id25.
The goal here is to represent UUID v7 as the shortest possible
alphanumeric string that is simple for humans to say and use. That is:
- It uses a single case not mixed case. We arbitrarily pick lower case.
- No punctuation. Thus a single double-click in a web page selects the whole id. There are also no complications for joining/splitting them if used in compound ids.
- If the whole alphabet
a-zis not needed, drop firstland thenosince these are the letters most easily confused with digits1and0.
To see why Id25 must be 25 characters long, UUIDs have 128 bits.
An alphabet of 36 characters (0-9, a-z) would need a string representation
l, resulting in the 35-character alphabet 0-9, a-k, m-z.
Finally, note this alphabet must put the digits before the letters so that the strings come out properly time-ordered.
var uuid7 = new Uuid7();
string s1 = uuid7.Id25(); // e.g. 0q994uri6sp53j3eu7bty4wsv
string s2 = uuid7.Id25(); // 0q994uri70qe0gjxrq8iv4iyu
Assert.IsTrue(String.Compare(s1, s2) < 0);The timestamp may be specified explicitly by passing in the time expressed as the whole number of nanoseconds since 00:00:00 on 1 January 1970 UTC. Here are two Id25 strings nominally 1ns apart, and a third at the same timestamp as the second.
var uuid7 = new Uuid7();
long t1 = Uuid7.TimeNs();
long t2 = t1 + 1;
string s1 = uuid7.Id25(t1); // e.g. 0q996kioxxyfds1stmjqajen6
string s2 = uuid7.Id25(t2); // 0q996kioxxyfj83w8bqp67d2j
string s3 = uuid7.Id25(t2); // 0q996kioxxyfj83z4pmujhrx4
Assert.IsTrue(String.Compare(s1, s2) < 0);
Assert.IsTrue(String.Compare(s2, s3) < 0);Note that the second and the third have been generated from the same
Uuid7 object. Its internal sequence counter ensures successive
UUIDv7/Id25 values remain time-ordered even if they have the
same timestamp.
The sequence counter resets when the timestamp changes.
In this final example, we cannot tell whether s1 > s3 or not.
That is determined by the lower 36 random bits of the two UUIDs.
string s1 = uuid7.Id25(t1);
string s2 = uuid7.Id25(t2);
string s3 = uuid7.Id25(t1);A simple console app prints out the current timestamp's Uuid7 in both UUID and Id25 forms. It also does a simple timing check to calculate the underlying clock precision.
> dotnet run
0634194d-3e69-72a4-8000-d602a09b0e94 0q9khv3tqz7e3jfv1v0i42yz0
Precision is 2852ns rather than 1863ns (653 unique timestamps from 1,000 loops taking 1.8626ms)