When evaluating intervals, postgres appears to define a month as 30 days exactly, even when there are 31 days in a month:
select age('2021-03-31 23:59:59.999', '2021-03-01'::date)
Returns: 30 days 23:59:59.999
Which in the case of March is Less than 1 month.
Yet:
select age('2021-03-31 23:59:59.999', '2021-03-01'::date) <= '1 month'
Evaluates to false.
A (not very clean) solution to this is:
select age('2021-03-31 23:59:59.999', '2021-03-01'::date) <= case (select DATE_PART('days', DATE_TRUNC('month', '2021-03-31'::Date) + '1 MONTH'::interval - '1 DAY'::INTERVAL))
when 31 then '31 days'::interval when 30 then '30 days'::interval
when 29 then '29 days'::interval else '28 days'::interval end
My question is in 2 parts:
Why does postgresql define a month as 30 days, particularly in the case where I give two dates as input to a builtin function?
Is there a cleaner solution to my problem than the above snippet?
Perhaps interval '1 month' is ambiguous. Is it 28, 29, 30 or 31 days as all them are correct depending upon which month. With nothing to compare it seems to just choose 1. Try reformulating the comparison.
select '2021-03-31 23:59:59.999'::timestamp - interval '1 month' < '2021-03-01'::date
Related
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.
I want to define the start of a “month” as the 26th day of the previous calendar month (and of course ending on 25th).
How can I group by this definition of month using date_trunc()?
This expression gives the month you want:
date_trunc(
'month',
date_add(
day,
case
when extract(day from date) > 25 then 7
else 0
end),
my_date_col
)
)
Select it and group by it.
The logic is: If the day of the month is greater than 25, then add some days to bump it into the next month before truncating it to the month.
I would use an INTERVAL to calculate the correct dates. Here an example using generate_series():
SELECT d::date as reference_date
, (d + interval '25 days')::date AS first_day
, (d + interval '1 month' + interval '24 days')::date as last_day
FROM generate_series('2020-01-01'::timestamp, '2021-01-01'::timestamp, '1 month') g(d);
I'm trying to compare values of current month's data to previous months using PostgreSQL. So if today is 4/23/2018, I want the data for 3/23/2018.
I've tried current_date - interval '1 month' but it is problematic for months with 31 days.
My table is structured as simply as
date, value
Check this example query:
WITH dates AS (SELECT date::date FROM generate_series('2018-01-01'::date, '2018-12-31'::date, INTERVAL '1 day') AS date)
SELECT
start_dates.date AS start_date,
end_dates.date AS end_date
FROM
dates AS start_dates
RIGHT JOIN dates AS end_dates
ON ( start_dates.date + interval '1 month' = end_dates.date AND
end_dates.date - interval '1 month' = start_dates.date);
It will output all end_dates and corresponding start_dates. The corresponding dates are defined by interval '1 month' and checked in both ways:
start_dates.date + interval '1 month' = end_dates.date AND
end_dates.date - interval '1 month' = start_dates.date
The output looks like this:
....
2018-02-26 2018-03-26
2018-02-27 2018-03-27
2018-02-28 2018-03-28
2018-03-29
2018-03-30
2018-03-31
2018-03-01 2018-04-01
2018-03-02 2018-04-02
2018-03-03 2018-04-03
2018-03-04 2018-04-04
....
Note, that there are 'gaps' for days without corresponding dates.
Back to your table, join the table with itself (giving aliases) and use given join condition, so the query would look like this:
SELECT
start_dates.value - end_dates.value AS change,
start_dates.date AS start_date,
end_dates.date AS end_date
FROM
_your_table_name_ AS start_dates
RIGHT JOIN _your_table_name_ AS end_dates
ON ( start_dates.date + interval '1 month' = end_dates.date AND
end_dates.date - interval '1 month' = start_dates.date);
Given the following table structure:
create table t (
d date,
v int
);
After populating with some dates and values, there is a way to find the value of the previous month using simple calculations and the LAG function, without resorting to joins. I am not sure how it compares from a performance perspective, so please run your own tests before selecting which solution to use.
select
*,
lag(v, day_of_month) over (order by d) as v_end_of_last_month,
lag(v, last_day_of_previous_month + day_of_month - cast(extract(day from d - interval '1 month') as int)) over (order by d) as v_same_day_last_month
from (
select
*,
lag(day_of_month, day_of_month) over (order by d) as last_day_of_previous_month
from (
select
*,
cast(extract(day from d) as int) as day_of_month
from
t
) t_dom
) t_dom_ldopm;
You may note that between the 29th and 31st of March, the comparison will be made against the 28th of February, since the same day does not exist in February for those particular dates. The same logic applies to other months with different number of 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 |
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;