Skip to content

Commit 2327274

Browse files
committed
Publish: UUID vs ULID vs IntID
1 parent b1afe29 commit 2327274

File tree

6 files changed

+137
-7
lines changed

6 files changed

+137
-7
lines changed

content/blog/2025/20250121_ulid-announcement/index.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,12 @@ For detailed usage instructions and a complete API reference, check out the [Git
8282
## Performance Highlights
8383
ByteAether.Ulid outpaces competitors while maintaining strict adherence to the ULID specification.
8484

85-
| Library | Generation Speed | Compliance | Error Handling |
86-
|-----------------|------------------|----------------|----------------|
87-
| ByteAether.Ulid | ✅ Fastest | ✅ Full | ✅ Graceful |
88-
| NetUlid | ⚠️ Slower | ✅ Full | ⚠️ Limited |
89-
| NUlid | ⚠️ Slower | ✅ Full | ⚠️ Limited |
90-
| Cysharp.Ulid | ⚠️ Slower | ❌ Incomplete | ⚠️ Limited |
85+
| Library | Generation Speed | Compliance | Error Handling |
86+
|-----------------|------------------------------------|----------------|----------------|
87+
| ByteAether.Ulid | ✅ Fastest | ✅ Full | ✅ Graceful |
88+
| NetUlid | ⚠️ Slower | ✅ Full | ⚠️ Limited |
89+
| NUlid | ⚠️ Slower | ✅ Full | ⚠️ Limited |
90+
| Cysharp.Ulid | ⚠️ Fast, Non-secure, Non-monotonic | ❌ Incomplete | ⚠️ Limited |
9191

9292
For a full breakdown of benchmarks and testing methodology, visit our [GitHub repository](https://github.com/ByteAether/Ulid).
9393

File renamed without changes.
File renamed without changes.
3.1 MB
Loading
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
---
2+
title: "UUID vs ULID vs Integer IDs: A Technical Guide for Modern Systems"
3+
date: 2025-02-04
4+
tags: ["ulid", "uuid", "guid", "id", "entity-framework", "database", "sql", "performance"]
5+
image: header.png
6+
---
7+
8+
Unique identifiers are critical components in software systems, serving as the foundation for data management, distributed architectures, and secure API design. While UUIDs (specifically UUIDv4) and integer IDs have been widely adopted, [ULIDs (Universally Unique Lexicographically Sortable Identifiers)](https://github.com/ulid/spec) are increasingly recognized as a superior choice for modern applications. This article explores the technical distinctions between these identifiers, focusing on performance in .NET ecosystems and the database-level implications of their structural differences.
9+
10+
## Understanding the Identifier Types
11+
12+
### Integer IDs
13+
Integer IDs are sequential numeric values typically managed by databases through auto-increment mechanisms. Their simplicity and minimal storage requirements (4 bytes for `INT`, 8 bytes for `BIGINT`) make them straightforward to implement and efficient for indexing. However, their predictability exposes systems to security risks such as enumeration attacks (e.g., `/users/123`). Additionally, their reliance on centralized generation makes them unsuitable for distributed systems where coordination between nodes is impractical.
14+
15+
### UUIDs
16+
UUIDs are 128-bit identifiers designed to guarantee global uniqueness through randomness. The UUIDv4 variant, which uses 122 bits of random data, is the most common implementation. While UUIDs excel at collision resistance, their lack of inherent sortability leads to significant database performance issues, particularly in write-heavy systems. The random distribution of UUIDv4 values causes index fragmentation, increasing I/O overhead and reducing cache efficiency.
17+
18+
### ULIDs
19+
ULIDs are 128-bit identifiers composed of a 48-bit timestamp (millisecond precision) and 80 bits of cryptographically secure randomness. Encoded as a 26-character base32 string, ULIDs are lexicographically sortable, compact, and URL-safe. The timestamp prefix ensures that new records are inserted in chronological order, aligning with database indexing patterns to minimize fragmentation.
20+
21+
## Performance in .NET Applications
22+
23+
### ULID Generation and Monotonicity
24+
In .NET, the **[ByteAether.Ulid](https://github.com/ByteAether/Ulid)** library provides a high-performance implementation of ULID generation. Benchmarks published in its GitHub repository demonstrate that it generates ULIDs with zero heap allocations, leveraging stack-based operations and optimized bitwise manipulation. A key feature of ULIDs is **monotonicity**, which ensures that identifiers generated within the same millisecond increment sequentially. This guarantees that data inserted in a logical order (e.g., batch processing workflows) remains ordered in the database. For example, in a distributed logging system, monotonicity ensures log entries from the same source retain their sequence even if generated microseconds apart. ByteAether.Ulid further enforces this by incrementing the timestamp when the random component overflows, avoiding errors during generation while preserving order.
25+
26+
### Why UUIDs Fall Short
27+
While `Guid.NewGuid()` in .NET is fast, UUIDs lack structural ordering. This forces databases to insert records at random positions within indexes, leading to frequent page splits and fragmentation. ULIDs, by contrast, align insertion patterns with the natural ordering of database indexes, reducing overhead and improving throughput.
28+
29+
## Database Performance: Index Fragmentation Explained
30+
31+
### How Databases Organize Data
32+
Databases store records in fixed-size blocks called **pages** (e.g., 8kB in SQL Server, 16kB in MySQL). These pages are managed within **B+ tree** structures, where leaf nodes store actual data rows, and internal nodes route queries using key values. Sequential insertion ensures new rows are added to the end of the leaf node chain, minimizing disk seeks and memory usage.
33+
34+
### The Impact of Random Identifiers
35+
When identifiers are random (e.g., UUIDv4), new rows are inserted at arbitrary positions within the B+ tree. This forces databases to split existing pages to accommodate entries, a process known as **page splitting**. For example, inserting a record with UUID `f47ac10b-...` into a page already containing `a5670e02-...` may split the page into two, with half the rows moved to a new location.
36+
37+
Page splits have three major consequences:
38+
1. **Increased I/O Overhead**: Fragmented pages require more disk reads to retrieve logically contiguous data.
39+
2. **Memory Pressure**: More pages are loaded into the buffer pool, reducing the cache's effectiveness.
40+
3. **Write Amplification**: Splits generate additional transaction log entries, slowing bulk inserts and increasing storage costs.
41+
42+
### ULID’s Structural Advantage
43+
ULIDs embed a timestamp in their high-order bits, ensuring that new records are inserted sequentially. This aligns with the B+ tree’s design, reducing page splits compared to UUIDv4.
44+
45+
## Implementing ULIDs in .NET with ByteAether.Ulid
46+
47+
### Integration with Database Systems
48+
The **ByteAether.Ulid** library simplifies storing ULIDs as binary data, minimizing storage overhead and accelerating queries. Below are examples of integrating ULIDs with popular .NET data access frameworks:
49+
50+
#### Entity Framework Core Configuration
51+
```csharp
52+
public class Order
53+
{
54+
public Ulid Id { get; set; } // Stored as BINARY(16)
55+
public string Product { get; set; }
56+
}
57+
58+
public class UlidToBytesConverter : ValueConverter<Ulid, byte[]>
59+
{
60+
private static readonly ConverterMappingHints DefaultHints = new(size: 16);
61+
62+
public UlidToBytesConverter() : this(defaultHints) { }
63+
64+
public UlidToBytesConverter(ConverterMappingHints? mappingHints = null)
65+
: base(
66+
convertToProviderExpression: x => x.ToByteArray(),
67+
convertFromProviderExpression: x => Ulid.New(x),
68+
mappingHints: defaultHints.With(mappingHints)
69+
)
70+
{ }
71+
}
72+
73+
// In DbContext configuration
74+
protected override void OnModelCreating(ModelBuilder modelBuilder)
75+
{
76+
// ...
77+
configurationBuilder
78+
.Properties<Ulid>()
79+
.HaveConversion<UlidToBytesConverter>();
80+
// ...
81+
}
82+
```
83+
84+
#### Dapper Type Handler
85+
86+
```csharp
87+
public class UlidTypeHandler : SqlMapper.TypeHandler<Ulid>
88+
{
89+
public override void SetValue(IDbDataParameter parameter, Ulid value)
90+
{
91+
parameter.Value = value.ToByteArray();
92+
}
93+
94+
public override Ulid Parse(object value)
95+
{
96+
return Ulid.New((byte[])value);
97+
}
98+
}
99+
100+
Dapper.SqlMapper.AddTypeHandler(new UlidTypeHandler());
101+
```
102+
103+
### Performance Metrics
104+
The ByteAether.Ulid library generates ULIDs efficiently with zero heap allocations, making it ideal for high-throughput scenarios like event sourcing or real-time analytics. Detailed benchmark results are available in its [GitHub repository](https://github.com/ByteAether/Ulid).
105+
106+
## Recommendations and Conclusion
107+
108+
### When to Use ULIDs
109+
110+
ULIDs are particularly advantageous in:
111+
112+
* **Distributed Systems:** Decentralized generation eliminates coordination overhead.
113+
* **Time-Series Workloads:** Embedded timestamps simplify partitioning and range queries.
114+
* **Batch Processing:** Monotonicity ensures data retains its logical insertion order.
115+
116+
### Why ByteAether.Ulid?
117+
118+
* **Speed:** Generates ULIDs faster than other .NET libaries.
119+
* **Efficiency:** Zero heap allocations reduce garbage collection pressure.
120+
* **Simplicity:** Seamless integration with EF Core, Dapper, JSON serializers and more.
121+
122+
## The Future of Identifiers
123+
124+
UUIDs and integer IDs remain viable for legacy use cases, but ULIDs address their core limitations: sortability, fragmentation, and scalability. For .NET developers, **[ByteAether.Ulid](https://github.com/ByteAether/Ulid)** offers a robust, future-proof solution that aligns application logic with database storage mechanics.
125+
126+
```bash
127+
# Install ByteAether.Ulid
128+
dotnet add package ByteAether.Ulid
129+
```
130+
131+
By adopting ULIDs, teams can achieve faster queries, reduced infrastructure costs, and architectures that scale effortlessly—proving that the right identifier can transform system design.

public/css/index.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ a:active {
100100

101101
a.ha {
102102
text-decoration: none;
103-
margin-top: -2px;
104103
}
105104

106105
h1, h2, h3, h4, h5, h6 {

0 commit comments

Comments
 (0)