UUIDs are everywhere in modern applications — for users, orders, events, and more. But if you're storing them in PostgreSQL, are you getting the best performance and clarity?
Let's dive into how PostgreSQL supports UUIDs natively, and how to use them efficiently with the right generation, indexing, and storage strategies.
🧬 The Native UUID Type
PostgreSQL has a built-in uuid
type that stores UUIDs as 16 bytes — compact, binary, and efficient.
✅ Benefits:
- Smaller than a
CHAR(36)
UUID string - Faster comparisons and joins
- Uses native binary format, not text
🔧 Example Table
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT
);
This gives you:
- Compact primary key
- No user-managed ID generation
- Easy joins and lookups using native
uuid
type
🔑 Generating UUIDs: `gen_random_uuid()` vs `uuid_generate_v4()`
PostgreSQL offers two main options:
1. `gen_random_uuid()` from `pgcrypto`
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
SELECT gen_random_uuid();
- Uses cryptographically secure randomness
- UUIDv4 format
- Recommended for production use
2. `uuid_generate_v4()` from `uuid-ossp`
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
SELECT uuid_generate_v4();
- Also generates UUIDv4
- Slightly more overhead
- More flexible (supports v1, v3, v5 too)
✅ Recommendation
Use pgcrypto
's gen_random_uuid()
unless you need other UUID versions.
📈 Indexing Performance
🔍 Index on UUID
CREATE INDEX idx_users_id ON users (id);
Native UUIDs index well — but random UUIDs (v4) cause scattered inserts in B-tree indexes.
🧠 Improve Locality with Time-Based UUIDs
- Use UUIDv7 or ULID for sortable identifiers
- Improves index locality and cache efficiency
If using UUIDv4, consider:
- Random insert patterns = slower index performance
- CLUSTERing periodically if you need better scan locality
🧪 Sorting and Querying
Common patterns:
-- Latest user
SELECT * FROM users ORDER BY created_at DESC LIMIT 1;
-- Find by UUID
SELECT * FROM users WHERE id = '550e8400-e29b-41d4-a716-446655440000';
UUIDs sort lexicographically, not chronologically — unless you use time-based UUIDs.
📊 Storage Efficiency
Format | Size | Notes |
---|---|---|
uuid | 16B | Native, fast, compact |
CHAR(36) | 36B | Avoid — wastes space |
BYTEA | 16B+ | Works, but less ergonomic |
Stick to the uuid
type unless you have exotic encoding needs.
🧰 Tips for Working with UUIDs in PostgreSQL
Tip | Why it helps |
---|---|
Use uuid not char/varchar | Saves space, faster operations |
Default to gen_random_uuid() | Built-in, safe, no external deps required |
Index your UUIDs | Fast lookup and joins |
Avoid truncating UUIDs | Breaks uniqueness |
Use UUIDv7/ULID for sortability | Better B-tree performance on large tables |
⚠️ Anti-Patterns to Avoid
- Text UUID columns: wastes space, slower comparisons
- Sequential UUIDs with `uuid_generate_v1()`: can leak MAC/timestamp
- Custom UUID strings: not compatible with tooling or type system
- Unindexed UUID joins: can cripple performance
Final Thoughts
PostgreSQL makes UUIDs first-class citizens — fast, efficient, and schema-friendly.
But the secret to success is not just “use UUIDs” — it’s use them right:
- Compact storage ✅
- Smart generation ✅
- Strategic indexing ✅
🐘 UUIDs + PostgreSQL = powerful combo. Now go use it for fun and profit.