PostgreSQL interval subtraction - postgresql

I've tried to subtract interval from timestamp, but I've got a wrong result in comparison to days via subtracting 2 dates.
E.g.:
select
(now::date - past::date) as days,
(now::date - past::date) / 365.25 as years,
justify_interval(now - past::date) as interval_test
from (
select '2020-09-17 00:00:01'::timestamp as now, '2010-09-17 00:00:01'::timestamp as past
) b;
gives results:
3653 days
10.0013 years
'10 years 1 mon 23 days' interval test
Could anyone help me to understand what is wrong with subtracting?
When I do it vice versa, it's ok:
select
(past::date + 3653)::date,
(past + interval '10 years')::date,
(past + 10*interval '1 year')::date,
(past + 10*12*interval '1 month')::date
from (
select '2020-09-17 00:00:01'::timestamp as now, '2010-09-17 00:00:01'::timestamp as past
) b;
all results give the same date '2020-09-17'
What I do wrong?
I am using PostgreSQL 10.5.

There is nothing wrong with subtracting. It is just that justify_interval doesn't do what you seem to expect. justify_interval uses 30 day months and 24 hour days. So 12 months becomes only 360 and 10 years only 3600 days. Leaving 53 days which is 1 (30 day) month and 23 days.
Edit
The justify_interval documentation on this page refers to justify_days and justify_hours which are directly above it which do mention the use of 30 days months and 24 hour days.
The justify functions do have to make these assumption because the interval type is a general length of time (it has no specific start and end). So the justify functions does not know over which specific months the interval was originally calculated.
The age function however does not take an interval it takes an end and a start so it actually knows which specific months and years are in that period.

Related

How can I always get the full period when grouping by week in PostgreSQL?

I'm used to do the following syntax when analysing weekly data:
select week(creation_date)::date as week,
count(*) as n
from table_1
where creation_date > current_date - 30
group by 1
However, by doing this I will get just part of the first week.
Is there any smart way to alway get a whole week in the beginning?
Like get the first day of the week I would get half of.
First off you need to define what you mean by "week". This is more difficult than it appears. While humans have an intuitive since of a week, computers are just not that smart. There are 2 common conventions: the ISO-8601 Standard and, for lack of a better term, Traditional. ISO-8601 defines a week as always beginning on Monday and always containing 7 days. Traditional weeks begin on Sunday (usually) but may have weeks with less than 7 days. This results from having the 1st week of the year beginning on 1-Jan regardless of day of week. Thus the 1st and/or last weeks may have less than 7 days. ISO-8601 throws it own curve into the mix: the 1st week of the year begins on the week containing 4-Jan. Thus the last days of Dec may be in week 1 of the next year and the first days Jan may be in week 52/53 of the prior year.
All the below assume the ISO-8061.
Secondly there is no week function in Postgres. In you need extract function. So for this particular case:
select extract(week from creation_date)::integer as week, ...
Finally, your predicate (current_date - 30) ensures you will unusually not begin on the 1st of the week. To get the correct date take that result back 1 week, then go forward to the next Monday.
with days_to_monday (day_adj) as
( values ('{7,6,5,4,3,2,1}'::int[]) )
select current_date - 30
, current_date - 30 - 7 + day_adj[extract (isodow from current_date - 30 )]
from table_1 cross join days_to_monday;
The CTE establishes an array which for a given day of the week contains the number of days need to the next Monday. That main query extracts the day of week of current date and uses that to index the array. The corresponding value is added to get the proper date.
Putting that together with your original query to arrive at:
with next_week (monday) as
( values (current_date - 30 - 7
+ ('{7,6,5,4,3,2,1}'::int[])[extract (isodow from current_date - 30 )])
)
select extract(week from creation_date) as week,
count(*) as n
from table_1
where creation_date >= (select monday from next_week)
group by 1
order by 1;
For full example see fiddle.

Recalculate date difference based on exact dates

I have 2 columns in my table firstdate and lastdate
firstdate is for example today (2020-1-22).
lastday is plus 1 years minus 1 day (2021-1-21).
So my insert script is like:
INSERT INTO <table> (firstdate, lastdate)
VALUES (current_date, current_date + interval '1 year -1 day')
Ok, so far so good. Now I want to extrapolate it to use it again.
My question is based on these 2 dates. How can i return the interval back again from these 2 dates?
When I use the Postgres function age, I get the following result: 0 years 11 months 30 days.
I can't use this interval to make the right calculation because interval 0 years 11 months 30 days is not the same as interval 1 years -1 days.
So somehow I need to get back 1 years -1 days
lastdate-firstdate gives you the interval between the dates. You cannot get back the literal string you used to create the original interval.
Best regards,
Bjarni
You can store the interval instead of lastdate then derive the lastdate when needed as column interval_value. So
select firstdate,first_date+interval_value lastdate ...
or if you have version 12, define lastdate as a virtual column. Then just select normally. Either way you have the necessary interval.

