Get elapsed/covered time in PostgreSQL - sql

I have the following table:
CREATE TABLE my_table
(
the_visitor_id varchar(5) NOT NULL,
the_visitor_visit timestamp NOT NULL
)
INSERT INTO my_table
VALUES ('VIS01', '2019-05-02 09:00:00'),
('VIS02', '2019-05-04 12:00:00'),
('VIS03', '2019-05-06 18:30:00'),
('VIS04', '2019-05-15 12:00:00'),
('VIS05', '2019-06-30 18:00:00')
I want to retrieve the time elapsed/covered (from a reference point) with decimals by hour, day and week. For example, for VIS01, the time elapsed would be 0.375 (9/24 from 0:00:00), the day elapsed would be 0.04435 (1 day 9 hours makes 1.375 divided by 31 days is 0.04435 in May, the month of the visit) and the week elapsed would be 3.375 (3 days 0.375 hours from the Monday in the week).
For VIS05 the time elapsed is 0.75 (18 hours/24 hours), the day elapsed would be 0.9917 (29 days .75 covered hours makes 29.75 divided by 30 days of June) and the week elapsed would be 6.75 (6 days from Monday in a week that starts on Monday and finish on Sunday plus 0.75 hours)
This should be the result:
the_visitor_id visitor_time_el visitor_days_el visitor_week_el
VIS01 0.375 0.0444 3.375
VIS02 0.5 0.1129 5.5
VIS03 0.7708 0.1539 0.7708
VIS04 0.5 0.4677 2.5
VIS05 0.75 0.9917 6.75
I've been stuck because I want to use this clause:
SELECT the_visitor_visit - date_trunc('month', the_visitor_visit) as visitor_days_el
But I don't quite get how I can convert that to decimals, and what to do with visitor_week_el. Please, any help will be greatly appreciated.

This is basically using epoch to extract the seconds from various date differences:
select t.*,
extract(epoch from the_visitor_visit::time) / (60 * 60 * 24) as visitor_time_el,
(extract(epoch from (the_visitor_visit - date_trunc('month', the_visitor_visit))) /
extract(epoch from (date_trunc('month', the_visitor_visit) + interval '1 month' - date_trunc('month', the_visitor_visit)))
) as visitor_day_el,
extract(epoch from (the_visitor_visit - date_trunc('week', the_visitor_visit))) / (60 * 60 * 24) as visitor_week_el
from my_table t
Here is a db<>fiddle.

Related

Different results when subtracting two timestamps using AGE (PostgreSQL)

Can anyone help me to understand why these are showing different outputs?
SELECT
EXTRACT (epoch FROM ('2021-02-01 00:00:00'::timestamp - '2021-01-01 00:00:00'::timestamp)) / 3600 / 24 as time1,
EXTRACT (epoch FROM age('2021-02-01 00:00:00'::timestamp , '2021-01-01 00:00:00'::timestamp)) / 3600 / 24 as time2
output:
time1
time2
31
30
There is a difference between the subtract operator and the function age(), per the documentation::
timestamp - timestamp › interval
Subtract timestamps (converting 24-hour intervals into days, similarly to justify_hours())
versus
age ( timestamp, timestamp ) › interval
Subtract arguments, producing a “symbolic” result that uses years and months, rather than just days
Example:
SELECT
'2021-02-01 00:00:00'::timestamp - '2021-01-01 00:00:00'::timestamp as interval1,
age('2021-02-01 00:00:00'::timestamp , '2021-01-01 00:00:00'::timestamp) as interval2
interval1 | interval2
-----------+-----------
31 days | 1 mon
(1 row)
The second interval is converted to 30 days in the further calculations described in the question.

How to extract the hour of day from an epoch and count each instance that occurs during that hour?

I have a question that I feel is pretty straight forward but is giving me some issues.
I have a column in table X called event_time which is an epoch. I am wanting to extract the hour of day out of that and count the number of rides that have occurred during that hour.
So the output will end up being a bar chart with x values 0-24 and the Y being the number of instances that occur (which is bike rides for example).
Here is what I have now, that isn't giving me the correct output:
select extract(hour from to_timestamp(start_time)::date) as hr,
count(*) as ct
from x
group by hr
order by hr asc
Any hints or help are appreciated.
Thanks
You can use arithmetic:
select floor( (start_time % (24 * 60 * 60)) / (60 * 60) ) as hour,
count(*)
from x
group by hour;
Or convert to a date/time and extract the hour:
select extract(hour from '1970-01-01'::date + start_time * interval '1 second') as hour, count(*)
from x
group by hour;

SQL Redshift difference in days between 2 timestamps

