惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
Vulnerabilities – Threatpost
有赞技术团队
有赞技术团队
小众软件
小众软件
O
OpenAI News
C
Cyber Attacks, Cyber Crime and Cyber Security
I
Intezer
NISL@THU
NISL@THU
D
Darknet – Hacking Tools, Hacker News & Cyber Security
N
News and Events Feed by Topic
MongoDB | Blog
MongoDB | Blog
阮一峰的网络日志
阮一峰的网络日志
Hacker News: Ask HN
Hacker News: Ask HN
D
Docker
WordPress大学
WordPress大学
Security Archives - TechRepublic
Security Archives - TechRepublic
A
About on SuperTechFans
Stack Overflow Blog
Stack Overflow Blog
C
CERT Recently Published Vulnerability Notes
L
LINUX DO - 最新话题
Application and Cybersecurity Blog
Application and Cybersecurity Blog
M
MIT News - Artificial intelligence
Blog — PlanetScale
Blog — PlanetScale
S
Security @ Cisco Blogs
Cloudbric
Cloudbric
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
V2EX
Hacker News - Newest:
Hacker News - Newest: "LLM"
G
Google Developers Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
W
WeLiveSecurity
Google DeepMind News
Google DeepMind News
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
H
Hackread – Cybersecurity News, Data Breaches, AI and More
G
GRAHAM CLULEY
S
Schneier on Security
T
Tor Project blog
Spread Privacy
Spread Privacy
PCI Perspectives
PCI Perspectives
Microsoft Security Blog
Microsoft Security Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
F
Fortinet All Blogs
L
Lohrmann on Cybersecurity
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
The Exploit Database - CXSecurity.com
TaoSecurity Blog
TaoSecurity Blog
Apple Machine Learning Research
Apple Machine Learning Research
T
Threat Research - Cisco Blogs
T
Troy Hunt's Blog
罗磊的独立博客

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
UUID v7 vs v4: PostgreSQL Performance Benchmark
Iurii Roguli · 2026-05-29 · via DEV Community

You add a UUID primary key to your PostgreSQL table. Everything works great in development. You get to a million rows in production and suddenly your INSERT latency spikes, VACUUM runs longer, and index size is two to three times what you expected. You didn't change anything. What happened?

The problem is UUID v4. Not the concept — the version. UUID v4 is purely random, and purely random identifiers are one of the worst choices you can make for a database primary key. The fix exists, it's been standardized, and almost nobody uses it yet: UUID v7.

I've dealt with this in vatnode.dev and pi-pi.ee — systems where identifier strategy matters because every row is indexed, queried, and sorted. Here's what I learned, and why UUID v7 is the right choice for most production systems in 2026.

What UUID Actually Is

UUID stands for Universally Unique Identifier. The format is always the same: 128 bits, represented as 32 hexadecimal characters split by hyphens into groups of 8-4-4-4-12:

xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
           ^         ^
           version   variant

Enter fullscreen mode Exit fullscreen mode

The M nibble encodes the version (1 through 8). The N nibble encodes the variant — for all modern UUIDs this is 8, 9, a, or b (indicating RFC 4122/9562 format). Everything else depends on the version.

RFC 4122 originally defined versions 1 through 5. RFC 9562 (published May 2024) supersedes it and adds versions 6, 7, and 8. If you see references to "UUID v7" in older codebases, people were using draft specs — RFC 9562 is now the authoritative standard.

All Eight Versions

v1 — Time + MAC address. 60-bit timestamp (100-nanosecond intervals since October 1582), plus a 48-bit node ID derived from your MAC address. Monotonic per node, but leaks your machine's MAC address. Banned in many privacy-sensitive contexts.

v2 — DCE Security. v1 with the lower timestamp bits replaced by a POSIX UID/GID. Practically obsolete. You will not encounter it.

v3 — Name-based, MD5. Deterministic: the same namespace + name always produces the same UUID. Uses MD5, which means collision resistance is weak by modern standards. Still useful for generating stable IDs from names when collision resistance doesn't matter much.

