Last active
June 16, 2025 20:41
-
-
Save danielmarschall/7fafd270a3bc107d38e8449ce7420c25 to your computer and use it in GitHub Desktop.
SQL Server sortable UUID in C# (Version 3)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Threading; | |
| namespace GenerateSqlServerSortableTimeBasedGuid_App | |
| { | |
| internal class Program | |
| { | |
| static readonly Random random = new Random(); | |
| /** | |
| * The sorting of SQL Server is rather confusing and incompatible with UUIDv6 and UUIDv7. | |
| * Therefore this method generates UUID which are sortable by SQL Server. | |
| * Version 1: Resolution of 1 milliseconds, random part of 16 bits, local timezone, 48 zero bits "signature", *NOT* UUIDv8 compliant. | |
| * Version 2: DO NOT USE. Same as version 3, but wrong order due to a bug (block 2 and 3 accidentally used Big Endian instead of Little Endian). | |
| * Version 3: Resolution of 1 milliseconds, random part of 16 bits, UTC time, 48 bit random "signature", UUIDv8 compliant. | |
| * C# implementation: https://gist.github.com/danielmarschall/7fafd270a3bc107d38e8449ce7420c25 | |
| * PHP implementation: https://github.com/danielmarschall/uuid_mac_utils/blob/master/includes/uuid_utils.inc.php | |
| */ | |
| public static Guid GenerateSqlServerSortableTimeBasedGuid(int hickelUuidVersion = 3) | |
| { | |
| string block1, block2, block3, block4, block5; | |
| DateTime dt; | |
| // The sorting in SQL Server is like this: | |
| // First Sort block 5, Big Endian | |
| if (hickelUuidVersion == 1) | |
| { | |
| block5 = "000000000000"; | |
| } | |
| else if (hickelUuidVersion == 2) | |
| { | |
| block5 = "5ce32bd83b96"; // UUIDv8 "Signature" | |
| } | |
| else if (hickelUuidVersion == 3) | |
| { | |
| block5 = "5ce32bd83b97"; // UUIDv8 "Signature" | |
| } | |
| else | |
| { | |
| throw new Exception("Invalid version"); | |
| } | |
| // Then: Sort block 4, Big Endian | |
| if (hickelUuidVersion == 1) | |
| { | |
| dt = DateTime.Now; | |
| block4 = String.Format("{0:yy}{1:D2}", dt, (int)Math.Floor((decimal)dt.Year / 100)); // Example: 0x2420 = 2024 | |
| } | |
| else | |
| { | |
| dt = DateTime.UtcNow; | |
| int variant = 0x8; // First nibble needs to be 0b10__ (0x8-0xB) for RFC 9562. | |
| int unused2bits = 0; // The two more bits CANNOT be used for random bits, because it would affect the sorting | |
| block4 = String.Format("{0:X1}{1:X3}", variant + (unused2bits & 0x3), dt.Year); | |
| } | |
| // Then: Sort block 3, Little Endian | |
| if (hickelUuidVersion == 1) | |
| { | |
| block3 = String.Format("{0:ddMM}", dt); // Little Endian is correct here | |
| } | |
| else | |
| { | |
| string ver = String.Format("{0:X1}", 8); // First nibble needs to be "8" for "UUIDv8 = Custom UUID" | |
| string doy = String.Format("{0:X3}", dt.DayOfYear/*1..366*/); | |
| if (hickelUuidVersion == 2) | |
| { | |
| block3 = ver + doy; // This is wrong! (Big Endian instead of Little Endian) | |
| } | |
| else | |
| { | |
| block3 = ver + doy.Substring(2, 1) + doy.Substring(0, 2); // doy 123 => 83 12 (Little Endian with UUID version at nibble 3) | |
| } | |
| } | |
| // Then: Sort block 2, Little Endian | |
| if (hickelUuidVersion == 1) | |
| { | |
| block2 = String.Format("{0:mmHH}", dt); // Little Endian is correct here | |
| } | |
| else | |
| { | |
| int minuteOfDay = (dt.Minute + dt.Hour * 60) + 1; // 1..1440 | |
| string mot = String.Format("{0:X4}", minuteOfDay); | |
| if (hickelUuidVersion == 2) | |
| { | |
| block2 = mot; // This is wrong! (Big Endian instead of Little Endian) | |
| } | |
| else | |
| { | |
| block2 = mot.Substring(2, 2) + mot.Substring(0, 2); // mot 1234 => 34 12 (Little Endian) | |
| } | |
| } | |
| // Then: Sort block 1, Little Endian | |
| int millisecond8bits = (int)Math.Round(((decimal)dt.Millisecond / 999) * 255); // deviation -2ms..2ms | |
| if (hickelUuidVersion == 1) | |
| { | |
| int rnd16bits = random.Next(0x0000, 0xFFFF); | |
| block1 = String.Format("{0:X4}{1:X2}{2:ss}", rnd16bits, millisecond8bits, dt); // Little Endian is correct here | |
| } | |
| else | |
| { | |
| int rnd16bits = random.Next(0x0000, 0xFFFF + 1); | |
| block1 = String.Format("{0:X4}{1:X2}{2:X2}", rnd16bits, millisecond8bits, dt.Second); // Little Endian is correct here | |
| } | |
| // Now build and parse UUID | |
| string sGuid = String.Format("{0}-{1}-{2}-{3}-{4}", block1, block2, block3, block4, block5); | |
| Thread.Sleep((int)Math.Ceiling((decimal)999 / 255)); // Make sure that "millisecond" is not repeated on this system | |
| return Guid.Parse(sGuid); | |
| } | |
| static void Main(string[] args) | |
| { | |
| Console.WriteLine(DateTime.Now); | |
| Guid test; | |
| test = GenerateSqlServerSortableTimeBasedGuid(1); | |
| Console.WriteLine(test); | |
| test = GenerateSqlServerSortableTimeBasedGuid(2); | |
| Console.WriteLine(test); | |
| test = GenerateSqlServerSortableTimeBasedGuid(3); | |
| Console.WriteLine(test); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment