UUID vs Auto-increment: Choosing the Right Primary Key
When to use UUIDs versus auto-incrementing integers for database primary keys in different scenarios.
Quick answer
Use auto-increment integers for single-database, performance-critical relational schemas. Use UUIDs (often v4) when generating IDs across services, offline clients, or public APIs. ByteToolBox generates UUID v4 in bulk for tests and prototypes.
Key takeaways
- ›Auto-increment IDs are smaller and faster for B-tree indexes in a single database.
- ›UUIDs avoid coordination and ID guessing across distributed writers.
- ›Random UUID v4 can fragment indexes — consider time-ordered IDs for very high insert rates.
- ›Generate test UUIDs with the UUID Generator without hitting your database sequence.
Apply this guide with the UUID Generator
Open UUID GeneratorChoosing the right primary key strategy is crucial for database design and can significantly impact your application's performance, scalability, and maintainability. Generate UUID v4 samples with the UUID Generator while evaluating these tradeoffs.
Understanding the Options
Auto-incrementing Integers
Auto-incrementing integers are sequential numbers automatically generated by the database:
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);UUIDs (Universally Unique Identifiers)
UUIDs are 128-bit identifiers that are globally unique across space and time:
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100),
email VARCHAR(100)
);Auto-incrementing Integers: Pros and Cons
Advantages
1. Performance
- Faster queries: Integer comparisons are faster than string comparisons
- Smaller storage: 4-8 bytes vs 16 bytes for UUIDs
- Better indexing: B-tree indexes work more efficiently with sequential integers
- Faster joins: Integer joins are typically faster than string joins
-- Integer primary key - very fast
SELECT * FROM users WHERE id = 12345;
-- Integer foreign key - efficient
SELECT u.*, p.*
FROM users u
JOIN posts p ON u.id = p.user_id;2. Human-readable
- Easy to reference in URLs:
/users/12345 - Simple to debug and troubleshoot
- Intuitive ordering (newer records have higher IDs)
3. Database optimization
- Most databases are optimized for integer primary keys
- Better query plan optimization
- More efficient storage and caching
Disadvantages
1. Scalability issues
- Single point of failure: One database generates all IDs
- Replication lag: Master-slave setups can cause ID conflicts
- Sharding complexity: Difficult to distribute across multiple databases
// Problem: Multiple servers generating IDs
// Server 1: generates ID 1001
// Server 2: generates ID 1001 (conflict!)2. Information leakage
- Exposes business metrics (user count, order count)
- Predictable patterns can be exploited
- Security concerns in public APIs
// Bad: Exposes business data
// /api/users/1000000 - reveals you have 1M users
// /api/orders/50000 - reveals order volume3. Merge conflicts
- Difficult to merge data from different systems
- ID conflicts when consolidating databases
UUIDs: Pros and Cons
Advantages
1. Global uniqueness
- No conflicts across distributed systems
- Safe for data replication and merging
- Perfect for microservices architecture
// UUIDs are globally unique
const uuid1 = '550e8400-e29b-41d4-a716-446655440000';
const uuid2 = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
// No chance of collision, even across different systems2. Privacy and security
- No sequential patterns to exploit
- Doesn't reveal business metrics
- Safer for public APIs
// Good: No information leakage
// /api/users/550e8400-e29b-41d4-a716-446655440000
// /api/orders/6ba7b810-9dad-11d1-80b4-00c04fd430c83. Distributed system friendly
- Can generate IDs without database round-trip
- No coordination needed between servers
- Perfect for offline-first applications
// Generate UUID on client side
const userId = crypto.randomUUID();
// Safe to use immediately, no database call neededDisadvantages
1. Performance overhead
- Larger storage: 16 bytes vs 4-8 bytes
- Slower comparisons: String comparison vs integer
- Index fragmentation: Random UUIDs cause B-tree index fragmentation
-- UUID comparison is slower
SELECT * FROM users WHERE id = '550e8400-e29b-41d4-a716-446655440000';
-- UUID foreign key joins are slower
SELECT u.*, p.*
FROM users u
JOIN posts p ON u.id = p.user_id;2. Debugging complexity
- Harder to remember and reference
- More complex URL patterns
- Difficult to manually inspect data
3. Index performance
- Random UUIDs cause index fragmentation
- Slower range queries
- More storage overhead for indexes
UUID Variants and Performance
UUID Version 1 (Time-based)
// Contains MAC address and timestamp
// More sequential, better for indexing
const uuid1 = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';UUID Version 4 (Random)
// Completely random
// Causes index fragmentation
const uuid4 = '550e8400-e29b-41d4-a716-446655440000';UUID Version 7 (Time-ordered)
// New standard (RFC 4122)
// Time-ordered, better for indexing
// Combines benefits of both approaches
const uuid7 = '018c4c6c-0000-7000-8000-000000000000';When to Use Each Approach
Use Auto-incrementing Integers When:
1. Single Database System
// Monolithic application with single database
const user = await db.users.create({
name: 'John Doe',
email: 'john@example.com'
// id will be auto-generated: 1, 2, 3, ...
});2. Performance is Critical
-- High-traffic applications where every millisecond counts
SELECT COUNT(*) FROM orders WHERE user_id = 12345;
-- Integer comparison is fastest3. Simple Applications
- Internal tools
- Prototypes and MVPs
- Applications that don't need to scale
Use UUIDs When:
1. Distributed Systems
// Microservices architecture
const orderService = {
createOrder: async (data) => {
const orderId = crypto.randomUUID(); // Generate on service
return await db.orders.create({ id: orderId, ...data });
}
};2. Data Replication and Merging
// Multiple databases that need to be merged
const mergeUsers = (db1Users, db2Users) => {
// No ID conflicts with UUIDs
return [...db1Users, ...db2Users];
};3. Public APIs
// REST API with UUIDs
app.get('/api/users/:id', (req, res) => {
const userId = req.params.id; // UUID, no information leakage
// ...
});4. Offline-First Applications
// Mobile app that works offline
const createOfflinePost = (data) => {
const postId = crypto.randomUUID(); // Generate offline
return { id: postId, ...data, synced: false };
};Hybrid Approaches
1. Composite Keys
CREATE TABLE users (
id INT AUTO_INCREMENT,
uuid UUID UNIQUE DEFAULT gen_random_uuid(),
name VARCHAR(100),
PRIMARY KEY (id),
INDEX idx_uuid (uuid)
);2. Internal vs External IDs
// Internal: Use integers for performance
const user = await db.users.findById(12345);
// External: Use UUIDs for APIs
app.get('/api/users/:uuid', (req, res) => {
const user = await db.users.findByUuid(req.params.uuid);
res.json(user);
});3. Sharded Auto-increment
// Use different ranges for different shards
// Shard 1: IDs 1-1000000
// Shard 2: IDs 1000001-2000000
// Shard 3: IDs 2000001-3000000Performance Optimization Tips
For Auto-incrementing Integers:
-- Use appropriate integer types
TINYINT -- 1 byte, 0-255
SMALLINT -- 2 bytes, -32,768 to 32,767
INT -- 4 bytes, -2B to 2B
BIGINT -- 8 bytes, -9Q to 9Q
-- Choose the smallest type that fits your needsFor UUIDs:
-- Use UUID v7 for better performance
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- or use time-ordered UUIDs
id UUID PRIMARY KEY DEFAULT uuid_generate_v7()
);
-- Consider using ULID as alternative
-- Shorter than UUID, time-ordered, URL-safeMigration Strategies
From Auto-increment to UUID:
-- 1. Add UUID column
ALTER TABLE users ADD COLUMN uuid UUID DEFAULT gen_random_uuid();
-- 2. Populate UUIDs for existing records
UPDATE users SET uuid = gen_random_uuid() WHERE uuid IS NULL;
-- 3. Make UUID unique
ALTER TABLE users ADD CONSTRAINT users_uuid_unique UNIQUE (uuid);
-- 4. Update foreign key references
ALTER TABLE posts ADD COLUMN user_uuid UUID;
UPDATE posts SET user_uuid = u.uuid
FROM users u WHERE posts.user_id = u.id;
-- 5. Switch primary key (careful!)
-- This is complex and requires downtimeBest Practices
- Choose based on your architecture: Monoliths favor integers, microservices favor UUIDs
- Consider future scaling: Will you need to shard or replicate?
- Profile your specific use case: Test with your actual data and queries
- Use hybrid approaches when appropriate: Internal integers, external UUIDs
- Document your decision: Explain why you chose your approach
- Plan for migration: Design with future changes in mind
Conclusion
The choice between UUIDs and auto-incrementing integers depends on your specific requirements:
- Use auto-incrementing integers for single-database applications where performance is critical
- Use UUIDs for distributed systems, public APIs, and applications that need to scale horizontally
- Consider hybrid approaches for the best of both worlds
Remember that this decision affects your entire application architecture, so choose carefully and consider your long-term scaling needs. The performance difference is often negligible for most applications, but the architectural benefits of UUIDs in distributed systems can be significant.