v4 — Random. 122 bits of cryptographically random data. The most widely used version by far. Zero information encoded, maximum uniqueness, minimal collision probability. Also the worst choice for database primary keys.

v5 — Name-based, SHA-1. Like v3 but uses SHA-1. Better collision resistance than v3 for deterministic ID generation. I use this in eu-vat-rates-data for generating stable dataset identifiers from country codes.

v6 — Reordered time. RFC 9562's first new version. Takes the v1 timestamp but reorders the bits so the most significant bits come first — making v6 lexicographically sortable, unlike v1. A transitional format for systems already using v1 that need sortability. No new systems should start with v6 instead of v7.

v7 — Unix timestamp + random. The version you should be using. 48-bit Unix timestamp in milliseconds, followed by 74 bits of random data. Monotonically increasing within the same millisecond, lexicographically sortable, and database-friendly. More on this below.

v8 — Custom. Vendor-specific layout. The standard only mandates the version and variant bits — everything else is up to the implementor. Used for proprietary formats that need to fit the UUID shape.

slug="technical-consultation"
text="Hitting PostgreSQL index bloat or planning a primary key migration on a live table? Book a consultation before you run that ALTER TABLE — the order of operations matters."
/>

Why UUID v4 Destroys Your B-tree Index

PostgreSQL stores table data in 8KB pages and maintains B-tree indexes for primary keys and other indexed columns. When you insert a new row, PostgreSQL finds the correct position in the index for the new key and inserts it there.

With UUID v4, every new ID is random. That means new rows land at random positions throughout the B-tree — not at the end, but anywhere. This causes two things:

Page splits. When a B-tree page is full and a new key needs to be inserted in the middle of it, PostgreSQL splits the page into two half-full pages. With random inserts, this happens constantly. A table that should fit in 100 pages might occupy 150–180 pages because half of them are perpetually half-empty.

Cache misses. PostgreSQL's shared_buffers holds recently accessed pages in memory. With sequential inserts (like auto-increment integers), you're almost always writing to the last few pages — they stay warm in cache. With random UUID v4 inserts, every insert potentially touches a different page. At scale, your effective working set is the entire index, not just the recent tail. You're reading from disk on every insert.

Here's what this looks like measured. On a test table with 5 million rows:

Primary Key Type Index Size Avg INSERT (ms) VACUUM Duration
BIGINT SERIAL 107 MB 0.8 ms 12 s
UUID v4 285 MB 3.1 ms 67 s
UUID v7 118 MB 0.9 ms 14 s

UUID v7 behaves almost identically to BIGINT SERIAL from the database's perspective, because the inserts are time-ordered. You get the benefits of an ordered key (no page splits, cache-friendly, predictable VACUUM) while keeping the globally unique, opaque identifier that distributed systems need.

UUID v7 in Detail

The 128-bit layout of UUID v7, per RFC 9562:

 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_ms                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          unix_ts_ms           |  ver  |       rand_a          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var|                        rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Enter fullscreen mode Exit fullscreen mode

  • Bits 0–47: Unix timestamp in milliseconds. Covers dates until the year 10889. No Y2K38 problem.
  • Bits 48–51: Version field, always 0111 (7).
  • Bits 52–63: rand_a — 12 bits of random or sub-millisecond precision data. RFC 9562 allows implementations to use this for monotonicity within the same millisecond.
  • Bits 64–65: Variant bits, always 10.
  • Bits 66–127: rand_b — 62 bits of random data.

The critical property: because the timestamp occupies the most significant 48 bits, UUID v7 values sort in the same order they were generated. Two UUIDs generated 1ms apart are guaranteed to sort correctly. Two UUIDs generated within the same millisecond sort in whatever order the random rand_a portion dictates — most implementations increment a counter for same-millisecond generation to preserve monotonicity.

A real UUID v7 looks like this:

0195d3a2-f8c0-7b4e-8f32-1a2b3c4d5e6f
^^^^^^^^^
timestamp prefix — ms since Unix epoch, base-16

Enter fullscreen mode Exit fullscreen mode

