Simplify calculation of months between 2 dates (postgresql) - postgresql

This question is asked many times and one of the suggested queries to get months between 2 dates is not working.
SELECT date_part('month',age('2016-06-30', '2018-06-30'))
The result of this query is 0. It should be 24 months. Because the months are 06 in both dates.
This works, but it is a bit clumsy compared to the sql server function:
SELECT date_part ('year', f) * 12 + date_part ('month', f)
FROM age ('2016-06-30', '2018-06-30') f
Like sql server (I think):
DATEDIFF(month, date1, date2)
Is there no simple way (like the above) to calculate the months between 2 dates in Postgresql? I prefer not to use a function if it is possible.

Unfortunately you already have the most elegant solution.
If you look at the documentation for extract (same as date_part):
https://www.postgresql.org/docs/current/static/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT
month
For timestamp values, the number of the month within the year (1 - 12) ; for interval values, the number of months, modulo 12 (0 - 11)
SELECT EXTRACT(MONTH FROM TIMESTAMP '2001-02-16 20:38:40');
Result: 2
SELECT EXTRACT(MONTH FROM INTERVAL '2 years 3 months');
Result: 3
SELECT EXTRACT(MONTH FROM INTERVAL '2 years 13 months');
Result: 1
For your problem it would be nice if there was a version of month that wasn't modulo 12 but that doesn't exist.
The option you have (extract the year * 12 + month) is the best option there is.
Edit
If you do want to create a function then see the following two functions:
CREATE OR REPLACE FUNCTION get_months(i interval) RETURNS double precision AS $$
SELECT date_part ('year', i) * 12 + date_part ('month', i) ;
$$ LANGUAGE SQL IMMUTABLE;
SELECT get_months(age('2016-06-30', '2018-06-30'));
Or
CREATE OR REPLACE FUNCTION get_months(to_date date, from_date date) RETURNS double precision AS $$
SELECT date_part ('year', f) * 12 + date_part ('month', f)
FROM age (to_date, from_date) f;
$$ LANGUAGE SQL IMMUTABLE;
SELECT get_months('2016-06-30', '2018-06-30');
You can actually create both then just use whichever suits your code.

This will give you the # of months between two dates excluding days.
CREATE OR REPLACE FUNCTION get_months_between(to_date date, from_date date) RETURNS double precision AS $$
SELECT (date_part ('year', to_date) * 12 + date_part ('month', to_date)) - (date_part ('year', from_date) * 12 + date_part ('month', from_date))
$$ LANGUAGE SQL IMMUTABLE;

Related

how to recognize if month has 30 or 31 days in sqlg

I need to get last of the five days from month like:
1-5 = 5
6-10 = 10
...
26-30/31 = 30/31 (here can be 6 days depending on the month)
I've prepared function like
create or replace function getfirstdayoffive()
returns date
as
$$
select date_trunc('month', current_date - 5)::date
+ (least(ceil(extract(day from current_date - 5) / 5) * 5,
date_part('day', date_trunc('month', startOp) + interval '1 month - 1 day')))::int - 1;
$$
language sql
stable;
and it is working fine to return last day of five. How can I modify it so it would recognize if the last period should have 5 or 6 days?
try following function:
CREATE OR REPLACE FUNCTION public.days_in_month(d date)
RETURNS integer
LANGUAGE sql
AS $function$
SELECT date_trunc('month', $1::timestamp + interval '1 month')::date
- date_trunc('month', $1::timestamp)::date;
$function$
Determine the last date of the month, then extract day. If the day is 31 then return minus 6 days, else return minus five days. That assumes you want the last 5 days for Feb. But then except for Feb you could just return the 25th of the month as that is what minus 5 for months with 30 days and minus 6 for days 31 always returns. Note: rather than hard coding current_date this allows a parameter with default value of current_date.
create or replace
function getfirstdayoffive(parm_date_in date default current_date)
returns date
language sql
immutable strict
as $$
with last_of_mon(eom) as
( select date_trunc('month', parm_date_in) + interval '1 month - 1 day' )
select case when extract(day from eom) = 31
then (eom-interval '6 days')::date
else (eom-interval '5 days')::date
end
from last_of_mon;
$$;
select * from getfirstdayoffive();
select * from getfirstdayoffive(date '2021-08-15');
select * from getfirstdayoffive(date '2020-02-15');
hmm for now I've got something like :
create or replace
function getlastdayoffive(parm_date_in date default current_date)
returns timestamp
language sql
immutable strict
as $$
with last_of_mon(eom) as
( select date_trunc('month', parm_date_in) + interval '1 month - 1 day' )
select case when extract(day from eom) = 31
then (least(ceil(extract(day from parm_date_in - 5) / 5) * 5, date_part('day', date_trunc('month', parm_date_in) + interval '1 month - 1 day')))::timestamp
else (least(ceil(extract(day from parm_date_in - 6) / 6) * 6, date_part('day', date_trunc('month', parm_date_in) + interval '1 month - 1 day')))::timestamp
end
from last_of_mon;
$$;
But I can't cast to timestamp, how can it be done? if I return integer then I got what I wanted, but the poblem is I need to have a full date in format YYYY-MM-dd

