How are months intervals internally calculated in Postgres? - postgresql

In PostgreSQL, the interval of '1 month' sometimes counts as 30 days and sometimes counts as 31 days. What are the criteria used to determine this?
I ran the below query to demonstrate my confusion.
select
now() - interval '1 month'
, now() - interval '30 days'
, interval '30 days' = interval '1 month'
, interval '31 days' = interval '1 month'
The query returns:
2022-03-27 21:09:30.933434+00 | 2022-03-28 21:09:30.933434+00 | true | false
I would expect the query to return both days on March 28th, since an interval of one month is equal to an interval of 30 days.

It comes down to the specific vs the general where day is the specific and month is not. The same happens with day and hour as in:
select '2022-03-13 12:00 PDT'::timestamptz - '1 day'::interval;
?column?
------------------------
2022-03-12 12:00:00-08
select '2022-03-13 12:00 PDT'::timestamptz - '24 hours'::interval;
?column?
------------------------
2022-03-12 11:00:00-08
DST occurred morning of 2022-03-13 in PST/PDT. So a day is generalized to the same time a day ago whereas 24 hours ago is actually 24 hours passing.
In your case:
select
now() - interval '1 month'
, now() - interval '30 days';
?column? | ?column?
-------------------------------+-------------------------------
2022-03-27 14:44:33.515669-07 | 2022-03-28 14:44:33.515669-07
The 1 month is going to go back to the same date and time one month back, whereas 30 days is going back an actual 30 days.
In this case:
select '2022-03-30 21:17:05'::timestamp - interval '1 month' ;
?column?
---------------------
2022-02-28 21:17:05
There is no day 30 in February so it goes to the actual end of the month the 28th.

Related

How to select month end data in PostgreSQL?

I'm working on the Accrual Reversal query in PostgreSQL. The system running doesn't have the reversal flag. So I need to consider all the end of the day of previous month accrued invoices as the reversal amount. And need to union them all with the main query. I can do it for last month but invoice date are dynamic, user may give 2 years as invoice period. For those 2 years, all the previous month data should be considered as accrued reversal. Here is the query
select invoicename, * from accountpay where invoice_date between '2020-01-01' and '2021-12-31'
union all
select concat('Accured Reversal', invoicename) as reference, * from accountpay where accrual = true and invoice_date::date = (select concat(date_part('year',((('2021-12-30'::date) - interval '1 month'))), '-', date_part('month',((('2021-12-30'::date) - interval '1 month'))), '-01')::date + interval '1 month' - interval '1 day')
Please help me to do this.
Thanks in Advance
SELECT (
Date_trunc('MONTH',a) + interval '1 month -1 day ')
as last_day_of_month
FROM generate_series(
'2020-01-01 00:00'::timestamp
- interval '12 months',
'2022-01-01 00:00',
'1 month') as dt(a);
get last_day_of_month from '2020-01-01 00:00' till '2022-01-01 00:00'
Then your sql would be
invoice_date in
(SELECT (Date_trunc('MONTH',a) + interval '1 month -1 day ')
as last_day_of_month
FROM generate_series(
'2020-01-01 00:00'::timestamp
- interval '12 months',
'2021-01-01 00:00',
'1 month') as dt(a))
This will get the last day of last 12 months so the number of months will be place holder (dynamic) and all the last day of the months will be in IN clause.
SQL re-written:
WITH date_cte AS
(
SELECT Date_trunc('MONTH',dt)+ interval '1 month -1 day ' last_day_of_month
FROM generate_series('2021-11-30 00:00:00'::timestamp - interval '12 months','2021-11-30 00:00:00','1 month') t(dt))
select invoicename, * from accountpay where invoice_date between '2020-01-01' and '2021-12-31'
union all
select concat('Accured Reversal', invoicename) as reference, * from accountpay where accrual = true and invoice_date::date in (select * from date_cte);
Basically , the last dates are generated this way for 12 months:
WITH date_cte AS
(
SELECT Date_trunc('MONTH',dt)+ interval '1 month -1 day ' last_day_of_month
FROM generate_series('2021-11-30 00:00:00'::timestamp - interval '12 months','2021-11-30 00:00:00','1 month') t(dt))
SELECT *
FROM date_cte;
last_day_of_month
---------------------
2020-11-30 00:00:00
2020-12-31 00:00:00
2021-01-31 00:00:00
2021-02-28 00:00:00
2021-03-31 00:00:00
2021-04-30 00:00:00
2021-05-31 00:00:00
2021-06-30 00:00:00
2021-07-31 00:00:00
2021-08-31 00:00:00
2021-09-30 00:00:00
2021-10-31 00:00:00
2021-11-30 00:00:00
You can replace 12 months by any number of months or you can make it year too like:
...generate_series('2021-11-30 00:00:00'::timestamp - interval '1 year','2021-11-30 00:00:00','1 month')

First day and Last day of previous year

I'm trying to find the date of the end of previous year in Postgres.
This will give me the first day of previous year
SELECT date_trunc('year', now()- interval '1 year')
returns the correct result #=> 2018-01-01 00:00:00
But trying to remove 1day from the first day of the current year doesn't give me the last day of previous year:
SELECT date_trunc('year', now() - interval '1 day')
returns #=> 2018-01-01 00:00:00
When I'm expecting it to be 2018-12-31
Still gives me the first day of the current year. Even if i remove 100 day it still returns the same result.
Same behavior for previous years:
SELECT date_trunc('year', now()- interval '2 year');
returns #=> 2017-01-01 00:00:00 which is what I expect.
but :
SELECT date_trunc('year', now()- interval '1 year' - interval '1 day');
returns #=> 2018-01-01 00:00:00 when I'm expecting 2017-12-31
--
For future reference I'm posting this in 2019.
This is an order of operations issue. Truncate the date before subtracting:
SELECT date_trunc('year', now()) - interval '1 day';
SELECT date_trunc('year', now()- interval '1 year') - interval '1 day';
Disclosure: I work for EnterpriseDB (EDB)

How do I generate months between start date and now() in postgresql

I also have the question how do i get code block to work on stack overflow but that's a side issue.
I have this quasi-code that works:
select
*
from
unnest('{2018-6-1,2018-7-1,2018-8-1,2018-9-1}'::date[],
'{2018-6-30,2018-7-31,2018-8-31,2018-9-30}'::date[]
) zdate(start_date, end_date)
left join lateral pipe_f(zdate...
But now I want it to work from 6/1/2018 until now(). What's the best way to do this.
Oh, postgresql 10. yay!!
Your query gives a list of first and last days of months between "2018-06-01" and now. So I am assuming that you want to this in a more dynamic way:
demo: db<>fiddle
SELECT
start_date,
(start_date + interval '1 month -1 day')::date as end_date
FROM (
SELECT generate_series('2018-6-1', now(), interval '1 month')::date as start_date
)s
Result:
start_date end_date
2018-06-01 2018-06-30
2018-07-01 2018-07-31
2018-08-01 2018-08-31
2018-09-01 2018-09-30
2018-10-01 2018-10-31
generate_series(timestamp, timestamp, interval) generates a list of timestamps. Starting with "2018-06-01" until now() with the 1 month interval gives this:
start_date
2018-06-01 00:00:00+01
2018-07-01 00:00:00+01
2018-08-01 00:00:00+01
2018-09-01 00:00:00+01
2018-10-01 00:00:00+01
These timestamps are converted into dates with ::date cast.
Then I add 1 month to get the next month. But as we are interested in the last day of the previous month I subtract one day again (+ interval '1 month -1 day')
Another option that's more ANSI-compliant is to use a recursive CTE:
WITH RECURSIVE
dates(d) AS
(
SELECT '2018-06-01'::TIMESTAMP
UNION ALL
SELECT d + INTERVAL '1 month'
FROM dates
WHERE d + INTERVAL '1 month' <= '2018-10-01'
)
SELECT
d AS start_date,
-- add 1 month, then subtract 1 day, to get end of current month
(d + interval '1 month') - interval '1 day' AS end_date
FROM dates

Postgres expand time window using date_part

Have two dates - '2018-05-01' and '2018-06-01'. I would like to expand this window to the past by day difference of those dates.
SELECT * FROM data
WHERE
start_time > CAST('2018-05-01' AS timestamptz) - INTERVAL '30 DAY'
AND start_time < CAST('2018-06-01' AS timestamptz)
How can I replace INTERVAL '30 DAY' with number of days between given dates without explicitly defining number of days? I know to calculate day difference:
date_part('day',age('2018-05-01', '2018-06-01'))
But not sure how to incorporate into the substraction. Dates and days between them will change.
You can use date_trunc('mon', some_date_expression) to round down to the start of a month:
select date_trunc('mon', now() - '3 mon'::interval) as date_begin
, date_trunc('mon', now() - '1 day'::interval) as date_end
;
Result
date_begin | date_end
------------------------+------------------------
2018-03-01 00:00:00+01 | 2018-06-01 00:00:00+02
(1 row)
You can simply subtract the difference from the start date:
with t (start_date, end_date) as (
values (date '2018-05-01', date '2018-06-01')
)
select start_date - (end_date - start_date) as new_start,
end_date
from t;
returns
new_start | new_end
-----------+-----------
2018-03-31 | 2018-06-01

postgres '1 year' equals '360 days'?

Am wondering if anyone else has encountered this or knows information about it.
Today is November 3, 2014 and if i check whether or not November 5, 2013 is within the last year i get different answers depending on how i check: 1 year versus 365 days
select now() - '20131105' as diff,
case when now() - '20131105' <= '1 year' then 'within year' else 'not within year' end as yr_check,
case when now() - '20131105' <= '365 days' then 'within 365 days' else 'not within 365 days' end as day_check
2014-11-03 16:27:38.39669-06; 363 days 16:27:38.39669; not within year; within 365 days
Looks like when querying against November 9 tho, it's ok
select now() as right_now, now() - '20131109' as diff,
case when now() - '20131109' <= '1 year' then 'within year' else 'not within year' end as yr_check,
case when now() - '20131109' <= '365 days' then 'within 365 days' else 'not within 365 days' end as day_check
2014-11-03 16:31:12.464469-06; 359 days 16:31:12.464469; within year; within 365 days
anyone have an idea about this? or is there something about date arithmetic that's funny?
postgres version is 9.2.4
or is there something about date arithmetic that's funny?
It's funny alright, but not in the way that makes you laugh.
Twelve months has to equal a year doesn't it?
=> SELECT '12 months'::interval = '1 year'::interval;
?column?
----------
t
Good. Makes sense. Hmm - wonder how long a month is.
=> SELECT '30 days'::interval = '1 month'::interval;
?column?
----------
t
Fair enough. Suppose they had to pick something.
Hmm - but that means...
=> SELECT '360 days'::interval = '12 months'::interval;
?column?
----------
t
Which seems to imply...
=> SELECT '360 days'::interval = '1 year'::interval;
?column?
----------
t
That can't be right! What they need to do is have a month equal to 30.41666 days. No hang on, what about leap years? Hmm - does this affect weeks? AARGH!
Basically, you can't convert sensibly between time units. There aren't 60 seconds in a minute, or 24 hours in a day, 52 weeks in a year or even 365 days. Unfortunately, humans (particularly customer-shaped humans) like converting between time units so we end up with a mess like this.
PostgreSQL's system is no more loony than any other and in fact is better than most.
I'm not sure what is real problem with this check, but it works other way around:
select now() - interval '1 year' <= date '2013-11-05'
I'm no expert in Postgres, but it can be something with type comparisons, because:
select pg_typeof(now() - date '2013-11-05'),
pg_typeof(now() - interval '1 year')
yields result:
interval, timestamp with time zone
so your example compares interval with interval, but for different scales - days vs year, and my solution compares timestamp with date, which seems to work
UPDATE:
You can check that interval '1 year' when not attached to year (not added to date or timestamp) equals to 360 days:
select interval '1 year' <= interval '359 days',
interval '1 year' <= interval '360 days'
which yields:
f, t
From my understanding you can't just compare random year interval when you don't know year it is attached - always compare dates, and just use interval to create new date object.
select now() - interval '1 year' <= now() - interval '365 days'
t
From www.postgresql.org/docs/current/static/datatype-datetime.html:
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. Because intervals are usually created from constant strings or timestamp subtraction, this storage method works well in most cases. Functions justify_days and justify_hours are available for adjusting days and hours that overflow their normal ranges.
Because you compare two intervals, PostgreSQL internally normalizes values (like justify_interval()), before comparing:
SELECT INTERVAL '31 days' > INTERVAL '1 mon' -- yields 't'
But, if you apply interval substraction/addition, varying day & month length taken into consideration:
SELECT (timestamptz '2014-11-03 00:00:00 America/New_York' - INTERVAL '1 day') AT TIME ZONE 'America/New_York',
timestamptz '2014-11-03 00:00:00 America/New_York' - timestamptz '2014-11-02 00:00:00 America/New_York' <= interval '1 day';
-- | timestamp | boolean |
-- +---------------------+---------+
-- | 2014-11-02 01:00:00 | f |
So, if you need to test, whether a timestamp/date is within a range, you should manipulate timestampts/dates (or use timestamp/date ranges) & compare those values with <, > or BETWEEN.
SELECT timestamp '2014-11-03 00:00:00' - timestamp '2014-10-03 00:00:00' <= interval '1 mon',
timestamp '2014-11-03 00:00:00' - interval '1 mon' <= timestamp '2014-10-03 00:00:00';
-- | boolean | boolean |
-- +---------+---------+
-- | f | t |