Right, let’s talk about UUIDs. You’ve probably seen these 36-character monstrosities (a0eebc99-9c0b-4d8b-b654-9b1f7d4e8b66) lurking in your database. They look like someone fell asleep on their keyboard, but I promise there’s a method to the hexadecimal madness. We use them when we need a unique identifier that can be generated anywhere—on your client, in your API, in ten different microservices at once—without anyone having to call a central “id-issuing authority” (like a database sequence) and risk a bottleneck or a collision.

PostgreSQL is a bit of an overachiever here. It gives you not one, but three ways to handle UUIDs. Two are great, one is a historical relic you should actively avoid. Let’s break them down.

The Storage: uuid vs. varchar

First, the most important detail: always use the native uuid data type. I know the siren song of varchar(36) is tempting. “It’s just text, right?” Wrong. The uuid type is stored as a 128-bit number, which is a compact 16 bytes. That varchar(36)? It’s bloated, slow, and a pain to index properly. The uuid type validates its input, performs lightning-fast comparisons, and its indexes are tiny and efficient. Using varchar is like shipping a single pebble in a massive cardboard box; using uuid is like putting it in your pocket.

-- Do this. Always.
CREATE TABLE orders (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    details text
);

-- For the love of all that is good, don't do this.
CREATE TABLE orders_bad (
    id varchar(36) PRIMARY KEY, -- Go sit in the corner and think about what you've done.
    details text
);

Generation: The Good, The Bad, The Ugly

Here’s where PostgreSQL’s history shows. You have three main options, but only one you should use for new work.

gen_random_uuid()

This is your workhorse. It’s part of the pg_crypto extension and generates a Version 4 UUID, which is basically just 122 bits of glorious, cryptographically-strong randomness. It’s the fastest and most secure built-in method. The chances of a collision are so astronomically low you’d have a better chance of being elected pope while being struck by lightning. Twice. You need to enable the extension first.

CREATE EXTENSION IF NOT EXISTS pg_crypto;

CREATE TABLE items (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    name text NOT NULL
);

-- Inserting will automatically generate a random UUID
INSERT INTO items (name) VALUES ('Vintage Rubber Chicken');

uuid_generate_v4()

This function from the uuid-ossp extension does the same thing as gen_random_uuid(): it makes a Version 4 UUID. So why does it exist? History. uuid-ossp was the only game in town for a long time. pg_crypto’s gen_random_uuid() is now the preferred method because a) it doesn’t require a separate extension if you’re already using pg_crypto for other things, and b) it’s arguably more “native.” They’re functionally identical for this use case. Don’t feel bad if you’ve been using this one; it’s fine. But for new code, gen_random_uuid() is the more direct path.

uuid_generate_v1()

This generates a Version 1 UUID, which combines your MAC address and the current timestamp. This is the one you avoid. Leaking your MAC address is a privacy nightmare, and they create a predictable order which can sometimes lead to unintentional data exposure. It’s a relic. Don’t use it.

The pg_crypto Extension and Best Practices

Enabling pg_crypto is trivial, but it’s a required step. It’s not part of the core PostgreSQL distribution because not every database needs cryptographic functions. But if you’re using UUIDs, you almost certainly should have it enabled.

The best practice is to generate your UUIDs at the database level as a DEFAULT value, as shown above. This ensures consistency. However, the real power of UUIDs shines when you generate them in your application code before the insert ever happens. This allows you to create the ID, create related objects in different services or tables, and then commit everything, all without a round-trip to the database just to get an ID. Most modern languages have excellent libraries for this (e.g., uuid in JavaScript, uuid in Python).

-- Your app generates the UUID and sends it along.
-- This is perfectly valid and often preferable.
INSERT INTO items (id, name) VALUES ('a0eebc99-9c0b-4d8b-b654-9b1f7d4e8b66', 'Vintage Rubber Chicken');

Just remember, if you’re going to do it in the app, you must be consistent. Don’t mix and match application-generated IDs and database-generated defaults on the same table, or you’ll inevitably DEFAULT your way into a primary key collision. Pick one strategy and stick with it. My advice? Let the app handle it. It’s more flexible and demonstrates the true, distributed power of the UUID.