UUIDv7 is a newer UUID variant designed for modern, distributed systems. Unlike UUIDv4 (fully random), UUIDv7 embeds a Unix timestamp (milliseconds) in the first portion of the identifier while preserving randomness for uniqueness. This makes UUIDv7 globally unique, roughly time-ordered, and safe to generate independently across many writers, without relying on centralized sequences or coordination.
For YugabyteDB applications, UUIDv7 is especially attractive during app modernization. It allows IDs to be generated either inside YSQL or at the application layer, works cleanly in multi-node and multi-region deployments, and provides better operability and debuggability than random UUIDs.
While YugabyteDB doesn’t currently ship a built-in uuidv7() generator the way newer PostgreSQL versions do, in this tip, we’ll show how to add a lightweight uuid_v7() function in YSQL, use it as a column default, and verify the embedded timestamp… all without changing your application code.
✅ Prereqs
Start a local node with yugabyted
./bin/yugabyted start
(Any local cluster method is fine; this is just the quickest for a demo.)
Enable pgcrypto (needed for gen_random_bytes())
CREATE EXTENSION IF NOT EXISTS pgcrypto;
🧪 Step 1: Create a UUIDv7 generator function
This is based on a commonly used YugabyteDB-friendly approach (uses clock_timestamp() to avoid transaction-timestamp surprises). Yugabyte docs explicitly steer people toward clock_timestamp() in cases where you truly need “now, right now.”
CREATE OR REPLACE FUNCTION uuid_v7()
RETURNS uuid
LANGUAGE sql
AS $$
WITH
t AS (
SELECT floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint AS unix_ms
),
r AS (
SELECT gen_random_bytes(16) AS b
),
ts AS (
SELECT
set_byte(
set_byte(
set_byte(
set_byte(
set_byte(
set_byte(
r.b,
0, (((t.unix_ms >> 40) & 255))::int
),
1, (((t.unix_ms >> 32) & 255))::int
),
2, (((t.unix_ms >> 24) & 255))::int
),
3, (((t.unix_ms >> 16) & 255))::int
),
4, (((t.unix_ms >> 8) & 255))::int
),
5, (((t.unix_ms ) & 255))::int
) AS b
FROM t, r
),
v AS (
SELECT
-- version 7 in high nibble of byte 6
set_byte(ts.b, 6, ((get_byte(ts.b, 6) & 15) | 112)::int) AS b
FROM ts
),
out AS (
SELECT
-- RFC 4122 variant in byte 8
set_byte(v.b, 8, ((get_byte(v.b, 8) & 63) | 128)::int) AS b
FROM v
)
SELECT encode(out.b, 'hex')::uuid
FROM out;
$$;
🧪 Step 2: Use UUIDv7 as a column default
Example table:
DROP TABLE IF EXISTS test_uuid7;
CREATE TABLE test_uuid7 (
id uuid NOT NULL DEFAULT uuid_v7(),
payload text,
PRIMARY KEY (id HASH)
);
Insert a few rows:
INSERT INTO test_uuid7(payload) VALUES
('Dasher'),
('Dancer'),
('Prancer'),
('Vixen'),
('Comet'),
('Cupid'),
('Donner'),
('Blitzen'),
('Rudolph');
If you want to show the “time-ish ordering” property:
SELECT id, payload FROM test_uuid7 ORDER BY id;
Example:
yugabyte=# SELECT id, payload FROM test_uuid7 ORDER BY id;
id | payload
--------------------------------------+---------
019b2e24-ead7-7c10-95c5-774cac98a0cd | Dasher
019b2e24-eadb-7b02-8c13-d5b3eb8cecda | Dancer
019b2e24-eadc-7ba7-aabf-5ccf6948b4a5 | Prancer
019b2e24-eadd-762d-a00d-a677071eb211 | Vixen
019b2e24-eade-7a32-8218-e013a87113ad | Comet
019b2e24-eadf-74fc-8d2e-f4a96d574391 | Donner
019b2e24-eadf-77a6-8db3-b38db5b9ae55 | Cupid
019b2e24-eae0-7b56-a9b6-e8f3f3909b7a | Blitzen
019b2e24-eae1-7065-98c6-8437bc1fe6de | Rudolph
(9 rows)
🔍 Bonus: Verify the embedded timestamp in a UUIDv7
UUIDv7 stores the Unix timestamp (milliseconds) in the first 48 bits. You can extract that back out in YSQL and convert it to a human-readable timestamp.
Decode UUIDv7 → approximate timestamp
SELECT
id,
to_timestamp(
(
(get_byte(uuid_send(id), 0)::bigint << 40) |
(get_byte(uuid_send(id), 1)::bigint << 32) |
(get_byte(uuid_send(id), 2)::bigint << 24) |
(get_byte(uuid_send(id), 3)::bigint << 16) |
(get_byte(uuid_send(id), 4)::bigint << 8) |
(get_byte(uuid_send(id), 5)::bigint)
) / 1000.0
) AS decoded_ts
FROM test_uuid7
ORDER BY id;
What this shows:
● The first 6 bytes of the UUID decode to a millisecond Unix timestamp
● The decoded time closely matches when the row was inserted
● Ordering by
idroughly tracks insertion time (with randomness preserved)
This makes UUIDv7 especially nice for:
● debugging,
● log correlation,
● visually inspecting recent vs older rows,
while still being safe for distributed ID generation.
Example:
yugabyte=# SELECT
yugabyte-# id,
yugabyte-# to_timestamp(
yugabyte(# (
yugabyte(# (get_byte(uuid_send(id), 0)::bigint << 40) |
yugabyte(# (get_byte(uuid_send(id), 1)::bigint << 32) |
yugabyte(# (get_byte(uuid_send(id), 2)::bigint << 24) |
yugabyte(# (get_byte(uuid_send(id), 3)::bigint << 16) |
yugabyte(# (get_byte(uuid_send(id), 4)::bigint << 8) |
yugabyte(# (get_byte(uuid_send(id), 5)::bigint)
yugabyte(# ) / 1000.0
yugabyte(# ) AS decoded_ts
yugabyte-# FROM test_uuid7
yugabyte-# ORDER BY id;
id | decoded_ts
--------------------------------------+----------------------------
019b2e24-ead7-7c10-95c5-774cac98a0cd | 2025-12-17 21:08:50.007+00
019b2e24-eadb-7b02-8c13-d5b3eb8cecda | 2025-12-17 21:08:50.011+00
019b2e24-eadc-7ba7-aabf-5ccf6948b4a5 | 2025-12-17 21:08:50.012+00
019b2e24-eadd-762d-a00d-a677071eb211 | 2025-12-17 21:08:50.013+00
019b2e24-eade-7a32-8218-e013a87113ad | 2025-12-17 21:08:50.014+00
019b2e24-eadf-74fc-8d2e-f4a96d574391 | 2025-12-17 21:08:50.015+00
019b2e24-eadf-77a6-8db3-b38db5b9ae55 | 2025-12-17 21:08:50.015+00
019b2e24-eae0-7b56-a9b6-e8f3f3909b7a | 2025-12-17 21:08:50.016+00
019b2e24-eae1-7065-98c6-8437bc1fe6de | 2025-12-17 21:08:50.017+00
(9 rows)
🏁 Conclusion
UUIDv7 is a strong fit for modern, distributed YugabyteDB applications where IDs need to be generated without coordination. With a small helper function, you can safely generate UUIDv7 values directly in YSQL and use them as column defaults, avoiding sequences while still embedding a time component that’s useful for debugging and operational visibility.
While YugabyteDB hash sharding handles distribution automatically, UUIDv7 adds a nice balance of global uniqueness, approximate time ordering, and simplicity, making it a common choice for teams modernizing their applications on YugabyteDB.
Have Fun!
