UUID v7 Time Range
Mint a v7 whose timestamp is any instant you pick, and get the smallest and largest v7 that can exist between two of them. Everything runs in your browser.
Mint a v7 at an instant
The timestamp is the instant you choose and the remaining 74 bits are random, so this is a real, usable ID. Useful for seeding test data that has to sort into a specific order: pick an instant per row and the rows come out in that sequence wherever the key is sorted.
Bounds for a date range
Every v7 created between these two instants sorts at or above the minimum and at or below the maximum, so a plain comparison on the primary key finds all of them.
Why a range query works at all
The first 48 bits of a v7 are a Unix millisecond count, big-endian. Nothing else in the ID comes before it. Two v7s created a millisecond apart therefore compare in the order they were created, and the same holds for any two IDs whose timestamps differ.
So a range of time is a range of values. The minimum on this page is the smallest v7 that could carry the start millisecond, the maximum is the largest that could carry the end millisecond, and every ID issued in between falls between them. Both ends are inclusive: the maximum has every non-fixed bit set, so an ID created during the end millisecond itself is still inside the range.
Comparing UUIDs as strings
In Postgres the uuid type compares by its sixteen bytes, and the literal you write in the query is parsed into those bytes before anything is compared. The text form is only how you spell it.
If the column is text or varchar, the same query still gives the right answer. Lowercase hex sorts in the same order as the numbers it spells, and every canonical UUID is the same length with dashes in the same places. Watch for mixed case, since 'A' sorts before 'a' under a byte-wise collation and will split your range in half.
On a binary, bytea or BINARY(16) column you need the raw bytes instead. Strip the dashes and hand the sixteen bytes to whatever your database uses for hex literals, such as UNHEX() in MySQL or '\x…'::bytea in Postgres.
You already have the index
The usual way to ask “what happened last Tuesday” is a created_at column with its own index. That index costs write throughput on every insert and disk on every row, and it duplicates information the primary key already carries.
When the key is a v7, the time range is a range scan on the primary key index. Rows in a time window are physically adjacent under a clustered key, which is the cheapest shape this query can have.
The bounds are not real IDs
Never insert one. The minimum and maximum are fence posts, chosen because no real v7 from that millisecond can sort outside them. A row keyed by the maximum would sit at the far edge of its millisecond and would collide with the next range query that used the same bound.
They are valid v7s by the letter of RFC 9562, which is why the version and variant bits stay fixed. Anything else would sort into the wrong place.
Everyone else can do this too
A v7 publishes the millisecond it was created to anyone holding it. The same arithmetic that turns your date range into bounds turns a customer's invoice ID into a signup time, and a handful of IDs into a signup rate. Paste one into the UUID decoder to see what it gives away.
That is the trade a sortable key asks for. If a creation time is sensitive, keep the v7 internal and hand out something opaque. See the UUID v7 generator for how the format is put together.