-- DDL for sample event table CREATE TABLE events ( event_id SERIAL PRIMARY KEY, event_timestamp TIMESTAMP WITH TIME ZONE NOT NULL );
-- SQL Query for hourly aggregation with empty buckets and time zone conversion WITH hourly_series AS ( SELECT generate_series( '2023-10-26 00:00:00'::timestamp with time zone AT TIME ZONE 'UTC', '2023-10-28 23:00:00'::timestamp with time zone AT TIME ZONE 'UTC', '1 hour'::interval ) AS hourly_bucket_utc ), local_hourly_series AS ( SELECT hourly_bucket_utc, (hourly_bucket_utc AT TIME ZONE 'UTC' AT TIME ZONE 'America/New_York') AS hourly_bucket_start FROM hourly_series ) SELECT lhs.hourly_bucket_start, COALESCE(COUNT(e.event_id), 0) AS event_count FROM local_hourly_series lhs LEFT JOIN events e ON e.event_timestamp >= lhs.hourly_bucket_utc AND e.event_timestamp < (lhs.hourly_bucket_utc + INTERVAL '1 hour') GROUP BY lhs.hourly_bucket_start ORDER BY lhs.hourly_bucket_start;
-- Expected Plan Notes The query will likely start by materializing the generate_series output, creating a temporary table or a CTE of all hourly buckets. This series is then joined with the events table using a LEFT JOIN. For optimal performance, the join condition e.event_timestamp >= lhs.hourly_bucket_utc AND e.event_timestamp < (lhs.hourly_bucket_utc + INTERVAL '1 hour') will benefit from an index on event_timestamp. The GROUP BY clause will then aggregate the counts. The COALESCE function ensures that hours with no matching events from the LEFT JOIN correctly show a count of 0 instead of NULL. The time zone conversion happens early in the CTEs, ensuring the join is performed on UTC timestamps, which is efficient given the event_timestamp is stored in UTC.
-- Index Recommendation CREATE INDEX idx_events_event_timestamp ON events (event_timestamp);
-- Test Data INSERT INTO events (event_timestamp) VALUES ('2023-10-26 00:15:00+00'), -- Hour 00, Day 1 ('2023-10-26 00:45:00+00'), -- Hour 00, Day 1 ('2023-10-26 01:05:00+00'), -- Hour 01, Day 1 ('2023-10-26 01:20:00+00'), -- Hour 01, Day 1 ('2023-10-26 03:30:00+00'), -- Hour 03, Day 1 (gap in 02) ('2023-10-27 10:00:00+00'), -- Hour 10, Day 2 ('2023-10-27 10:59:00+00'), -- Hour 10, Day 2 ('2023-10-27 11:15:00+00'), -- Hour 11, Day 2 ('2023-10-28 22:00:00+00'), -- Hour 22, Day 3 ('2023-10-28 22:30:00+00'); -- Hour 22, Day 3