In my Redshift table I have 2 columns that stores timestamp values: start_date_time and end_date_time.
I need to find the difference between start_date_time and end_date_time, such that, if difference is 1 day, then result should be 1. If diff is 12 hours, than result should be 0.5, if diff is 8 hours - than 0.3333 etc.
How can I do this? I tried to use datediff function: but that will not return what I wanted:
select datediff(day,'2011-12-31 8:30:00','2011-12-31 20:30:00') as day_diff;
Will result in 0. But I need 0.5, because difference is 12 hours.
Then just do it in hours and divide by 24.0?
DATEDIFF(HOUR,'2011-12-31 8:30:00','2011-12-31 20:30:00') / 24.0
Or for better granularity, in minutes or seconds?
DATEDIFF(MINUTE,'2011-12-31 8:30:00','2011-12-31 20:30:00') / 1440.0
DATEDIFF(SECOND,'2011-12-31 8:30:00','2011-12-31 20:30:00') / 86400.0
Use a smaller time unit:
select datediff(hour, '2011-12-31 8:30:00', '2011-12-31 20:30:00')/24.0 as day_diff;
Or:
select datediff(minute, '2011-12-31 8:30:00', '2011-12-31 20:30:00')/(24.0 * 60) as day_diff;
Or:
select datediff(second, '2011-12-31 8:30:00', '2011-12-31 20:30:00')/(24.0 * 60 * 60) as day_diff;

SQL statement dynamically using current time to choose a time frame in a field (Oracle)

