Generate a series of timestamp ranges, join with your data, calculate the overlap and aggregate: ``` SELECT user_id, bucket, coalesce(sum(upper(inters) - lower(inters)), 0) AS duration FROM (SELECT user_id, lower(ranges.r) AS bucket, tsrange(tab.start, tab.end, '[)') * ranges.r AS inters FROM (SELECT tsrange(st, st + '1 hour'::interval, '[)') AS r FROM generate_series( '2021-01-01 00:00:00'::timestamp, '2021-12-31 23:00:00'::timestamp, '1 hour'::interval ) AS g(st) ) AS ranges LEFT JOIN tab ON tsrange(tab.start, tab.end, '[)') * ranges.r <> 'empty' ) AS intersections GROUP BY tab.user_id, bucket; ```