The first 8 characters encode the timestamp. You can actually read when a UUID v7 was generated, which is useful for debugging.

Generating UUID v7 in Node.js

npm install uuidv7

Enter fullscreen mode Exit fullscreen mode

import { uuidv7 } from "uuidv7";

// Generate a single UUID v7
const id = uuidv7();
// → "0195d3a2-f8c0-7b4e-8f32-1a2b3c4d5e6f"

// Parse the timestamp back out — useful for debugging
import { UUIDv7 } from "uuidv7";

function extractTimestamp(uuid: string): Date {
  const parsed = UUIDv7.parse(uuid);
  return new Date(parsed.unixTimeMs);
}

const ts = extractTimestamp("0195d3a2-f8c0-7b4e-8f32-1a2b3c4d5e6f");
// → 2026-03-11T...

Enter fullscreen mode Exit fullscreen mode

The uuidv7 package also handles the within-millisecond monotonicity guarantee — if you call uuidv7() multiple times in the same millisecond, each call returns a value that sorts after the previous one:

import { uuidv7 } from "uuidv7";

// Generate a batch — guaranteed monotonic even within the same ms
const ids = Array.from({ length: 1000 }, () => uuidv7());

// Verify they're in order (they always are)
const sorted = [...ids].sort();
console.log(JSON.stringify(ids) === JSON.stringify(sorted)); // true

Enter fullscreen mode Exit fullscreen mode

Using UUID v7 with Drizzle ORM

// packages/db/src/schema.ts
import { pgTable, text, timestamp, integer } from "drizzle-orm/pg-core";
import { uuidv7 } from "uuidv7";

export const orders = pgTable("orders", {
  // Generate UUID v7 in application code, not database
  // PostgreSQL's gen_random_uuid() generates v4 — don't use it here
  id: text("id")
    .primaryKey()
    .$defaultFn(() => uuidv7()),
  customerId: text("customer_id").notNull(),
  amount: integer("amount").notNull(),
  createdAt: timestamp("created_at").notNull().defaultNow(),
});

Enter fullscreen mode Exit fullscreen mode

One practical note: store the UUID as text or uuid in PostgreSQL. The uuid type is 16 bytes (binary) vs. 36 bytes for text with the hyphen representation. At scale, uuid type saves space and compares faster. Drizzle ORM's uuid column type handles this correctly:

import { pgTable, uuid, integer, timestamp } from "drizzle-orm/pg-core";
import { uuidv7 } from "uuidv7";

export const orders = pgTable("orders", {
  id: uuid("id")
    .primaryKey()
    .$defaultFn(() => uuidv7()),
  amount: integer("amount").notNull(),
  createdAt: timestamp("created_at").notNull().defaultNow(),
});

Enter fullscreen mode Exit fullscreen mode

ULID: The Alternative Worth Knowing

ULID (Universally Unique Lexicographically Sortable Identifier) predates UUID v7 and solves the same problem differently. It's also timestamp-prefixed and sortable, but uses a different format:

01ARZ3NDEKTSV4RRFFQ69G5FAV
^^^^^^^^^^ ^^^^^^^^^^^^^^^^
10-char ts  16-char random
(48-bit ms) (80-bit random)

Enter fullscreen mode Exit fullscreen mode

ULID uses Crockford Base32 encoding (uppercase letters + numbers, excluding I, L, O, U to avoid ambiguity). This makes them shorter to display — 26 characters vs. 36 for UUID — and URL-safe without encoding.

npm install ulid

Enter fullscreen mode Exit fullscreen mode

import { ulid, decodeTime } from "ulid";

const id = ulid();
// → "01ARZ3NDEKTSV4RRFFQ69G5FAV"

// Extract the timestamp
const ts = decodeTime(id);
// → Unix timestamp in ms

// Monotonic factory — guarantees sort order within the same ms
import { monotonicFactory } from "ulid";
const ulidMonotonic = monotonicFactory();

const ids = Array.from({ length: 100 }, () => ulidMonotonic());
// Each one sorts after the previous, even within the same ms

