Regrouper les résultats des requêtes par mois et par année dans postgresql


157

J'ai la table de base de données suivante sur un serveur Postgres:

id      date          Product Sales
1245    01/04/2013    Toys    1000     
1245    01/04/2013    Toys    2000
1231    01/02/2013    Bicycle 50000
456461  01/01/2014    Bananas 4546

Je voudrais créer une requête qui donne le SUMde la Salescolonne et regroupe les résultats par mois et par année comme suit:

Apr    2013    3000     Toys
Feb    2013    50000    Bicycle
Jan    2014    4546     Bananas

Y a-t-il un moyen simple de le faire?

Réponses:


219
select to_char(date,'Mon') as mon,
       extract(year from date) as yyyy,
       sum("Sales") as "Sales"
from yourtable
group by 1,2

À la demande de Radu, j'expliquerai cette requête:

to_char(date,'Mon') as mon, : convertit l'attribut "date" dans le format défini de la forme abrégée du mois.

extract(year from date) as yyyy : La fonction "extraire" de PostgreSQL est utilisée pour extraire l'année AAAA de l'attribut "date".

sum("Sales") as "Sales" : La fonction SOMME () additionne toutes les valeurs "Ventes" et fournit un alias sensible à la casse, avec le respect de la casse en utilisant des guillemets doubles.

group by 1,2: La fonction GROUP BY doit contenir toutes les colonnes de la liste SELECT qui ne font pas partie de l'agrégat (c'est-à-dire toutes les colonnes ne figurant pas dans les fonctions SUM / AVG / MIN / MAX, etc.). Cela indique à la requête que le SUM () doit être appliqué pour chaque combinaison unique de colonnes, qui dans ce cas sont les colonnes du mois et de l'année. La partie "1,2" est un raccourci au lieu d'utiliser les alias de colonne, bien qu'il soit probablement préférable d'utiliser les expressions complètes "to_char (...)" et "extract (...)" pour la lisibilité.


5
Je ne pense pas que donner une réponse sans explication soit une très bonne idée, surtout pour les débutants. Vous auriez dû expliquer la logique de votre réponse, peut-être au moins un peu (même si cela peut sembler simple et direct pour le reste d'entre nous).
Radu Gheorghiu

1
@BurakArslan Les résultats ressemblaient-ils à ce que le PO demandait spécifiquement?
bma

2
@rogerdpack, la sortie de date_truncn'est pas exactement ce que le demandeur voulait: select date_trunc('month', timestamp '2001-02-16 20:38:40')::date=>2001-02-01
pisaruk

2
J'aime l'idée d'utiliser date_truncdans l' group byarticle.
pisaruk

1
Problèmes possibles de "champ doit être dans la clause group by" ... Il est préférable d'utiliser OVER (PARTITION BY).
Zon

318

Je ne peux pas croire que la réponse acceptée a autant de votes positifs - c'est une méthode horrible.

Voici la bonne façon de le faire, avec date_trunc :

   SELECT date_trunc('month', txn_date) AS txn_month, sum(amount) as monthly_sum
     FROM yourtable
 GROUP BY txn_month

C'est une mauvaise pratique mais vous pourriez être pardonné si vous utilisez

 GROUP BY 1

dans une requête très simple.

Vous pouvez aussi utiliser

 GROUP BY date_trunc('month', txn_date)

si vous ne souhaitez pas sélectionner la date.


6
malheureusement, la sortie de date_truncn'est pas ce que le demandeur attendait: select date_trunc('month', timestamp '2001-02-16 20:38:40')=> 2001-02-01 00:00:00.
pisaruk

4
Je suis d'accord que cette méthode est meilleure. Je ne suis pas sûr mais je pense que c'est aussi plus efficace, car il n'y a qu'un seul regroupement au lieu de deux. Si vous avez besoin de reformater la date, vous pouvez le faire par la suite en utilisant les méthodes décrites dans d'autres réponses:to_char(date_trunc('month', txn_date), 'YY-Mon')
Paweł Sokołowski

1
oui, le nombre de votes pour la réponse acceptée est ahurissant. date_trunca été créé exactement dans ce but. il n'y a aucune raison de créer deux colonnes
allenwlee

2
Très agréable! C'est une réponse supérieure, d'autant plus que vous pouvez également commander. Vote positif!
bobmarksie

1
Encore un autre exemple où la réponse la plus votée devrait apparaître avant la réponse acceptée
Brian Risk

33

to_char vous permet en fait de sortir l'année et le mois d'un seul coup!

select to_char(date('2014-05-10'),'Mon-YY') as year_month; --'May-14'
select to_char(date('2014-05-10'),'YYYY-MM') as year_month; --'2014-05'

ou dans le cas de l'exemple de l'utilisateur ci-dessus:

select to_char(date,'YY-Mon') as year_month
       sum("Sales") as "Sales"
from some_table
group by 1;

6
Je vous déconseille fortement de faire cela si vous avez une quantité décente de données dans votre tableau. Cela fonctionne bien pire que la date_truncméthode lors de l'exécution du groupe par. Expérimenter sur une base de données que j'ai à portée de main, sur une table de 270 000 lignes, la méthode date_trunc est plus de deux fois la vitesse de TO_CHAR
Chris Clark

@ChrisClark si les performances sont un problème, je conviens qu'il peut être judicieux d'utiliser date_trunc, mais dans certains cas, il est préférable d'avoir une chaîne de date formatée, et si vous utilisez un entrepôt de données performant, le calcul supplémentaire peut ne pas être un facteur décisif . Par exemple, si vous exécutez un rapport d'analyse rapide à l'aide de redshift, et que cela prend généralement 3 secondes, une requête de 6 secondes est probablement acceptable (bien que, si vous exécutez des rapports, le calcul supplémentaire peut ralentir les choses d'un pourcentage plus faible, car il y a une surcharge de calcul plus importante)
mgoldwasser