postgresql error : Multiple decimal points

So I'm having this query:
SELECT
TO_CHAR(date_part('hour', created_at), 'YYYY-MM-DD HH24'),
to_char(created_at, 'day') ",
COUNT(*) AS "
FROM table
GROUP BY 1,2
ORDER BY 1 DESC
When I execute the query I get this:
ERROR: multiple decimal points
Searching stackoverflow I found some recommendations here:
How to format bigint field into a date in Postgresql? but I don't get why do I have to divide by 1000 and how this would apply in the case of the date_part function.
I assume created_at is a timestamp?.. I'm choosing from date_part(text, timestamp) and date_part(text, interval), if so date_part will return a double precision, to which you try to apply the mask 'YYYY-MM-DD HH24', eg:
v=# select date_part('hour', now());
date_part
-----------
9
and I don't see how you could possibly get year, month, day and hour from nine...
Yet I assume you wanted to apply the mask against truncated date to the hour precision, which is done with date_trunc(text, timestamp):
v=# select date_trunc('hour', now());
date_trunc
------------------------
2017-06-20 09:00:00+01
(1 row)
so now you can apply the time format:
v=# select to_char(date_trunc('hour', now()),'YYYY-MM-DD HH24');
to_char
---------------
2017-06-20 09
(1 row)
but if this is what you want, then you don't need to truncate time at all:
v=# select to_char(now(),'YYYY-MM-DD HH24');
to_char
---------------
2017-06-20 09
(1 row)
https://www.postgresql.org/docs/current/static/functions-datetime.html

Count months between two timestamp on postgresql?