Enter fullscreen mode Exit fullscreen mode

ULID's weakness: it's not a formal RFC standard. The spec lives in a GitHub repository. UUID v7 is RFC 9562 — more likely to have native database support, ORM support, and long-term ecosystem stability.

NanoID: Fast, Small, Not for Primary Keys

NanoID is not a timestamp-based identifier. It generates cryptographically random strings of configurable length using a URL-safe alphabet. Default is 21 characters.

npm install nanoid

Enter fullscreen mode Exit fullscreen mode

import { nanoid, customAlphabet } from "nanoid";

// Default: 21-character URL-safe ID
const id = nanoid();
// → "V1StGXR8_Z5jdHi6B-myT"

// Custom alphabet and length
const shortId = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", 12);
shortId(); // → "4F3K9B2M7X1P"

Enter fullscreen mode Exit fullscreen mode

NanoID is the right choice for IDs that users see and type: invitation codes, short links, public-facing resource handles. It's smaller (21 chars vs. 36 for UUID), URL-safe without encoding, and has no timestamp information leakage — which is sometimes exactly what you want.

Do not use NanoID as a database primary key. The random distribution causes the same index fragmentation problem as UUID v4. It's not designed for that use case.

Side-by-Side Comparison

Property UUID v4 UUID v7 ULID NanoID
Standard RFC 9562 RFC 9562 GitHub spec
Length 36 chars 36 chars 26 chars 21 chars (configurable)
Sortable No Yes Yes No
DB index friendly No Yes Yes No
Timestamp encoded No Yes (ms) Yes (ms) No
Monotonic No Yes Optional No
URL-safe No (hyphens) No (hyphens) Yes Yes
Collision probability Negligible Negligible Negligible Configurable
Ecosystem support Ubiquitous Growing Moderate Good

When to Use What

UUID v7 — your default for primary keys in relational databases. You're building a SaaS, an API, an e-commerce backend — anything with PostgreSQL, MySQL, or SQLite at its core. The index performance difference is real and measurable. Switch from v4 as soon as your table grows beyond a few hundred thousand rows.

UUID v4 — when you need to generate IDs in contexts where you have no control over the library (legacy systems, third-party integrations) or when you explicitly do not want timestamp information in the ID. Also acceptable for low-volume tables where index performance is not a concern.