All, I have something that is stumping me and I have seen a lot of examples, but nothing is helping solve this.
I have time frames like 03:30:00 to 11:29:59 that I work with (say shift times). I want to dynamically query data for the last shift based on the current shift.
Example: if it is currently between 11:30:00 AM and 7:29:59 PM, I want get the last shift that was between 03:30:00 AM and 11:30:00 AM.
This would look like an if statement in my mind:
If time between .... then
select time between....
elseif time between.... then
select time between...
I tried many combinations and can't figure this out. I think I would need a CASE and maybe a subquery? or maybe DECODE will work?
SELECT CAST(ccd.DATEc AS TIME) as time_occured,
FROM db.datatb ccd
WHERE ccd.DATE > SYSDATE - interval '1440' minute
AND (
((TO_CHAR(SYSDATE, 'hh24:mi:ss')BETWEEN '03:30:00' AND '11:29:59' IN (SELECT
ccd.DATEc FROM db.datatb WHERE (CAST(ccd.DATEc AS TIME)NOT BETWEEN '03:30:00
AM' AND '07:29:59 PM')))
OR (TO_CHAR(SYSDATE, 'hh24:mi:ss')BETWEEN '11:30:00' AND '19:29:59' IN
(SELECT ccd.DATEc FROM db.datatb WHERE (CAST(ccd.DATEc AS TIME) BETWEEN
'03:30:00 AM' AND '11:29:59 AM')))
OR (TO_CHAR(SYSDATE, 'hh24:mi:ss')NOT BETWEEN '03:30:00' AND '19:29:59' IN
(SELECT ccd.DATEc FROM db.datatb WHERE (CAST(ccd.DATEc AS TIME) BETWEEN
'11:30:00 AM' AND '07:29:59 PM')))
)
SELECT *
FROM db.datatb
CROSS JOIN
( SELECT TRUNC( SYSDATE - INTERVAL '210' MINUTE )
+ NUMTODSINTERVAL(
TRUNC(
( SYSDATE - INTERVAL '210' MINUTE
- TRUNC( SYSDATE - INTERVAL '210' MINUTE )
) * 3
) * 480
+ 210,
'MINUTE'
) AS current_shift_start
FROM DUAL
) css
WHERE DATEc >= css.current_shift_start - INTERVAL '8' HOUR
AND DATEc < css.current_shift_start;
Explanation:
The shifts are 8 hours each starting at 03:30 (or 210 minutes past midnight); so SYSDATE - INTERVAL '210' MINUTE will move offset the times so that after this offset they start at 00:00, 08:00 and 16:00 which is thirds of a day.
date_value - TRUNC( date_value ) calculates the fraction of a day (between 0 and 1) that the time component represents; so TRUNC( ( date_value - TRUNC( date_value ) ) * 3 ) maps that fraction of the day to 0, 1 or 2 corresponding to whether it is in the 1st, 2nd or 3rd 8 hour period of the day. Multiple that value by 480 minutes and then add the 210 minutes that the date was originally offset by and you have the minutes past the start of the day that the shift starts.

How to get numeric value of Months between two dates in PostgreSQL?

I have two dates in format Time Stamp Without Time Zone.
I want to compare them and get the numeric value of months between them:
select age(NOW(), '2012-03-24 14:44:55.454041+03')
Gives:
4 years 9 mons 2 days 21:00:27.165482
The trick here is that I need to convert this result into one value of months.
So:
In order to convert the YEARS to Months:
select EXTRACT(YEAR FROM age) * 12 + EXTRACT(MONTH FROM age)
FROM age(NOW(), '2012-06-24 14:44:55.454041+03') AS t(age)
I get 57 which is 4*12+9.
My problem is that I don't know how to convert the days.
In the above example I need to convert the '2 days' into it's value in months.
'2 days' isn't 0 months!
In Months of 30 days 15 days are 0.5 months.
How can I do that?
The final result should be 57.something
You can get a rough estimation with:
select (extract(epoch from timestamptz '2012-06-24 14:44:55.454041+03')
- extract(epoch from timestamptz '2017-03-27 00:00:00+03'))
/ extract(epoch from interval '30.44 days') rough_estimation
(you can divide with extract(epoch from interval '1 month') for an even more rough estimation).
The problem with your original formula is that it is designed to give a complete month difference between two dates. If you want to account days too an interesting problem arises: in your example, the result should be 57 months and 2 days 21:00:27.165482. But in what month should the 2 days 21:00:27.165482 part is calculated? An average-length month (30.44 days)? If you want to be precise, it should be noted that in your example case, the difference is really only 56 months, plus almost 7 days in 2012-06 (which had 30 days) and 27 days in 2017-03 (which has 31 days). The question you should ask yourself: is it really worth an advanced formula which takes account both range ends' days-in-a-month or not?
Edit: For completeness, here is a function, which can take both range end into consideration:
create or replace function abs_month_diff(timestamptz, timestamptz)
returns numeric
language sql
stable
as $func$
select extract(year from age)::numeric * 12 + extract(month from age)::numeric
+ (extract(epoch from (lt + interval '1 month' - l))::numeric / extract(epoch from (lt + interval '1 month' - lt))::numeric)
+ (extract(epoch from (g - gt))::numeric / extract(epoch from (gt + interval '1 month' - gt))::numeric)
- case when gt <= l or lt = l then 1 else 0 end
from least($1, $2) l,
greatest($1, $2) g,
date_trunc('month', l) lt,
date_trunc('month', g) gt,
age(gt, l)
$func$;
(Note: if you use timestamp instead of timestamptz, this function is immutable instead of stable. Just like the date_trunc functions.)
So:
select age('2017-03-27 00:00:00+03', '2012-06-24 14:44:55.454041+03'),
abs_month_diff('2017-03-27 00:00:00+03', '2012-06-24 14:44:55.454041+03');
will yield:
age | abs_month_diff
---------------------------------------+-------------------------
4 years 9 mons 2 days 09:15:04.545959 | 57.05138456751051843959
http://rextester.com/QLABV31257 (outdated)
Edit: function is corrected to produce exact results when the difference is less than a month.
See f.ex:
set time zone 'utc';
select abs_month_diff('2017-02-27 00:00:00+03', '2017-02-24 00:00:00+03'), 3.0 / 28,
abs_month_diff('2017-03-27 00:00:00+03', '2017-03-24 00:00:00+03'), 3.0 / 31,
abs_month_diff('2017-04-27 00:00:00+03', '2017-04-24 00:00:00+03'), 3.0 / 30,
abs_month_diff('2017-02-27 00:00:00+00', '2017-03-27 00:00:00+00'), 2.0 / 28 + 26.0 / 31;
http://rextester.com/TIYQC5325 (outdated)
Edit 2: This function is based on the following formula, to calculate the length of a month:
select (mon + interval '1 month' - mon)
from date_trunc('month', now()) mon
This will even take DST changes into account. F.ex. in my country there was a DST change yesterday (on 2017-03-26), so today (2017-03-27) the above query reports: 30 days 23:00:00.
Edit 3: Function is corrected again (thanks to #Jonathan who noticed an edge-case of an edge-case).
http://rextester.com/JLG68351
You can obtain an approximate value with something similar to this:
SELECT A, B*12+C-1+E/30 MONTHSBETWEEN
FROM (
SELECT current_timestamp A, EXTRACT(YEAR FROM current_timestamp) B, EXTRACT(MONTH FROM current_timestamp) C, EXTRACT(DAYS FROM current_timestamp) E
) X;
or better precision with something like this:
SELECT A, B*12+C-1+E/ DATE_PART('days',
DATE_TRUNC('month', A)
+ '1 MONTH'::INTERVAL
- '1 DAY'::INTERVAL
) MONTHSBETWEEN
FROM (
SELECT current_timestamp A, EXTRACT(YEAR FROM current_timestamp) B, EXTRACT(MONTH FROM current_timestamp) C, EXTRACT(DAYS FROM current_timestamp) E
) X;
This should do:
select (EXTRACT(YEAR FROM age) * 12 + EXTRACT(MONTH FROM age) + EXTRACT(DAY FROM age) / 30)::numeric
FROM age(NOW(), '2012-06-24 14:44:55.454041+03') AS t(age)
You can also add ROUND() to make it prettier:
select ROUND((EXTRACT(YEAR FROM age) * 12 + EXTRACT(MONTH FROM age) + EXTRACT(DAY FROM age) / 30)::numeric,2) as Months
FROM age(NOW(), '2012-06-24 14:44:55.454041+03') AS t(age)