Agrupar resultados de consultas por mes y año en postgresql

157

Tengo la siguiente tabla de base de datos en un servidor 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

Me gustaría crear una consulta que proporcione el SUMde la Salescolumna y agrupe los resultados por mes y año de la siguiente manera:

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

¿Hay una manera simple de hacer eso?

Frechi
fuente

Respuestas:

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

A petición de Radu, explicaré esa consulta:

to_char(date,'Mon') as mon, : convierte el atributo "fecha" en el formato definido de la forma abreviada del mes.

extract(year from date) as yyyy : La función "extraer" de Postgresql se utiliza para extraer el año AAAA del atributo "fecha".

sum("Sales") as "Sales" : La función SUMA () suma todos los valores de "Ventas" y proporciona un alias que distingue entre mayúsculas y minúsculas, manteniendo la distinción entre mayúsculas y minúsculas mediante comillas dobles.

group by 1,2: La función GROUP BY debe contener todas las columnas de la lista SELECT que no forman parte del agregado (es decir, todas las columnas que no están dentro de las funciones SUM / AVG / MIN / MAX, etc.). Esto le dice a la consulta que el SUM () debe aplicarse para cada combinación única de columnas, que en este caso son las columnas de mes y año. La parte "1,2" es una abreviatura en lugar de usar los alias de columna, aunque probablemente sea mejor usar las expresiones completas "to_char (...)" y "extract (...)" para facilitar la lectura.

bma
fuente
55
No creo que dar una respuesta sin una explicación sea una muy buena idea, especialmente para principiantes. Debería haber explicado la lógica detrás de su respuesta, tal vez al menos un poco (aunque puede parecer simple y directo para el resto de nosotros).
Radu Gheorghiu
1
@BurakArslan ¿Los resultados se parecían a lo que el OP solicitó específicamente?
bma
2
@rogerdpack, la salida de date_truncno es exactamente lo que el autor de la pregunta quería: select date_trunc('month', timestamp '2001-02-16 20:38:40')::date=>2001-02-01
pisaruk
2
Me gusta la idea de usar date_truncen la group bycláusula.
pisaruk
1
Posibles problemas "el campo debe estar en el grupo por la cláusula" ... Es mejor usar OVER (PARTITION BY).
Zon
318

No puedo creer que la respuesta aceptada tenga tantos votos positivos, es un método horrible.

Aquí está la forma correcta de hacerlo, con date_trunc :

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

Es una mala práctica, pero puede ser perdonado si usa

 GROUP BY 1

en una consulta muy simple

También puedes usar

 GROUP BY date_trunc('month', txn_date)

si no quieres seleccionar la fecha.

Burak Arslan
fuente
66
desafortunadamente, la salida de date_truncno es lo que el autor de la pregunta esperaba: select date_trunc('month', timestamp '2001-02-16 20:38:40')=> 2001-02-01 00:00:00.
pisaruk
44
Estoy de acuerdo en que este método es mejor. No estoy seguro, pero creo que también es más eficiente, ya que solo hay una agrupación en lugar de dos. Si necesita reformatear la fecha, puede hacerlo luego usando los métodos descritos en otras respuestas:to_char(date_trunc('month', txn_date), 'YY-Mon')
Paweł Sokołowski
1
Sí, el número de votos para la respuesta aceptada es alucinante. date_truncfue creado para este propósito exacto. no hay razón para crear dos columnas
allenwlee
2
¡Muy agradable! Esta es una respuesta superior, especialmente porque también puede ordenar. ¡Votado!
bobmarksie
1
Otro ejemplo más en el que la respuesta más votada debería aparecer antes de la respuesta aceptada
Brian Risk,
33

to_char en realidad te permite sacar el año y el mes de una sola vez!

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'

o en el caso del ejemplo del usuario anterior:

select to_char(date,'YY-Mon') as year_month
       sum("Sales") as "Sales"
from some_table
group by 1;
mgoldwasser
fuente
66
Recomiendo encarecidamente que no haga esto si tiene una cantidad decente de datos en su tabla. Esto funciona mucho peor que el date_truncmétodo cuando se realiza el grupo por. Experimentando en un DB que tengo a mano, en una mesa con 270k filas, el método date_trunc supera el doble de la velocidad de TO_CHAR
Chris Clark
@ChrisClark si el rendimiento es una preocupación, estoy de acuerdo en que podría tener sentido usar date_trunc, pero en algunos casos es preferible tener una cadena de fecha formateada, y si está utilizando un almacén de datos con rendimiento, el cálculo adicional podría no ser un factor decisivo . Por ejemplo, si está ejecutando un informe analítico rápido con desplazamiento al rojo, y generalmente toma 3 segundos, una consulta de 6 segundos probablemente esté bien (aunque, si está ejecutando informes, el cálculo adicional podría ralentizar las cosas en un porcentaje menor, porque hay una sobrecarga computacional más grande)
mgoldwasser
1
aún puede hacerlo, solo realice el formateo como un paso separado 'ajustando' el grupo por consulta. Por ejemplo, SELECT to_char (d, 'YYYY-DD') FROM (SELECT date_trunc ('month', d) AS "d" FROM tbl) AS foo. ¡Lo mejor de ambos mundos!
Chris Clark
1
Esta solución es simple y elegante. Me gusta y en mi caso es lo suficientemente rápido. Gracias por esta respuesta!
guettli
5

Hay otra forma de lograr el resultado utilizando la función date_part () en 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)

Gracias

Nayan
fuente
1

bma respuesta es genial! Lo he usado con ActiveRecords, aquí está si alguien lo necesita en 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"
)
mekdigital
fuente
3
o puede hacerlo yourscopeorclass.group("extract(year from tablename.colname)")y puede encadenarlo 3 veces para obtener año, mes, día
nruth
1

Eche un vistazo al ejemplo E de este tutorial -> https://www.postgresqltutorial.com/postgresql-group-by/

Debe llamar a la función en su GROUP BY en lugar de llamar al nombre del atributo virtual que creó en select. Estaba haciendo lo que todas las respuestas anteriores recomendaban y recibía un column 'year_month' does not existerror.

Lo que funcionó para mí fue:

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

Postgres tiene pocos tipos de marcas de tiempo:

marca de tiempo sin zona horaria - (Preferible para almacenar marcas de tiempo UTC) Lo encuentra en el almacenamiento de bases de datos multinacionales. El cliente en este caso se encargará de la compensación de zona horaria para cada país.

marca de tiempo con zona horaria: el desplazamiento de la zona horaria ya está incluido en la marca de tiempo.

En algunos casos, su base de datos no utiliza la zona horaria, pero aún necesita agrupar registros con respecto a la zona horaria local y el horario de verano (por ejemplo, https://www.timeanddate.com/time/zone/romania/bucharest )

Para agregar una zona horaria, puede usar este ejemplo y reemplazar el desplazamiento de la zona horaria con el suyo.

"your_date_column" at time zone '+03'

Para agregar el desplazamiento de horario de verano +1 específico para el horario de verano, debe verificar si su marca de tiempo cae en un horario de verano. Como esos intervalos varían con 1 o 2 días, usaré una aproximación que no afecta los registros de fin de mes, por lo que en este caso puedo ignorar el intervalo exacto de cada año.

Si se debe generar una consulta más precisa, debe agregar condiciones para crear más casos. Pero más o menos, esto funcionará bien al dividir los datos por mes con respecto a la zona horaria y SummerTime cuando encuentre la marca de tiempo sin zona horaria en su base de datos:

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)
profimedica
fuente