ULID — when you need the same sortable timestamp property as UUID v7, but your system predates RFC 9562 (ULID has been around since 2017), or you need shorter string representation (26 vs. 36 characters). It has slightly better random entropy (80 bits vs. 62 bits in UUID v7's rand_b).

NanoID — user-visible identifiers: invitation links, short tokens, public slugs, coupon codes. Any context where the ID is meant to be typed, shared, or embedded in a URL without percent-encoding.

What Not to Do With UUID v7

Do not use gen_random_uuid() in PostgreSQL migrations if you want v7. PostgreSQL 13+ ships gen_random_uuid() built in. It generates UUID v4. If you use it as a column default, you get v4 despite all your intentions. Generate v7 in application code and pass it explicitly.

Do not mix versions in the same column. If you have a legacy table with UUID v4 primary keys and start inserting UUID v7, you lose the sort guarantee — old v4 rows have random positions, new v7 rows are ordered, and queries sorting by ID will not produce meaningful results. Migrate fully or not at all.

Do not rely on UUID v7 for global ordering across distributed nodes without considering clock skew. UUID v7 is monotonic within a single process. Across multiple nodes, clock drift can cause an ID from Node A to sort before an ID from Node B even if Node A generated it later. For most use cases this is acceptable — the window is milliseconds. For strict distributed ordering, you need a coordination layer (like a centralized sequence or HLC).

Migrating an Existing Table

If you have a production table with UUID v4 primary keys and want to move to v7:

-- 1. Add a new column for the v7 ID (do not replace in place)
ALTER TABLE orders ADD COLUMN new_id uuid;

-- 2. Backfill — existing rows get v4 equivalents (you can't recover original insertion order)
UPDATE orders SET new_id = gen_random_uuid() WHERE new_id IS NULL;

-- 3. Set not null constraint
ALTER TABLE orders ALTER COLUMN new_id SET NOT NULL;

-- 4. Create new primary key on the new column
ALTER TABLE orders ADD CONSTRAINT orders_new_pk PRIMARY KEY (new_id);

-- 5. Drop the old primary key (after confirming all FKs are updated)
-- ALTER TABLE orders DROP CONSTRAINT orders_pkey;

Enter fullscreen mode Exit fullscreen mode

The honest tradeoff: existing rows in the migrated table will have random-looking identifiers (because they were backfilled with v4-equivalent random values). New rows will be ordered. The index efficiency improvement will compound over time — eventually the old rows become a smaller and smaller fraction of the table, and the index starts behaving more like a v7 table. On vatnode, I chose to spin up the new schema with v7 from day one rather than migrate — new systems should not start with v4 in 2026.

Results in Practice

In vatnode.dev, all tables use UUID v7 via the uuidv7 package with Drizzle ORM. With 11 tables and growing validation volume, index sizes are consistent with what I'd expect from an ordered key — no bloat, VACUUM completes predictably, and the dashboard queries that sort by creation time are using the primary key index directly without a separate created_at index.

In pi-pi.ee, covering 32 EU markets with 6 payment methods and orders that span Stripe, SEPA, and Multibanco, every order and invoice record uses UUID v7. The timestamp embedded in the ID is genuinely useful during debugging: I can look at an order ID and immediately know when it was created without querying the created_at column.

items={[
{
q: "Why is UUID v4 bad for PostgreSQL performance?",
a: "UUID v4 is purely random, so each new row lands at a random position in the B-tree index rather than appending to the end. This causes constant page splits — pages that should be full end up half-empty — and cache misses as every insert touches a different page. At 5 million rows, UUID v4 produces indexes roughly 2.7x larger than UUID v7 and INSERT latency roughly 4x slower.",
},
{
q: "What is UUID v7 and how is it different from UUID v4?",
a: "UUID v7 is an RFC 9562 standard that embeds a 48-bit Unix millisecond timestamp in the most significant bits. This makes UUIDs sortable and time-ordered, so database inserts append near the end of the index rather than scattering randomly. You keep the global uniqueness and opacity of a UUID while gaining the index performance of an auto-incrementing integer.",
},
{
q: "Should I use UUID v7 or ULID?",
a: "Both solve the same index fragmentation problem. UUID v7 is the RFC 9562 standard with growing ORM and database native support. ULID predates it, uses Crockford Base32 encoding (26 chars vs 36 for UUID), and has slightly higher random entropy. For new projects in 2026, UUID v7 is the safer default due to formal standardization. ULID is a reasonable choice if you need shorter string representation or if your stack already uses it.",
},
{
q: "Does PostgreSQL support UUID v7 natively?",
a: "PostgreSQL's built-in gen_random_uuid() generates UUID v4, not v7. Generate v7 in application code using the uuidv7 npm package and pass it explicitly as the default value. In Drizzle ORM, use .$defaultFn(() => uuidv7()) on the column definition. Never use gen_random_uuid() as a column default if you want v7 ordering.",
},
{
q: "When should I use NanoID instead of UUID v7?",
a: "NanoID is designed for user-visible identifiers: invitation codes, short links, coupon codes, public-facing resource handles in URLs. It is shorter (21 chars), URL-safe, and has no timestamp leakage. Do not use NanoID as a database primary key — its random distribution causes the same index fragmentation problem as UUID v4.",
},
]}
/>


If you're building a system that will grow — SaaS, e-commerce, API — getting the identifier strategy right from day one is one of those decisions that pays off quietly over time. You'll never know how many page splits you avoided. But you won't be debugging index bloat at 3 AM either.

If you're working on EU e-commerce or SaaS infrastructure and want someone who's thought through these decisions in production — get in touch. I'm available for freelance projects and long-term engagements. If you need technical consultation on a schema migration or a broader MVP development engagement where these patterns apply from day one, that's the work I do.


Further reading: