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;
```