1

I have a table with several terabytes of event data in a very simple (id, bucket_id, data, created_at) schema and there is an index like so

create index index_events_on_created_at_and_bucket_id
 on public.events (created_at desc, bucket_id asc);

Now I thought it would be fast to find the id of the most recent event in each bucket with a query like:

select max(created_at), bucket_id from events group by bucket_id;

Explain output:

HashAggregate (cost=170172168.62..170172178.41 rows=979 width=16)
 Group Key: bucket_id
 -> Index Only Scan using index_events_on_created_at_and_bucket_id on events (cost=0.70..156003994.34 rows=2833634856 width=16)

It seems to be using the index, but doing an index scan instead of just grabbing the head value like I expected. Either way, it does not complete in a timely manner. I suppose it's a problem with using the aggregate function in the query, but I don't know how to fix it.

Is there a query that can return the most recent (i.e. first in the index) created_at timestamp for each bucket by fetching it out of this index?

Erwin Brandstetter
186k28 gold badges463 silver badges636 bronze badges
asked Sep 24, 2023 at 21:19

1 Answer 1

4

Better index with leading bucket_id

You want one row per bucket. An index with leading bucket_id will be much more useful.

CREATE INDEX events_bucket_id_created_at_idx ON events (bucket_id, created_at DESC);

Related:

Since you have a very small number of distinct values in bucket_id ("rows=979"), this query technique should give you dramatically faster results, based on my suggested index:

WITH RECURSIVE cte AS (
 ( -- parentheses required
 SELECT bucket_id, created_at
 FROM events
 ORDER BY bucket_id, created_at DESC
 LIMIT 1
 )
 
 UNION ALL
 SELECT e.*
 FROM cte c
 CROSS JOIN LATERAL (
 SELECT e.bucket_id, e.created_at
 FROM events e
 WHERE e.bucket_id > c.bucket_id
 ORDER BY e.bucket_id, e.created_at DESC
 LIMIT 1
 ) e
 WHERE c.bucket_id IS NOT NULL
 )
SELECT * FROM cte
WHERE bucket_id IS NOT NULL;

It emulates a "loose index scan", only picking the "first" row for every distinct bucket_id - exactly what you are looking for.

Note how the sort order in the query meticulously matches the index.

If the visibility map of the table is up to date (i.e. the table is vacuum'ed enough), you get index-only scans. Should apply, since the slow query you demonstrated got an index-only scan, too. (Though that's scanning the whole index instead of just leading entries per bucket). Related:

This assumes both columns of interest to be NOT NULL. Else you have to do more.

If you also have a table bucket with one row per relevant bucket_id, this is even a bit faster, yet:

SELECT b.bucket_id, e.created_at
FROM bucket b
CROSS JOIN LATERAL (
 SELECT e.created_at
 FROM events e
 WHERE e.bucket_id = b.bucket_id
 ORDER BY e.created_at DESC
 LIMIT 1
 ) e
ORDER BY b.bucket_id;

See:

Stuck with index on (created_at DESC, bucket_id ASC)

We can work with the additional meta information from your comments:

I know all the buckets I care about have recent events

You can enhance the queries above, but a different angle based on that should perform better:

SELECT DISTINCT ON (bucket_id)
 bucket_id, created_at
FROM events
WHERE created_at > now() - interval '15 minutes' -- adapt as needed
ORDER BY bucket_id, created_at DESC;

Should be faster when limited to the tiny (?) fraction of the most recent rows. Postgres can read top rows from the index and feed that to DISTINCT ON. About DISTINCT ON:

answered Sep 25, 2023 at 4:23
4
  • I do have a buckets table, but does that final query also require the differently ordered index? the inner query is very fast if I stick in a hardcoded bucket_id but with the cross join it does not complete. This is a readonly connection so I'm trying to use the existing index. Commented Sep 25, 2023 at 12:12
  • To make it fast, you need an index with leading bucket_id for either query. Or any query, for that matter. Well, there are workarounds for special cases. Like, when you know a minimum (recent) timestamp for each bucket. Or you know that the latest entry for each bucket is recent. So we can find it cheaply from the top of an index with leading created_at. We don't want to traverse billions of entries for a hit, not even once. Commented Sep 25, 2023 at 12:30
  • Thank you for that last hint, because I know all the buckets I care about have recent events i was able to add where ... and e.created_at > now() - interval '15 minutes' to your final example query and got results within 5 seconds. Thanks again, very helpful. Commented Sep 25, 2023 at 13:16
  • 1
    @Segfault: Note the addendum. Commented Sep 25, 2023 at 16:34

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.