On 1st of July 2026, Masahiko Sawada committed patch:
Add min() and max() aggregate support for uuid. The uuid type already has a full set of comparison operators and a btree operator class, so it is totally ordered. min() and max() were the only common aggregates missing for it. Add the uuid_larger() and uuid_smaller() support functions and register the min(uuid) and max(uuid) aggregates that use them. uuid values are compared lexicographically over their 128 bits. For UUIDv7, whose most significant bits encode a Unix timestamp, this coincides with chronological order, so min() and max() return the oldest and newest values. Bump catalog version. Author: Tristan Partin <tristan@partin.io> Reviewed-by: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com> Reviewed-by: Zsolt Parragi <zsolt.parragi@percona.com> Reviewed-by: Masahiko Sawada <sawada.mshk@gmail.com> Discussion: https://postgr.es/m/DJGML0T9FCDV.3VA29JLODXEHZ@partin.io
So, this is pretty obvious as to what it means, but anyway, let's see it in action.
Obviously, it will make most sense with UUID v7 – the one with timestamps. So, let's try it:
=$ psql -c "create table test (x uuid);" =$ for i in {1..20}; do psql -c "insert into test (x) values (uuidv7())"; sleep "$(( RANDOM % 5))"; done
So, now I have 20 records:
=$ select x, uuid_extract_timestamp(x) from test; x │ uuid_extract_timestamp ──────────────────────────────────────┼──────────────────────────── 019f46d6-a86c-7a87-b1d2-d3a0400333a7 │ 2026-07-09 14:25:00.78+02 019f46d6-b81d-7dc5-ac8f-2cb913c268c9 │ 2026-07-09 14:25:04.797+02 019f46d6-c002-7527-a691-78823b82f33c │ 2026-07-09 14:25:06.818+02 019f46d6-c00b-70d9-8ba2-0e15a3ca332e │ 2026-07-09 14:25:06.827+02 019f46d6-cfb4-73d6-b3e1-f9d1cc5bde5a │ 2026-07-09 14:25:10.836+02 019f46d6-df69-7bf8-b0b4-9266f2780b5d │ 2026-07-09 14:25:14.857+02 019f46d6-eb41-7973-a1e9-e2a27079b02d │ 2026-07-09 14:25:17.889+02 019f46d6-f326-7e1a-99ed-c8a47e07dea9 │ 2026-07-09 14:25:19.91+02 019f46d6-fef6-7e3b-830d-b26f32c15faf │ 2026-07-09 14:25:22.934+02 019f46d6-ff0a-75fc-9010-d2f8f6aa5616 │ 2026-07-09 14:25:22.954+02 019f46d7-06e5-77fd-b7a2-c50041f0f2ff │ 2026-07-09 14:25:24.965+02 019f46d7-0ae1-7040-8c71-150b72e82be1 │ 2026-07-09 14:25:25.985+02 019f46d7-0af4-7a75-98a8-e12f8842912a │ 2026-07-09 14:25:26.004+02 019f46d7-0ee7-7dd3-8ce4-18aec2d106c7 │ 2026-07-09 14:25:27.015+02 019f46d7-1e9e-7070-83c9-7f5a65e33fd9 │ 2026-07-09 14:25:31.038+02 019f46d7-2e53-74bc-937b-bea2368c1412 │ 2026-07-09 14:25:35.059+02 019f46d7-3a1f-7a99-a6fc-ac020b1a5e70 │ 2026-07-09 14:25:38.079+02 019f46d7-49d5-77f2-86dc-698081ef6ef5 │ 2026-07-09 14:25:42.101+02 019f46d7-5597-7487-9b43-b4af82606bb5 │ 2026-07-09 14:25:45.111+02 019f46d7-5993-794e-86e4-c7b72f7aad8b │ 2026-07-09 14:25:46.131+02 (20 rows)
We can now use min and max aggregates:
=$ select min(x), uuid_extract_timestamp(min(x)), max(x), uuid_extract_timestamp(max(x)) from test \gx ─[ RECORD 1 ]──────────┬───────────────────────────────────── min │ 019f46d6-a86c-7a87-b1d2-d3a0400333a7 uuid_extract_timestamp │ 2026-07-09 14:25:00.78+02 max │ 019f46d7-5993-794e-86e4-c7b72f7aad8b uuid_extract_timestamp │ 2026-07-09 14:25:46.131+02
Nice. What about random uuids?
=$ truncate test; TRUNCATE TABLE =$ insert into test (x) select gen_random_uuid() from generate_series(1,20); INSERT 0 20 =$ select min(x) from test; min ────────────────────────────────────── 1184701e-aaef-4460-bc7d-b238f99add62 (1 row) =$ select min(x), max(x) from test; min │ max ──────────────────────────────────────┼────────────────────────────────────── 1184701e-aaef-4460-bc7d-b238f99add62 │ f9acfd38-48fb-400a-8d4c-a9b63ced9500 (1 row) =$ select * from test order by x; x ────────────────────────────────────── 1184701e-aaef-4460-bc7d-b238f99add62 195ce867-a721-4364-8f82-c3154e2972c9 214a926f-2b8d-47bd-a6a3-df1d27ab007a 2d56a275-6724-4f79-955c-e868e418511e 3e7816b4-7914-4acd-8233-577c40d02803 3ee9ccbe-d1df-4f12-bf83-fba4c3469a46 510279c2-4a5c-4d92-910c-3dfe20ead2c0 5b399a72-62a0-47c1-9f84-9615c8de995f 7db53215-ff0d-4079-844d-46417af5f108 8e55ec52-f264-44ae-bb12-1049d3989d36 ac1357eb-0bf8-4d4b-bf83-0eaa700a9629 bf2235fa-8ea4-4349-9c27-d1af21379546 c9550fa2-d988-4f9f-965f-5ae5ff7de2a0 d24ba0fb-be86-4c6d-b6ba-71a857859e1c dbe79a47-d400-4476-b71c-5f89b60d821f e7ceb7bc-944b-4ec0-9fce-1654be72398f ed5f2487-2d29-4665-ad99-dcd3a1af13bd f0b71187-39a6-4433-bcca-9e20db165d7d f802db33-852e-4b00-b64f-2b00c6138fa8 f9acfd38-48fb-400a-8d4c-a9b63ced9500 (20 rows)
Nice. I can definitely see some usecases for it.
Thanks a lot to everyone that worked on it.