UUIDs across SQL databases
On this page
Every major SQL database supports UUIDs, but the spelling, storage, and recommended generator differ. This page is the side-by-side cheatsheet. For the deeper reasoning behind any of it, see UUID as primary key.
Side-by-side
| Engine | UUID type | Bytes | Random generator | Time-ordered |
|---|---|---|---|---|
| PostgreSQL 18+ | uuid | 16 | gen_random_uuid() | uuidv7() ✅ |
| PostgreSQL 13–17 | uuid | 16 | gen_random_uuid() | generate in app |
| PostgreSQL ≤12 | uuid (with pgcrypto) | 16 | gen_random_uuid() | generate in app |
| MySQL 8+ | BINARY(16) + UUID_TO_BIN/BIN_TO_UUID | 16 | UUID() | generate in app |
| MariaDB 10.7+ | UUID (native) | 16 | UUID() | generate in app |
| SQL Server | uniqueidentifier | 16 | NEWID() (v4) | NEWSEQUENTIALID() (proprietary) |
| SQLite | BLOB(16) or TEXT | 16 / 37 | none built-in | generate in app |
| Oracle 23+ | UUID (native) | 16 | SYS_GUID() (proprietary, not RFC-compliant) | generate in app |
The general advice is the same across all of them: store as 16 bytes, use the native type, and generate v7 in your application code if the engine doesn’t have a built-in v7 generator.
PostgreSQL
-- PG 18+ (October 2025): time-ordered, recommended
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT uuidv7(),
created_at timestamptz NOT NULL DEFAULT now()
);
-- PG 13-17: random
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT gen_random_uuid()
);
-- PG ≤12: needs pgcrypto extension
CREATE EXTENSION IF NOT EXISTS pgcrypto;
See the dedicated PostgreSQL UUID guide.
MySQL 8+
MySQL has a UUID() function returning v1, but no native UUID type. The recommended pattern is BINARY(16):
CREATE TABLE orders (
id BINARY(16) PRIMARY KEY,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- generate in MySQL (v1, not recommended for new code)
INSERT INTO orders (id) VALUES (UUID_TO_BIN(UUID()));
-- read as string
SELECT BIN_TO_UUID(id) AS id FROM orders;
For v7, generate in your application:
INSERT INTO orders (id) VALUES (UUID_TO_BIN(?));
-- bind the v7 UUID generated in app code
UUID_TO_BIN(uuid, 1) reorders bytes for better B-tree locality with v1 UUIDs (the second arg is “swap_flag”). Don’t use it with v7 UUIDs — v7 already has the right byte order for sorting.
MariaDB 10.7+
MariaDB added a native UUID type that stores 16 bytes:
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT UUID(),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Better than MySQL for ergonomics — no UUID_TO_BIN round-tripping. But the default UUID() is still v1; for v7, generate in app code.
SQL Server
-- v4 random (don't use as clustered primary key — fragments the index)
CREATE TABLE Orders (
Id uniqueidentifier PRIMARY KEY DEFAULT NEWID(),
CreatedAt datetime2 NOT NULL DEFAULT SYSDATETIME()
);
-- "sequential" GUID — better for clustered index, but reveals partial timestamp
CREATE TABLE Orders (
Id uniqueidentifier PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
CreatedAt datetime2 NOT NULL DEFAULT SYSDATETIME()
);
-- v7 from .NET 9+ application code (recommended)
INSERT INTO Orders (Id, CreatedAt) VALUES (@id, @now);
-- @id from Guid.CreateVersion7()
NEWSEQUENTIALID() is not a UUID v7. It’s a SQL Server-specific monotonic-within-a-machine variant. For cross-platform v7, generate in C# / .NET 9+ — see the C# / .NET guide.
A SQL Server-specific gotcha: stored bytes are in Microsoft byte order (first three groups little-endian). When reading raw bytes from another platform, byte-swap. See UUID vs GUID.
SQLite
SQLite has no UUID type. Two patterns:
As BLOB (16 bytes — recommended)
CREATE TABLE orders (
id BLOB(16) PRIMARY KEY,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
Half the storage of TEXT, faster comparisons. Your application generates UUIDs and inserts the binary form.
As TEXT (37 chars)
CREATE TABLE orders (
id TEXT PRIMARY KEY,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
Easier to debug (legible in sqlite3 REPL), but ~2× storage. Fine for small databases.
For both, generate v7 in your application — SQLite has no built-in UUID functions.
Oracle 23+
Oracle 23 added a native UUID type:
CREATE TABLE orders (
id UUID DEFAULT SYS_GUID() PRIMARY KEY,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP
);
Note: SYS_GUID() is a proprietary 16-byte value, not a strict RFC 4122 UUID. The version and variant bits are not set. If you need standard RFC UUIDs in Oracle, generate in your application code and insert as UUID.
For Oracle 21 and earlier, use RAW(16):
CREATE TABLE orders (
id RAW(16) PRIMARY KEY,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP
);
Indexing
Every engine indexes UUIDs the same way: B-tree on the native type. The performance differences are entirely about whether the UUIDs are random (v4 — bad for clustered indexes) or sequential (v7, NEWSEQUENTIALID — good).
For non-clustered or hash-partitioned indexes, v4 is fine — the page-split problem only applies to ordered storage like a clustered primary key.
Storage comparison
| Approach | Bytes per ID |
|---|---|
| Native UUID type | 16 (recommended everywhere) |
BINARY(16) / BLOB(16) / RAW(16) | 16 |
CHAR(36) (with hyphens) | 36 |
VARCHAR(37) (canonical) | ~37 |
CHAR(32) (no hyphens) | 32 |
TEXT storing canonical | ~37 |
The 2× storage difference between native (16 bytes) and text (~37 bytes) compounds at every level: row size, index size, working-set memory, replication bandwidth.
Migration: text → uuid
Common scenario — historical schema stored UUIDs as varchar(36) or text. Migrating to the native type:
-- PostgreSQL
ALTER TABLE users
ALTER COLUMN id TYPE uuid USING id::uuid;
-- SQL Server (more involved, requires recreating the column)
ALTER TABLE Users ADD IdNew uniqueidentifier NULL;
UPDATE Users SET IdNew = CAST(Id AS uniqueidentifier);
-- then drop and rename
Test on a copy first. Some engines need a brief table lock for this.
Migration: v4 → v7
Don’t backfill old rows. Just change the default for new ones:
ALTER TABLE orders ALTER COLUMN id SET DEFAULT uuidv7(); -- PG 18+
Existing v4 IDs stay v4 forever. New rows get v7. The version is encoded in each ID, so anything that needs to know can check.
Common pitfalls
- Storing as
varchar(36)“for readability” — costs you 2× storage forever. The performance penalty is real on large tables. - MySQL’s
UUID_TO_BIN(x, 1)with v7 — don’t. The byte-swap is for v1 timestamps, not v7. UseUUID_TO_BIN(x)(no second arg) with v7. - Oracle’s
SYS_GUID()is not RFC-compliant — version/variant bits aren’t set. If a downstream system expects standard UUIDs, generate in your application code instead. - SQL Server’s
NEWID()for clustered primary keys — random Guids fragment the index. UseNEWSEQUENTIALID()or generate v7 in .NET.
Try the tools
- UUID v7 generator — primary keys for any database
- Bulk UUID generator — seed data
- UUID as primary key — performance discussion
- PostgreSQL UUID guide — Postgres-specific deep dive
- C# / .NET Guid guide — SQL Server-side patterns