TL;WR: How to query the average of monthly sum, when some months don't have record (so should be 0)?
Background
My kids are reporting daily how long they have done chores (in a PostgreSQL database). My dataset then looks like this:
date,user,duration
2020-01-01,Alice,120
2020-01-02,Bob,30
2020-01-03,Charlie,10
2020-01-23,Charlie,10
2020-02-03,Charlie,10
2020-02-23,Charlie,10
2020-03-02,Bob,30
2020-03-03,Charlie,10
2020-03-23,Charlie,10
I want to know how much, on average, do they do per month. Concretely, the result I want is:
- Alice: 40
=(120+0+0)÷3 - Bob: 20
=(30+0+30)÷3 - Charlie: 20
=([10+10]+[10+10]+[10+10])÷3
Problem
On some months, I don't have record for some users (e.g., Alice in February and March). Thus, running the following nested query doesn't return the result I want; indeed, this doesn't take in consideration that, because there is no record for these months, Alice's contribution in February and March should be 0 (here the average is wrongly computed as 120).
-- this does not work
SELECT
"user",
round(avg(monthly_duration)) as avg_monthly_sum
FROM (
SELECT
date_trunc('month', date),
"user",
sum(duration) as monthly_duration
FROM
public.chores_record
GROUP BY
date_trunc('month', date),
"user"
) AS monthly_sum
GROUP BY
"user"
;
-- Doesn't return what I want:
--
-- "unique_user","avg_monthly_sum"
-- "Alice",120
-- "Bob",30
-- "Charlie",20
Thus, I have built a quite cumbersome query as follows:
- List the unique months,
- List the unique users,
- Generate the months×users combinations,
- Add the monthly sum from the original data,
- Get the average of monthly sum (assuming 'null' = 0).
SELECT
unique_user,
round(avg(COALESCE(monthly_duration, 0))) -- COALESCE transforms 'null' into 0
FROM (
-- monthly duration with 'null' if no record for that user×month
SELECT
month_user_combinations.month,
month_user_combinations.unique_user,
monthly_duration.monthly_duration
FROM
(
(
-- all months×users combinations
SELECT
month,
unique_user
FROM (
(
-- list of unique months
SELECT DISTINCT
date_trunc('month', date) as month
FROM
public.chores_record
) AS unique_months
CROSS JOIN
(
-- list of unique users
SELECT DISTINCT
"user" as "unique_user"
FROM
public.chores_record
) AS unique_users
)
) AS month_user_combinations
LEFT OUTER JOIN
(
-- monthly duration for existing month×user combination only
SELECT
date_trunc('month', date) as month,
"user",
sum(duration) as monthly_duration
FROM
public.chores_record
GROUP BY
date_trunc('month', date),
"user"
) AS monthly_duration
ON (
month_user_combinations.month = monthly_duration.month
AND
month_user_combinations.unique_user = monthly_duration.user
)
)
) AS monthly_duration_for_all_combinations
GROUP BY
unique_user
;
This query works, but is quite bulky.
Question
How to query the average of monthly sum more elegantly than above, taking “no record ⇒ monthly sum = 0” into account?
Note: it is safe to assume that I want to compute the average on the months that have at least one record only (i.e. it's normal not to consider December or April here.)
MWE
CREATE TABLE public.chores_record
(
date date NOT NULL,
"user" text NOT NULL,
duration integer NOT NULL,
PRIMARY KEY (date, "user")
);
INSERT INTO
public.chores_record(date, "user", duration)
VALUES
('2020-01-01','Alice',120),
('2020-01-02','Bob',30),
('2020-01-03','Charlie',10),
('2020-01-23','Charlie',10),
('2020-02-03','Charlie',10),
('2020-02-23','Charlie',10),
('2020-03-02','Bob',30),
('2020-03-03','Charlie',10),
('2020-03-23','Charlie',10)
;