TL; WR: Comment interroger la moyenne de la somme mensuelle, lorsque certains mois n'ont pas d'enregistrement (donc devrait être 0)?
Mes enfants rapportent quotidiennement combien de temps ils ont fait les tâches ménagères (dans une base de données PostgreSQL). Mon ensemble de données ressemble alors à ceci:
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) ;
Je veux savoir combien font-ils en moyenne par mois. Concrètement, le résultat que je souhaite est:
=(120+0+0)÷3
=(30+0+30)÷3
=([10+10]+[10+10]+[10+10])÷3
Certains mois, je n'ai pas de dossier pour certains utilisateurs (par exemple, Alice en février et mars). Ainsi, l'exécution de la requête imbriquée suivante ne renvoie pas le résultat souhaité; en effet, cela ne prend pas en compte le fait que, parce qu'il n'y a pas d'enregistrement pour ces mois, la contribution d'Alice en février et mars devrait être de 0 (ici la moyenne est incorrectement calculée comme 120).
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 ;
Ainsi, j'ai construit une requête assez lourde comme suit:
-- 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
Cette requête fonctionne, mais est assez volumineuse.
Comment interroger la moyenne de la somme mensuelle plus élégamment que ci-dessus, en prenant en compte «aucun enregistrement» somme mensuelle = 0 »?
Remarque: il est prudent de supposer que je veux calculer la moyenne sur les mois qui ont au moins un enregistrement seulement (c'est-à-dire qu'il est normal de ne pas considérer décembre ou avril ici.)
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
4 Réponses :
Vous pouvez utiliser un CTE pour construire un calendrier-table:
-- EXPLAIN WITH cal AS ( -- The unique months SELECT DISTINCT date_trunc('mon', zdate) AS tick FROM chores_record ) , cnt AS ( -- the number of months (a scalar) SELECT COUNT(*) AS nmonth FROM cal ) SELECT x.zuser , SUM(x.duration) AS tot_duration , SUM(x.duration) / SUM(c.nmonth) AS Averarage_month -- this is ugly ... FROM cal t JOIN cnt c ON true -- This is ugly LEFT JOIN chores_record x ON date_trunc('mon', x.zdate) = t.tick GROUP BY x.zuser ;
Vous avez besoin de deux jeux de données supplémentaires pour cela: la liste des enfants et la liste des mois:
with ... select k."user", avg(coalesce(d.duration, 0)) as duration from k cross join m left join d on (k."user" = d."user" and m."month" = d."month") group by k."user" order by k."user"; âââââââââââ¬ââââââââââââââââââââââ â user â duration â âââââââââââ¼âââââââââââââââââââââ⤠â Alice â 40.0000000000000000 â â Bob â 20.0000000000000000 â â Charlie â 20.0000000000000000 â âââââââââââ´ââââââââââââââââââââââ
Et la dernière étape est le calcul des moyennes:
with k as (/* list of kids */ select distinct "user" from chores_record), m as (/* list of months in format "yyyy-mm-01" */ select distinct date_trunc('month', "date") as "month" from chores_record), d as (/* sums by moths and kids */ select date_trunc('month', "date") as "month", "user", sum(duration) as duration from chores_record group by 1, 2) select m."month", k."user", coalesce(d.duration, 0) as duration from k cross join m left join d on (k."user" = d."user" and m."month" = d."month") order by "month", "user"; ââââââââââââââââââââââââââ¬ââââââââââ¬âââââââââââ â month â user â duration â ââââââââââââââââââââââââââ¼ââââââââââ¼ââââââââââ⤠â 2020-01-01 00:00:00+02 â Alice â 120 â â 2020-01-01 00:00:00+02 â Bob â 30 â â 2020-01-01 00:00:00+02 â Charlie â 20 â â 2020-02-01 00:00:00+02 â Alice â 0 â â 2020-02-01 00:00:00+02 â Bob â 0 â â 2020-02-01 00:00:00+02 â Charlie â 20 â â 2020-03-01 00:00:00+02 â Alice â 0 â â 2020-03-01 00:00:00+02 â Bob â 30 â â 2020-03-01 00:00:00+02 â Charlie â 20 â ââââââââââââââââââââââââââ´ââââââââââ´âââââââââââ
Comme le cas d'utilisation était plus petit (pas des millions de lignes), une approche simple consiste à rechercher séparément
rejoignez les 2 pour avoir la réponse
Alice,40 Bob,20 Charlie,20
select "user", totalHours/monthCount from (select "user", sum(duration) totalHours from chores_record group by "user") as a, (select count(distinct(to_char(date, 'YYYYMM'))) monthCount from chores_record) as b ;
Bien que ce code puisse répondre à la question, fournir un contexte supplémentaire sur la manière et / ou pourquoi il résout le problème améliorerait la valeur à long terme de la réponse.
Dans Postgres, je recommanderais generate_series()
pour construire la table de calendrier, puis l'agrégation. L'avantage est que cela fonctionne même s'il y a des mois où aucun utilisateur n'était actif.
select u."user", avg(coalesce(c.duration, 0)) avg_duration from ( select generate_series(date_trunc('month', min(date)), date_trunc('month', max(date)), '1 month') as dt from chores_record ) d cross join (select distinct "user" from chores_record) u left join ( select "user", date_trunc('month', date) as dt, sum(duration) as duration from chores_record c group by "user", date_trunc('month', date) ) c on c."user" = u."user" and c.dt = d.dt group by u."user"
generate_series()
génère tous les débuts de mois entre la date la plus ancienne et la plus récente du tableau. Ensuite, nous cross join
cela avec la liste des utilisateurs distincts (dans une situation réelle, vous auriez probablement une table référentielle pour stocker les utilisateurs, que vous utiliseriez à la place). Ensuite, nous agrégons la table d'origine par utilisateur et par mois, puis nous la left join
avec les combinaisons utilisateur / mois. La dernière étape est l'agrégation externe.
generate_series()
est à la fois intelligent et puissant, merci: c'est exactement le genre de «moyens plus intelligents» que je recherchais! (Petite faute de frappe: l'avant-dernière ligne doit lire […] and c.dt = d.dt
)
@ebosi: en effet. J'ai corrigé la faute de frappe.
Envisagez de gérer les problèmes d'affichage des données (par exemple, des données manquantes) dans le code de l'application
@Strawberry semble intéressant, mais je ne suis pas sûr de bien comprendre. Pourriez-vous détailler ou donner un exemple de ce que vous voulez dire?