I want to count the number of months between two dates.
Doing :
SELECT TIMESTAMP '2012-06-13 10:38:40' - TIMESTAMP '2011-04-30 14:38:40';
Returns :
0 years 0 mons 409 days 20 hours 0 mins 0.00 secs
and so:
SELECT extract(month from TIMESTAMP '2012-06-13 10:38:40' - TIMESTAMP '2011-04-30 14:38:40');
returns 0.
age function returns interval:
age(timestamp1, timestamp2)
Then we try to extract year and month out of the interval and add them accordingly:
select extract(year from age(timestamp1, timestamp2)) * 12 +
extract(month from age(timestamp1, timestamp2))
Please note that the most voted answer by #ram and #angelin is not accurate when you are trying to get calendar month difference using.
select extract(year from age(timestamp1, timestamp2))*12 + extract(month from age(timestamp1, timestamp2))
for example, if you try to do:
select extract(year from age('2018-02-02'::date, '2018-03-01'::date))*12 + extract(month from age('2018-02-02'::date , '2018-03-01'::date))
the result will be 0 but in terms of months between March from February should be 1 no matter the days between dates.
so the formula should be like the following saying that we start with timestamp1 and timestamp2:
((year2 - year1)*12) - month1 + month2 = calendar months between two timestamps
in pg that would be translated to:
select ((extract('years' from '2018-03-01 00:00:00'::timestamp)::int - extract('years' from '2018-02-02 00:00:00'::timestamp)::int) * 12)
- extract('month' from '2018-02-02 00:00:00'::timestamp)::int + extract('month' from '2018-03-01 00:00:00'::timestamp)::int;
you can create a function like:
CREATE FUNCTION months_between (t_start timestamp, t_end timestamp)
RETURNS integer
AS $$
select ((extract('years' from $2)::int - extract('years' from $1)::int) * 12)
- extract('month' from $1)::int + extract('month' from $2)::int
$$
LANGUAGE SQL
IMMUTABLE
RETURNS NULL ON NULL INPUT;
The age function give a justified interval to work with:
SELECT age(TIMESTAMP '2012-06-13 10:38:40', TIMESTAMP '2011-04-30 14:38:40');
returns 1 year 1 mon 12 days 20:00:00, and with that you can easily use EXTRACT to count the number of months:
SELECT EXTRACT(YEAR FROM age) * 12 + EXTRACT(MONTH FROM age) AS months_between
FROM age(TIMESTAMP '2012-06-13 10:38:40', TIMESTAMP '2011-04-30 14:38:40') AS t(age);
If you will do this multiple times, you could define the following function:
CREATE FUNCTION months_between (t_start timestamp, t_end timestamp)
RETURNS integer
AS $$
SELECT
(
12 * extract('years' from a.i) + extract('months' from a.i)
)::integer
from (
values (justify_interval($2 - $1))
) as a (i)
$$
LANGUAGE SQL
IMMUTABLE
RETURNS NULL ON NULL INPUT;
so that you can then just
SELECT months_between('2015-01-01', now());
SELECT date_part ('year', f) * 12
+ date_part ('month', f)
FROM age ('2015-06-12', '2014-12-01') f
Result: 6 Months
Gives the differenece of months of two dates
SELECT ((extract( year FROM TIMESTAMP '2012-06-13 10:38:40' ) - extract( year FROM TIMESTAMP '2011-04-30 14:38:40' )) *12) + extract(MONTH FROM TIMESTAMP '2012-06-13 10:38:40' ) - extract(MONTH FROM TIMESTAMP '2011-04-30 14:38:40' );
The Result : 14
Have to extract months seperately for both the dates and then the difference of both the results
Here is a PostgreSQL function with the exact same behavior as the Oracle MONTHS_BETWEEN function.
It has been tested on a wide range of years (including leap ones) and more than 700k combinations of dates (including end of every months).
CREATE OR REPLACE FUNCTION months_between
( DATE,
DATE
)
RETURNS float
AS
$$
SELECT
(EXTRACT(YEAR FROM $1) - EXTRACT(YEAR FROM $2)) * 12
+ EXTRACT(MONTH FROM $1) - EXTRACT(MONTH FROM $2)
+ CASE
WHEN EXTRACT(DAY FROM $2) = EXTRACT(DAY FROM LAST_DAY($2))
AND EXTRACT(DAY FROM $1) = EXTRACT(DAY FROM LAST_DAY($1))
THEN
0
ELSE
(EXTRACT(DAY FROM $1) - EXTRACT(DAY FROM $2)) / 31
END
;
$$
LANGUAGE SQL
IMMUTABLE STRICT;
This function requires a LAST_DAY function (behaving the same as Oracle's one) :
CREATE OR REPLACE FUNCTION last_day
( DATE
)
RETURNS DATE
AS
$$
SELECT
(DATE_TRUNC('MONTH', $1) + INTERVAL '1 MONTH' - INTERVAL '1 DAY')::date
;
$$
LANGUAGE SQL
IMMUTABLE STRICT;
I had the same problem once upon a time and wrote this ... it's quite ugly:
postgres=> SELECT floor((extract(EPOCH FROM TIMESTAMP '2012-06-13 10:38:40' ) - extract(EPOCH FROM TIMESTAMP '2005-04-30 14:38:40' ))/30.43/24/3600);
floor
-------
85
(1 row)
In this solution "one month" is defined to be 30.43 days long, so it may give some unexpected results over shorter timespans.
Extract by year and months will floor on months:
select extract(year from age('2016-11-30'::timestamp, '2015-10-15'::timestamp)); --> 1
select extract(month from age('2016-11-30'::timestamp, '2015-10-15'::timestamp)); --> 1
--> Total 13 months
This approach maintains fractions of months (thanks to tobixen for the divisor)
select round(('2016-11-30'::date - '2015-10-15'::date)::numeric /30.43, 1); --> 13.5 months
Try this solution:
SELECT extract (MONTH FROM age('2014-03-03 00:00:00'::timestamp,
'2013-02-03 00:00:00'::timestamp)) + 12 * extract (YEAR FROM age('2014-03-03
00:00:00'::timestamp, '2013-02-03 00:00:00'::timestamp)) as age_in_month;
SELECT floor(extract(days from TIMESTAMP '2012-06-13 10:38:40' - TIMESTAMP
'2011-04-30 14:38:40')/30.43)::integer as months;
Gives an approximate value but avoids duplication of timestamps. This uses hint from tobixen's answer to divide by 30.43 in place of 30 to be less incorrect for long timespans while computing months.
I made a function like this:
/* similar to ORACLE's MONTHS_BETWEEN */
CREATE OR REPLACE FUNCTION ORACLE_MONTHS_BETWEEN(date_from DATE, date_to DATE)
RETURNS REAL LANGUAGE plpgsql
AS
$$
DECLARE age INTERVAL;
declare rtn real;
BEGIN
age := age(date_from, date_to);
rtn := date_part('year', age) * 12 + date_part('month', age) + date_part('day', age)/31::real;
return rtn;
END;
$$;
Oracle Example)
SELECT MONTHS_BETWEEN
(TO_DATE('2015-02-02','YYYY-MM-DD'), TO_DATE('2014-12-01','YYYY-MM-DD') )
"Months" FROM DUAL;
--result is: 2.03225806451612903225806451612903225806
My PostgreSQL function example)
select ORACLE_MONTHS_BETWEEN('2015-02-02'::date, '2014-12-01'::date) Months;
-- result is: 2.032258
From the result you can use CEIL()/FLOOR() for rounding.
select ceil(2.032258) --3
select floor(2.032258) --2
Try;
select extract(month from age('2012-06-13 10:38:40'::timestamp, '2011-04-30 14:38:40'::timestamp)) as my_months;

How to get the number of days in a month?

I am trying to get the following in Postgres:
select day_in_month(2);
Expected output:
28
Is there any built-in way in Postgres to do that?
SELECT
DATE_PART('days',
DATE_TRUNC('month', NOW())
+ '1 MONTH'::INTERVAL
- '1 DAY'::INTERVAL
)
Substitute NOW() with any other date.
Using the smart "trick" to extract the day part from the last date of the month, as demonstrated by Quassnoi. But it can be a bit simpler / faster:
SELECT extract(days FROM date_trunc('month', now()) + interval '1 month - 1 day');
Rationale
extract is standard SQL, so maybe preferable, but it resolves to the same function internally as date_part(). The manual:
The date_part function is modeled on the traditional Ingres equivalent to the SQL-standard function extract:
But we only need to add a single interval. Postgres allows multiple time units at once. The manual:
interval values can be written using the following verbose syntax:
[#] quantity unit[quantity unit...] [direction]
where quantity is a number (possibly signed); unit is microsecond,
millisecond, second, minute, hour, day, week, month, year, decade,
century, millennium, or abbreviations or plurals of these units;
ISO 8601 or standard SQL format are also accepted. Either way, the manual again:
Internally interval values are stored as months, days, and seconds.
This is done because the number of days in a month varies, and a day
can have 23 or 25 hours if a daylight savings time adjustment is
involved. The months and days fields are integers while the seconds
field can store fractions.
(Output / display depends on the setting of IntervalStyle.)
The above example uses default Postgres format: interval '1 month - 1 day'. These are also valid (while less readable):
interval '1 mon - 1 d' -- unambiguous abbreviations of time units are allowed
IS0 8601 format:
interval '0-1 -1 0:0'
Standard SQL format:
interval 'P1M-1D';
All the same.
Note that expected output for day_in_month(2) can be 29 because of leap years. You might want to pass a date instead of an int.
Also, beware of daylight saving : remove the timezone or else some monthes calculations could be wrong (next example in CET / CEST) :
SELECT DATE_TRUNC('month', '2016-03-12'::timestamptz) + '1 MONTH'::INTERVAL
- DATE_TRUNC('month', '2016-03-12'::timestamptz) ;
------------------
30 days 23:00:00
SELECT DATE_TRUNC('month', '2016-03-12'::timestamp) + '1 MONTH'::INTERVAL
- DATE_TRUNC('month', '2016-03-12'::timestamp) ;
----------
31 days
This works as well.
WITH date_ AS (SELECT your_date AS d)
SELECT d + INTERVAL '1 month' - d FROM date_;
Or just:
SELECT your_date + INTERVAL '1 month' - your_date;
These two return interval, not integer.
SELECT cnt_dayofmonth(2016, 2); -- 29
create or replace function cnt_dayofmonth(_year int, _month int)
returns int2 as
$BODY$
-- ZU 2017.09.15, returns the count of days in mounth, inputs are year and month
declare
datetime_start date := ('01.01.'||_year::char(4))::date;
datetime_month date := ('01.'||_month||'.'||_year)::date;
cnt int2;
begin
select extract(day from (select (datetime_month + INTERVAL '1 month -1 day'))) into cnt;
return cnt;
end;
$BODY$
language plpgsql;
You can write a function:
CREATE OR REPLACE FUNCTION get_total_days_in_month(timestamp)
RETURNS decimal
IMMUTABLE
AS $$
select cast(datediff(day, date_trunc('mon', $1), last_day($1) + 1) as decimal)
$$ LANGUAGE sql;

Create date efficiently

On Pavel's page is the following function:
CREATE OR REPLACE FUNCTION makedate(year int, dayofyear int)
RETURNS date AS $$
SELECT (date '0001-01-01' + ($1 - 1) * interval '1 year' + ($2 - 1) * interval '1 day'):: date
$$ LANGUAGE sql;
I have the following code:
makedate(y.year,1)
What is the fastest way in PostgreSQL to create a date for January 1st of a given year?
Pavel's function would lead me to believe it is:
date '0001-01-01' + y.year * interval '1 year' + interval '1 day';
My thought would be more like:
to_date( y.year||'-1-1', 'YYYY-MM-DD');
Am looking for the fastest way using PostgreSQL 8.4. (The query that uses the date function can select between 100,000 and 1 million records, so it needs speed.)
Thank you!
I would just use the following, given that year is a variable holding the year, instead of using a function:
(year || '-01-01')::date
Btw. I can't believe that this conversion is your bottleneck. But maybe you should have a look at generate_series here (I don't know your usecase).
select current_date + s.a as dates from generate_series(0,14,7) as s(a);
dates
------------
2004-02-05
2004-02-12
2004-02-19
(3 rows)
Using to_date() is even simpler than you expect:
> select to_date('2008','YYYY');
to_date
------------
2008-01-01
(1 row)
> select to_date(2008::text,'YYYY');
to_date
------------
2008-01-01
(1 row)
Note that you still have to pass the year as a string, but no concatenation is needed.
As suggested by Daniel, in the unlikely case that this conversion is a bottleneck, you might prefer to precompute the function and store in a table. Eg:
select ynum, to_date( ynum ||'-01-01', 'YYYY-MM-DD') ydate
from generate_series(2000,2009) as ynum;
If there are a few years (and hence no need of indexes), you might even create the table dinamically for the scope of each query, with the new WITH.