Time range graph using Grafanas variables

We are using the graph compare time and we want to compare the amount of sessions for example between 14 days ago and 28 days ago and same wih 30 days ago and 60 days ago.
We currently wrote date BETWEEN $__timeFrom() AND $__timeTo() and it changes upon selecting the date ranges on the top right but we want the client to be able to change the date ranges and for this query to adjust to show -14 days and -30 days ago as well.
SELECT
$__timeGroup(date, '24h'),
sum(sessions) as sessions
FROM
ourchart
WHERE
date BETWEEN $__timeFrom() AND $__timeTo()
group by time
order by time
EDIT - I am able to make intervals of course but we are trying to compare this month to last month in the same graph....
Play with interval math. Example, which will move your timerange 30 days back:
date BETWEEN
timestamp $__timeFrom() - interval '30 days' AND
timestamp $__timeTo() - interval '30 days'

why justify_interval('360 days'::interval) results '1 year'

For some reason justify_interval(now() - '2013-02-14'::timestamptz) produces weird results:
postgres=# select justify_interval(concat(365*4 +1,' days')::interval); -[ RECORD 1 ]----+----------------
justify_interval | 4 years 21 days
I checked one year:
postgres=# select justify_interval('365 days'::interval);
justify_interval
------------------
1 year 5 days
So I went further:
postgres=# select justify_interval('360 days'::interval);
justify_interval
------------------
1 year
(1 row)
This behavior is not platform specific (tried several Linuxes, 9.2, 9.3, 9.6)
Why one year is 360 days?..
It seems that you are looking for something, which PostgreSQL calls a "symbolic" result that uses years and months, rather than just days, which is what the age(timestamp, timestamp) (and age(timestamp)) function(s) returns.
select age(now(), '2013-02-14'); -- 4 years 16:41:02.571547
select age(timestamp '2013-02-14'); -- 4 years
The - operator always returns the difference in days (at most). The justify_*() functions (and the *, /, <, > operators) always "cut" values to an average (i.e. 1 day is 24 hours and 1 month is 30 days) despite the fact that 1 day actually can contain 23-25 hours (just think of daylight saving time zones) and 1 month can contain 28-31 days (so the result depends on the actual start and end points of the range, which creates the interval).
accrding to docs:
justify_interval(interval) - Adjust interval using justify_days and
justify_hours, with additional sign adjustments
and further:
justify_days(interval) - Adjust interval so 30-day time periods
are represented as months
So 30*12=360
Not expected but obviously defined in docs...

How to calculate average weekly hours between 2 dates covering multiple weeks?

Postgresql 8.4.
I'm new to this concept so if people could teach me I'd appreciate it.
For Obamacare, anyone that works 30 hours per week or more must be offered the same healthcare as is offered to any other worker. We can't afford that so we have to limit work hours for temp and part-timers. This is affecting the whole country.
I need to calculate the hours worked (doesn't matter if overtime,
regular time, double time, etc) between two dates, say Jan 1, 2014,
and Nov 1, 2014 (Saturday) for each custom week (which beings on Sunday), not the week as defined by Postgresql (which begins on Monday).
Each of my custom work weeks begins on Sunday and ends on Saturday.
I don't know if I have to include weeks where
they did not work at all in the average, but let's assume I do. Zero hours that week would draw down the average.
Table name is 'employeetime', date field is 'employeetime.stopdate', hours worked per day is in the field 'employeetime.hours', employeeid field is 'employeetime.empid'.
I'd prefer to do this in one query per employee and I will execute the query once per employee as I loop through employees. If not I'm open to suggestions. But I'd like to understand the SQL presented in the answer.
Currently EXTRACT(week from '2014-01-01') calculates the start of the week as a Monday, so that doesn't work for me. Link here.
How would I do that without doing, say a separate query for each week, per person? We have 200 people to process.
Thank you.
I have set up a table to match your format:
select * from employeetime order by date;
id date hours
1 2014-11-06 10
1 2014-11-07 3
1 2014-11-08 5
1 2014-11-09 3
1 2014-11-10 5
You can get the week starting on Sunday by shifting. Note, here the 9th is a Sunday, so that is where we want the boundary.
select *, extract(week from date + '1 day'::interval) as week
from employeetime
order by week;
id date hours week
1 2014-11-07 3 45
1 2014-11-06 10 45
1 2014-11-08 5 45
1 2014-11-09 3 46
1 2014-11-10 5 46
And now the week shifts on Sunday rather than Monday. From here, the query to get hours by week/employee would be simple:
select id, sum(hours) as hours, extract(week from date + '1 day'::interval) as week
from employeetime
group by id, week
order by id, week;
id hours week
1 18 45
1 8 46