1
vous pouvez toujours le faire - faites simplement le formatage comme une étape distincte en «encapsulant» le groupe par requête. Par exemple, SELECT to_char (d, 'YYYY-DD') FROM (SELECT date_trunc ('month', d) AS "d" FROM tbl) AS foo. Le meilleur des deux mondes!
Chris Clark

1
Cette solution est simple et élégante. J'aime ça et dans mon cas c'est assez rapide. Merci pour cette réponse!
guettli

5

Il existe une autre façon d'obtenir le résultat en utilisant la fonction date_part () dans postgres.

 SELECT date_part('month', txn_date) AS txn_month, date_part('year', txn_date) AS txn_year, sum(amount) as monthly_sum
     FROM yourtable
 GROUP BY date_part('month', txn_date)

Merci


1

La réponse bma est géniale! Je l'ai utilisé avec ActiveRecords, le voici si quelqu'un en a besoin dans Rails:

Model.find_by_sql(
  "SELECT TO_CHAR(created_at, 'Mon') AS month,
   EXTRACT(year from created_at) as year,
   SUM(desired_value) as desired_value
   FROM desired_table
   GROUP BY 1,2
   ORDER BY 1,2"
)

3
ou vous pouvez le faire yourscopeorclass.group("extract(year from tablename.colname)")et vous pouvez enchaîner cela 3 fois pour obtenir l'année, le mois, le jour
nruth

1

Jetez un œil à l'exemple E de ce tutoriel -> https://www.postgresqltutorial.com/postgresql-group-by/

Vous devez appeler la fonction sur votre GROUP BY au lieu d'appeler le nom de l'attribut virtuel que vous avez créé lors de la sélection. Je faisais ce que toutes les réponses ci-dessus recommandaient et j'obtenais une column 'year_month' does not existerreur.

Ce qui a fonctionné pour moi était:

SELECT 
    date_trunc('month', created_at), 'MM/YYYY' AS month
FROM 
    "orders"  
GROUP BY 
    date_trunc('month', created_at)

0

Postgres a quelques types d'horodatages:

horodatage sans fuseau horaire - (préférable pour stocker les horodatages UTC) Vous le trouvez dans le stockage de base de données multinational. Le client dans ce cas se chargera du décalage du fuseau horaire pour chaque pays.

horodatage avec fuseau horaire - Le décalage du fuseau horaire est déjà inclus dans l'horodatage.

Dans certains cas, votre base de données n'utilise pas le fuseau horaire, mais vous devez tout de même regrouper les enregistrements en fonction du fuseau horaire local et de l'heure d'été (par exemple https://www.timeanddate.com/time/zone/romania/bucharest )

Pour ajouter un fuseau horaire, vous pouvez utiliser cet exemple et remplacer le décalage du fuseau horaire par le vôtre.

"your_date_column" at time zone '+03'

Pour ajouter le décalage d'heure d'été +1 spécifique à DST, vous devez vérifier si votre horodatage correspond à un DST d'été. Comme ces intervalles varient avec 1 ou 2 jours, j'utiliserai une approximation qui n'affecte pas les enregistrements de fin de mois, donc dans ce cas, je peux ignorer chaque intervalle exact de l'année.

Si une requête plus précise doit être créée, vous devez ajouter des conditions pour créer plus de cas. Mais en gros, cela fonctionnera bien pour diviser les données par mois en fonction du fuseau horaire et de l'heure d'été lorsque vous trouvez un horodatage sans fuseau horaire dans votre base de données:

SELECT 
    "id", "Product", "Sale",
    date_trunc('month', 
        CASE WHEN 
            Extract(month from t."date") > 03 AND
            Extract(day from t."date") > 26 AND
            Extract(hour from t."date") > 3 AND
            Extract(month from t."date") < 10 AND
            Extract(day from t."date") < 29 AND
            Extract(hour from t."date") < 4
        THEN 
            t."date" at time zone '+03' -- Romania TimeZone offset + DST
        ELSE
            t."date" at time zone '+02' -- Romania TimeZone offset 
        END) as "date"
FROM 
    public."Table" AS t
WHERE 1=1
    AND t."date" >= '01/07/2015 00:00:00'::TIMESTAMP WITHOUT TIME ZONE
    AND t."date" < '01/07/2017 00:00:00'::TIMESTAMP WITHOUT TIME ZONE
GROUP BY date_trunc('month', 
    CASE WHEN 
        Extract(month from t."date") > 03 AND
        Extract(day from t."date") > 26 AND
        Extract(hour from t."date") > 3 AND
        Extract(month from t."date") < 10 AND
        Extract(day from t."date") < 29 AND
        Extract(hour from t."date") < 4
    THEN 
        t."date" at time zone '+03' -- Romania TimeZone offset + DST
    ELSE
        t."date" at time zone '+02' -- Romania TimeZone offset 
    END)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.