Waiting for PostgreSQL 20 – Add min() and max() aggregate support for uuid.

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;
                 minmax
──────────────────────────────────────┼──────────────────────────────────────
 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.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.