Postgresql. Dates interval issue - postgresql

I'm trying to get difference in days, casting result to decimal:
SELECT
CAST( TO_DATE('2999-01-01','yyyy-mm-dd') - TO_DATE('2909-01-01','yyyy-mm-dd') AS DECIMAL )
;
Now if I add 1 month to the 2nd date:
SELECT
CAST( TO_DATE('2999-01-01','yyyy-mm-dd') - (TO_DATE('2909-01-01','yyyy-mm-dd') + INTERVAL '1 MONTH' * (1) ) AS DECIMAL )
;
I'm getting an error:
ERROR: cannot cast type interval to numeric
OK, I can cast to char to get result:
SELECT
CAST( TO_CHAR( TO_DATE('2909-02-10','yyyy-mm-dd') - (TO_DATE('2909-01-01','yyyy-mm-dd') + INTERVAL '1 MONTH' * (1) ), 'DD') AS DECIMAL )
;
But in this case the 1st query modified with TO_CHAR casting stop working:
SELECT
CAST( TO_CHAR(TO_DATE('2999-01-01','yyyy-mm-dd') - TO_DATE('2909-01-01','yyyy-mm-dd'), 'DD') AS DECIMAL )
;
I'm getting ERROR: multiple decimal points.
So, my question is, how can I get days using the same sql statement? For both sql queries.

Look at your first two examples again. If you remove the outer CAST ... AS DECIMAL you get
?column?
----------
32872
?column?
------------
32841 days
Clearly the difference is in the "days". The second is an interval value rather than a simple number. You only want the number (because you always just want days) so you need to extract that part. Then you can cast to whatever precision you like:
SELECT extract(days FROM '32841 days'::interval)::numeric(9,2);
date_part
-----------
32841.00
Edit responding to Alexandr's follow-up:
Your first example fails with a fairly specific error:
SELECT extract(days FROM (TO_DATE('2999-01-01','yyyy-mm-dd') - TO_DATE('2909-01-01','yyyy-mm-dd'))::interval)::numeric(9,2);
ERROR: cannot cast type integer to interval
LINE 1: ...yyyy-mm-dd') - TO_DATE('2909-01-01','yyyy-mm-dd'))::interval...
Here you've got an integer (which is what you originally wanted) and try to cast it to an interval (for reasons I don't understand). It's complaining it doesn't know what units you want. You want 32872 what in your interval - seconds, hours, weeks, centuries?
The second example is complaining because you are trying to extract the "day" part from a simple integer, and of course there's no extract() function in the system to do that.
I think you probably need to take a step back and just take the time to understand the values your various expressions return.
Subtracting one date from another gives the number of days separating them - as an integer. There is no other sensible measure, really.
Adding (or subtracting) an interval to a date gives you a timestamp (without time zone) since the interval may contain whole days, days and hours, seconds etc.
Subtracting a timestamp from a date will give you an interval since the result may contain days, hours, seconds etc.
If you have an interval and you just want the days part then you use extract() on it and you will get an integer number of days back.
You will need an integer (or floating-point) number of days if you want to cast to numeric, not an interval because casting an interval to an scalar number makes no sense without units.
So - either stick to dates and date arithmetic (easy), or realise you are using timestamps (flexible) but understand which it is.
To get an illustration of what's happening you can do something like this (in psql):
CREATE TEMP TABLE tt AS SELECT
('2909-01-02'::date - '2909-01-01'::date) AS a,
('2909-01-02'::date - '2909-01-02 00:00:00'::timestamp) AS b;
\x
SELECT * FROM tt;
\d tt
That will show you the values and types you are dealing with. Repeat for as many columns as you find useful.
HTH

If you're doing interval arithmetic with dates, you should generally be using timestamps instead, as mentioned in the docs.
# SELECT extract(days FROM TO_TIMESTAMP('2999-01-01','yyyy-mm-dd') - TO_TIMESTAMP('2909-01-01','yyyy-mm-dd'))
date_part
-----------
32872
# SELECT extract(days FROM TO_TIMESTAMP('2999-01-01','yyyy-mm-dd') - (TO_TIMESTAMP('2909-01-01','yyyy-mm-dd') + '1 month'::interval) );
date_part
-----------
32841

The result of adding an interval to a date is actually a timestamp, not another date (the interval might have contained time portions), so you have to cast the result of the addition back down to date first:
SELECT
CAST( TO_DATE('2999-01-01','yyyy-mm-dd')
- CAST( (TO_DATE('2909-01-01','yyyy-mm-dd') + INTERVAL '1 MONTH' * (1) ) AS DATE)
AS DECIMAL )

Related

Find days between dates

I am looking to subtract 2 dates to get the number of days in between. However, the columns are defined as "timestamp without time zone". I'm not sure how to get a whole integer.
I have a stored procedure with this code:
v_days_between_changes := DATE_PATH('day',CURRENT_DATE - v_prev_rec.date_updated)::integer;
But I get this error:
HINT: No function matches the given name and argument types. You might need to add explicit type casts.
QUERY: SELECT DATE_PATH('day',CURRENT_DATE - v_prev_rec.date_updated)::integer
Any help would be great.
You can compute the difference between the dates, which returns an interval. Then, extract the number of days from this interval.
WITH src(dt1,dt2) AS (select '1/1/2019'::timestamp without time zone, CURRENT_DATE )
select EXTRACT(day FROM dt2-dt1) nbdays
from src;
nbdays
-----------
98

Convert integer to week interval

How one can convert integer to week interval?
CREATE TABLE integers( i integer);
INSERT INTO integers VALUES ('10');
Output would be table with one column indicating 10 weeks interval.
http://sqlfiddle.com/#!17/4b404/5/0
One take would be to create constant interval of 1 week and multiply it by integer.
I would prefer function to do it directly, but I am not aware of it.
SELECT interval '1 week' * i AS weeks_interval FROM integers;
Your solution is well accepted.
If you don't want to keep the "1" in the string you could write this instead
SELECT (i || 'week')::interval FROM intervals
demo: db<>fiddle

How can I get a floating average over timestamps in PostgreSQL?

Let's say I have a table for a time-sheet like this:
CREATE TABLE foo (
spent_on DATETIME,
hours FLOAT
)
Assuming spent_on is the timestamp the value was logged, and hours is a floating point value representing the amount of hours spent on a task.
How can I get a floating average of hours over the past 7 days?
I've came up with the following but it won't work:
select spent_on, hours, avg(hours)
over RANGE BETWEEN spent_on - INTERVAL '7 days' AND CURRENT ROW from daily;
I get the following error:
ERROR: syntax error at or near "ROW"
LINE 1: ... BETWEEN spent_on - INTERVAL '7 days' AND CURRENT ROW from d...
I've tried to understand the docs for window functions, but I have real trouble grasping the idea between partitions, windows and frames. And as a result, can't come up with a query.
I'm not sure about the RANGE syntax, so let me offer a solution with a sub query (If performance is not an issue with small tables ETC..) :
SELECT t.spent_on,t.hours,
COALESCE( (SELECT AVG(s.hours) FROM foo
WHERE t.spent_on > CURRENT_TIMESTAMP - INTERVAL '7 days'),0) float_avg
FROM foo t

PostgreSQL: exists robust third party date-math functions to augment the built-in date operators?

I'm porting some T-SQL stored procs to PL/pgSql and, being very new to PostgreSQL, don't know what helpful utility functions might be available in the pg community. Is there a set of robust date-math functions that "nearly everybody uses" out there somewhere? I don't want to quickly cobble together some date-math functions if there's already a great package out there.
The PostgreSQL date math operators with "natural language" string literal arguments are user-friendly if you're typing a query and you happen to know the interval:
select now() - interval '1 day'
but if the interval 1 is the result of a calculation involving nested date-math function calls, these string literals are actually not very user-friendly at all, and it would easier to work with a date_add function:
select dateadd(d, {calculation that returns the interval}, now() )
Thanks
Let me give you an example. I want to subtract from an arbitrary date the number of months that have elapsed since 1/1/1970, and then add that number of months to 1/1/1970 to return the first day of the month in which the arbitrary date falls
select (date_trunc('month', '2013-01-30'::date))::date
Or add a month to the first day of this month to get the first day of the next month, then subtract one day to get the last day of this month
select date_trunc('month', '2013-01-30'::date + 1 * interval '1 month')::date - 1
Notice in the above example you can add any number of months by multiplying the interval '1 month' by an integer. You can do that with any interval without manipulating the string '1 month'. So to add or subtract any interval you just:
select current_date + 5 * interval '1 month'
No need for messy string manipulations. You can multiply by fractions also:
select current_timestamp + 3.5 * interval '1 minute'
To add or subtract days to a date type you use an integer:
select current_date + 10
The "natural language" strings you're talking about are interval literals. Intervals can also be obtained by using date arithmetic.
Surely dateadd can be quite simply emulated in Postgresql as follows:
select d + ({calculation the returns the interval}::text || ' day')::interval
Substitute "month" or "hours" etc as appropriate.
In PostgreSQL, you simply add and subtract interval values to datetime
values:
'2001-06-27 14:43:21'::TIMESTAMP - '00:10:00'::INTERVAL = '2001-06-27 14:33:21'::TIMESTAMP
'2001-06-27 14:43:21'::TIMESTAMP- '2001-06-27 14:33:21'::TIMESTAMP = '00:10:00'::INTERVAL
For more information, see "Functions and Operators" in the PostgreSQL
online docs.
To compute the first day of the month of a date: date_trunc('month', date)
First day of the next month: date_trunc('month', date) + '1 month'::INTERVAL
Add three months to the first day of the month of this date: date_trunc('month', date) + 3*('1 month'::INTERVAL)
The interval is a data type, not a string, and you can do computations with its values.

Sort timestamps (including future) by absolute distance from "now"

With a date field I can do this:
ORDER BY ABS(expiry - CURRENT_DATE)
With a timestamp field I get the following error:
function abs(interval) does not exist
Use now() or CURRENT_TIMESTAMP for the purpose.
The reason for the different outcome of your queries is this:
When you subtract two values of type date, the result is an integer and abs() is applicable.
When you subtract two values of type timestamp (or just one is a timestamp), the result is an interval, and abs() is not applicable. You could substitute with a CASE expression:
ORDER BY CASE WHEN expiry > now() THEN expiry - now() ELSE now() - expiry END
Or you can extract() the unix epoch from the resulting interval like #Craig already demonstrated. I quote: "for interval values, the total number of seconds in the interval". Then you can use abs() again:
ORDER BY abs(extract(epoch from (expiry - now())));
age() would just add a more human readable representation to the interval by summing up days into months and years for for bigger intervals. But that's beside the point: the value is only used for sorting.
As your column is of type timestamp, you should use CURRENT_TIMESTAMP (or now()) instead of CURRENT_DATE, or you will get inaccurate results (or even incorrect for "today").
Compare with current_timestamp
SELECT the_timestamp > current_timestamp;
The age function is probably what you want when comparing them:
SELECT age(the_timestamp);
eg:
regress=# SELECT age(TIMESTAMP '2012-01-01 00:00:00');
age
----------------
8 mons 17 days
(1 row)
If you want an absolute distance, use:
SELECT abs( extract(epoch from age(the_timestamp)) );
This works (and gives the correct sorting):
ABS(EXTRACT(DAY FROM expiry - CURRENT_TIMESTAMP))
Unfortunately, as Erwin Brandstetter pointed out, it reduces the granularity of the sorting to a full day.