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

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.

Related

Can't extract date from milliseconds epoch postgresql

I'm querying the database (RedShift) and I have a piece of information stored in epoch MS format. Envision a table along the lines of:
Purchase, date
1, 1620140227019
2, 1620140227045
3, 1620140226573
I need to convert the timestamp to a readable date but I can't make it work with to_timestamp() or extract(). The problem is first with the size of the value (13 digits are not supported).
The closest solution I have is
select to_timestamp(1620140226573/1000, 'SS')
But the result is 0051-05-04 14:57:06. In other words month, date and seconds are correct but the year is wrong.
You can run this query
select to_timestamp(round(1620140227254/1000))
The solution was in the documentation: https://docs.aws.amazon.com/redshift/latest/dg/r_Dateparts_for_datetime_functions.html
SELECT timestamp with time zone 'epoch' + 1620140227019/1000 * interval '1 second' AS converted_timestamp
or
select '1970-01-01'::date + 1620140227019/1000 * interval '1 second'

How to subtract a number of days in one column from CURRENT_DATE in PostgreSQL

I have a column called number_of_days_since_event and I want to change it to data_of_last_event, the way I can do that is subtracting today's date from the number of days in the number_of_days_since_event column. But I do not know how to subtract days in a column.
This question answers the problem when you know the number of days in advance, i.e., if I would like to subtract 10 days from today it would be:
SELECT CURRENT_DATE - INTERVAL '10 days';
However, I would like to do something like:
SELECT CURRENT_DATE - INTERVAL "myTable.number_of_days_since_event" 'days'
FROM myTable;
But this does not work leading to the error message: syntax error at or near "'day'"
The following using concat solves my problem:
SELECT CURRENT_DATE - concat(myTable.number_of_days_since_event::text,' day')::interval
FROM myTable;
If you are happy with a date result, you could use
SELECT current_date - number_of_days_since_event::integer
FROM mytable;

PostgreSql: how to do where CURRENT_DATE - timestamp > (Value of years/months/days)

May I ask, in PostgreSql, how to do
where CURRENT_DATE - timestamp > (Value of years/months/days).
Here I'd like to calculate the age of a person and verify if it is older or younger than a certain value, so I need to make the subtraction of CURRENT_DATE and timestamp when the person was born, but then I do not know how to do, anyway one could help me?
Thank you very much
You'll need the INTERVAL type.
It can be subtracted from DATE/TIMESTAMP, so your best predicate will be:
WHERE CURRENT_TIMESTAMP - INTERVAL '18 years' > your_timestamp_column
Or, (when the given INTERVAL should be a query parameter, and) if you can only bind basic types (most likely), you can just bind f.ex. an INTEGER year value:
WHERE CURRENT_TIMESTAMP - (? * INTERVAL '1 year') > your_timestamp_column
Technically, your original logic could work too:
WHERE CURRENT_TIMESTAMP - your_timestamp_column > INTERVAL '18 years'
But that won't use any indexes on your_timestamp_column & is subject to INTERVAL justification.

Postgresql. Dates interval issue

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 )

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.