Assumptions
- Current Postgres 9.6.
actions.created
is a bigint
column holding a count of ms since 1970-01-01 UTC (epoch in microseconds).
actions.groupid
is integer
.
The basic query:
SELECT date_trunc('hour', to_timestamp(created / 1000) AT TIME ZONE 'UTC') AS hour -- !
, group_id
, count(*)::int AS ct -- count never returns NULL
FROM actions
WHERE group_id IN (1489, 1616) -- more?
AND created >= EXTRACT(EPOCH FROM (now() AT TIME ZONE 'UTC')::date - 7) * 1000 -- !
GROUP BY 1, 2
ORDER BY 1, 2;
Major points
Don't make your query depend on the time zone setting of the session or you may be in for surprises: The result changes with the setting. Remember that "today" is a function of the time zone you are in. Detailed explanation:
Replace 'UTC' in above query with the time zone you want your times and dates to reflect. Use a time zone name (not an abbreviation) to be perfectly sure.
Transform the WHERE
condition:
TO_TIMESTAMP(created / 1000)::timestamp >= current_date-7
to the equivalent:
created >= EXTRACT(EPOCH FROM (now() AT TIME ZONE 'UTC')::date - 7) * 1000`
That's generally faster and can use an index on created
. (Makes it "sargable".)
Depending on data distribution, size of the table and typical you may want a multicolumn index on (group_id, created DESC)
to optimize read performance. Performance optimization depends on the complete picture, though ...
The aggregate function count()
never returns NULL. COALESCE(count(*), 0)
is useless noise and can safely be replaced with just count(*)
.
Pivot
You want the result pivoted with one column per selected group_id
. Use crosstab()
from the additional tablefunc
module for fastest results. If you are not familiar with it, read basic instructions here first:
SELECT * FROM crosstab(
$$SELECT date_trunc('hour', to_timestamp(created / 1000) AT TIME ZONE 'UTC')
, group_id
, count(*)::int
FROM actions
WHERE group_id IN (1489, 1616) -- more?
AND created >= EXTRACT(EPOCH FROM (now() AT TIME ZONE 'UTC')::date - 7) * 1000
GROUP BY 1, 2
ORDER BY 1, 2$$
,$$SELECT unnest('{1489, 1616}'::int[])$$
) AS ct (hour timestamp, "1489" int, "1616" int); -- more?
Missing elements are NULL. You cannot catch this with COALESCE
around count()
in the inner query. Use COALESCE
in the outer query instead. Like:
SELECT hour, COALESCE("1489", 0) AS "1489", COALESCE("1616", 0) AS "1616" FROM crosstab( ...
Note that hours without any rows are still missing in the result. Use an OUTER JOIN
to a complete time grid if you want empty rows in the result. Like: