Calculate time difference between two timestamp with additional conditions in postgres - postgresql

I would like to get the total number of hours,minute, and seconds difference between two timestamp fields based on starting and ending time in postgres
Table supermarket has fields opening_time and closing_time
Table orders have the fields order_id, arrived_date, and picked_date
I want to calculate the total time spend to pick an order. (The orders can be picked only the supermarket is open but the orders will get placed and queued anytime in the day)
Condition: The total picking time should be calculated considering the opening and closing time of the store.
Example
Consider opening_time is 09:00:00 and closing_time is 22:00:00
Case 1: if an order arrives on 2020-09-08 10:00:00 and is picked at 2020-09-08 12:00:00, then the total picking time should be 02 hours
Case 2: if an order arrives on 2020-09-08 06:00:00 and is picked at 2020-09-08 12:00:00, then the total picking time should be 03 hours, not 06 hours considering the opening time
Case 3: if an order arrives on 2020-09-08 23:00:00 and is picked next day at 2020-09-09 10:00:00, then the total picking time should be 01 hour considering closing and opening time

It is a bit more complicated that it would seem to, especially if the ranges may expand over more than 24 hours. The safest solution approach might be a brute-force approach that generates all hours in the range, then filtering and aggregation:
select s.*, x.*
from supermarket s
cross join lateral (
select count(*) no_hours
from generate_series(s.opening_time, s.closing_time, '1 hour') x(x_time)
where x_time::time >= '09:00:00'::time and x_time::time < '22:00:00'::time
) x
This assumes that the opening and closing dates are truncated to the hour, as shown in your examples. If you want to handle minutes, then:
select s.*, x.*
from supermarket s
cross join lateral (
select count(*) no_minutes
from generate_series(s.opening_time, s.closing_time, '1 minute') x(x_time)
where x_time::time >= '09:00:00'::time and x_time::time < '22:00:00'::time
) x

Timestamps can simply be subtracted resulting in an interval.
test=# select '2020-09-08 12:00:00'::timestamp - '2020-09-08 10:00:00'::timestamp;
?column?
----------
02:00:00
(1 row)
That interval can then be formatted with to_char.
test=# select to_char('2020-09-08 12:00:00'::timestamp - '2020-09-08 10:00:00'::timestamp, 'HH24 hours MI "minutes"');
to_char
---------------------
02 hours 00 minutes
(1 row)
To get the correct interval considering opening and closing time, you need a little bit of logic to figure out the actual picking start time.
select
case
when arrived_date::time < opening_time
-- It arrived before you're open. Start when you open that day.
arrived_date::date + opening_time
when arrived_date::time > closing_time
-- It arrived after you closed. Start when you open tomorrow.
arrived_date::date + '1 day'::interval + opening_time
else
-- It arrived while you're open. Start when it arrives.
arrived_date
end as picking_start
The trick is to cast arrived_date to time, truncating the date part, to compare with the opening and closing times. Similarly, we can cast arrived_date to date and use just the date part, then add the opening time. This assumes that arrived_date is a timestamp and that opening_time and closing_time are time columns.
This could be condensed into a function for easy use.

Related

Postgres query certain overlapping times on weekdays

Imagine you have a set of records for a certain period of time, for example produced products in a month.
Now you want to know how many of those products have been produced at 22 to 3 on Wednesdays and Fridays.
To get all products let's assume the query looks like:
SELECT count(id)
FROM products
WHERE timestamp BETWEEN to_timestamp($startOfMonth) AND to_timestamp($endOfMonth)
where $startOfMonth and $endOfMonth are placeholder values for some integer timestamps.
To get products of a certain time we can make use of "BETWEEN TIME" and add it to the query:
SELECT count(id)
FROM products
WHERE timestamp BETWEEN to_timestamp($startOfMonth) AND to_timestamp($endOfMonth)
AND CAST(timestamp AS time) BETWEEN TIME '3:00' AND TIME '22:00'
Next thing we do is adding the exclusive days we want to query:
SELECT count(id)
FROM products
WHERE timestamp BETWEEN to_timestamp($startOfMonth) AND to_timestamp($endOfMonth)
AND CAST(timestamp AS time) BETWEEN TIME '3:00' AND TIME '22:00'
AND EXTRACT(dow FROM timestamp) IN (3,5)
Everything works fine.
But now I want to query a time overlapping a day:
SELECT count(id)
FROM products
WHERE timestamp BETWEEN to_timestamp($startOfMonth) AND to_timestamp($endOfMonth)
AND CAST(timestamp AS time) BETWEEN TIME '22:00' AND TIME '12:00'
AND EXTRACT(dow FROM timestamp) IN (3,5)
and I don't receive any results.
How to run this query if the starting time is on another day than the ending time?
Thank you ;-)
BETWEEN TIME '3:00' AND TIME '22:00' seems to be the opposite of what you're asking in the question ("produced at 22 to 3").
I think you'll just have to split the time range and use an OR condition, since it crosses midnight. Assuming that "22 to 3" on a Wednesday, you actually mean "22 to midnight" on a Wednesday and "midnight to 3am" on a Thursday (same for Friday/Saturday)
You won't even need BETWEEN, since the lower/upper bounds will be midnight, when the day changes. Something like this should work:
SELECT count(id)
FROM products
WHERE timestamp BETWEEN to_timestamp($startOfMonth) AND to_timestamp($endOfMonth)
AND ((CAST(timestamp AS time) >= TIME '22:00'
AND EXTRACT(dow FROM timestamp) IN (3,5))
OR (CAST(timestamp AS time) < TIME '03:00'
AND EXTRACT(dow FROM timestamp) IN (4,6)))
You may need to adapt this a little if you want to include the timestamps that are on the first Thursday or Saturday of the month (before 3am) into the count for the previous month (if it makes sense for your report): a quick way to do this is to adapt the integer timestamp you provide $endOfMonth to be at 3am on the first day of the following month.
(As a side note, I'd avoid using timestamp as a column name, since it's also a type and can cause confusion.)
Perhaps an easier way is to restate the times in question. That is instead of working with TIMEs a 22:00 and 03:00 consider start time as date+22hours and end time as date+27 hours.(duration of 5 hours from 22:00 - 03:00) Then process with the full timestamp.
SELECT count(id)
FROM products
WHERE timestamp BETWEEN to_timestamp($startOfMonth) AND to_timestamp($endOfMonth)
AND EXTRACT(dow FROM timestamp) IN (3,5)
and timestamp between date_trunc('day', timestamp) + interval '22 hours'
and date_trunc('day', timestamp) + interval '27 hours';
Note: I echo #Bruno on not using timestamp as a column (or any DB object) name. While it is not a Postgres reserved word, Postgres restricts is use and it is a SQL Standard reserved word.

Postgres: How to change start day of week and apply it in date_part?

with partial as(
select
date_part('week', activated_at) as weekly,
count(*) as count
from vendors
where activated_at notnull
group by weekly
)
This is the query counts number of vendors activating per week. I need to change the start day of week from Monday to Saturday. Similar posts like how to change the first day of the week in PostgreSQL or Making Postgres date_trunc() use a Sunday based week but non explain how to embed it in date_part function. I would like to know how to use this function in my query and start day from Saturday.
Thanks in advance.
maybe a little bit overkill for that, you can use some ctes and window functions, so first generate your intervals, start with your first saturday, you want e.g. 2018-01-06 00:00 and the last day you want 2018-12-31, then select your data, join it , sum it and as benefit you also get weeks with zero activations:
with temp_days as (
SELECT a as a ,
a + '7 days'::interval as e
FROM generate_series('2018-01-06 00:00'::timestamp,
'2018-12-31 00:00', '7 day') as a
),
temp_data as (
select
1 as counter,
vendors.activated_at
from vendors
where activated_at notnull
),
temp_order as
(
select *
from temp_days
left join temp_data on temp_data.activated_at between (temp_days.a) and (temp_days.e)
)
select
distinct on (temp_order.a)
temp_order.a,
temp_order.e,
coalesce(sum(temp_order.counter) over (partition by temp_order.a),0) as result
from temp_order

Column of type "timestamp(6) with timezone" and "current time" difference in minutes

I have Oracle 12c DB table and one of it's column utc_timestamp is of type
UTC_TIMESTAMP TIMESTAMP(6) WITH TIME ZONE
It stores timestamp in UTC while current_timestamp and systimestamp both gives timestamp in different timezones.
How can I get time difference in MAX(utc_timestamp) and current_timestamp in minutes ignoring time difference due to different time zones.
For example:
select current_timestamp from dual;
Gives=> 23-AUG-17 04.43.16.253931000 PM AMERICA/CHICAGO
select systimestamp from dual;
Gives=> 23-AUG-17 05.43.16.253925000 PM -04:00
select max(UTC_TIMESTAMP) from table_name;
Gives=> 23-AUG-17 09.40.02.000000000 PM +00:00
For above condition when I run SQL to check time difference between in MAX(utc_timestamp) and current_timestamp I should get number 3.
I think I need something like:
select (extract(minute from current_timestamp) - extract(minute from max(UTC_TIMESTAMP)) * 1440) AS minutesBetween from table_name;
But different timezones are messing it up and I get negative number like -4317. This might be correct as current_timestamp will be higher than max(utc_timestamp) being in CST. So I tried:
select (extract(minute from CAST(current_timestamp as TIMESTAMP(6) WITH TIME ZONE)) - extract(minute from max(UTC_TIMESTAMP)) * 1440) AS minutesBetween from table_name;
This SQL runs without error but producing a big negative number like -83461. Please help me find what am I doing wrong.
You really have two problems here.
One is to convert CURRENT_TIMESTAMP to UTC. That is trivial:
select CURRENT_TIMESTAMP AT TIME ZONE 'UTC' from dual [.....]
(use the AT TIME ZONE clause https://docs.oracle.com/cd/B19306_01/server.102/b14225/ch4datetime.htm#i1007699)
The other is that the difference between two timestamps is an interval, not a number.
select current_timestamp at time zone 'UTC'
- to_timestamp_tz('24-AUG-17 04.00.00.000 AM UTC', 'dd-MON-yy hh.mi.ss.ff AM TZR')
from dual;
produces something like
+00 00:02:39.366000
which means + (positive difference) 00 days, 00 hours, 02 minutes, 39.366 seconds.
If you just want the minutes (always rounded down), you may wrap this whole expression within extract( minute from < ...... > ). Be aware though that the answer will still be 2 (minutes) even if the difference is five hours and two minutes. It is probably best to leave the result in interval data type, unless you are 100% sure (or more) that the result is always less than 1 hour.

postgresql table data movement

In PostgreSQL i want to create script which can delete old data of before 1 month from A table(which contain many rows) and insert this data into one new alias table. and i want to execute this script every month.
for that i have created script as
insert into B select * from A where date >(now-'30 day'::interval);
delete from A where date <(now()-'30 days.
but in some month there is 30 days and in some 31 days .so how can i set this in cron tab to delete exact data and move in alias table.
While Laurenz answer makes much sense and seems to be a good guess of what OP really wants (cron monthly probably means he wants to flush not "older than one month", but of "previous month" so date_trunc to month is a case)
Yet answering it the other way (as I understand the original post):
begin;
insert into B select * from A where date >(now()-'1 month'::interval);
delete from A where date <(now()-'1 month'::interval);
end;
will delete not all data for previous month, but same timestamp one month ago, eg:
t=# select now()-'1 month'::interval;
?column?
-------------------------------
2017-05-12 07:31:31.785402+00
(1 row)
And with this logic you might want to schedule this data purge daily, not monthly - to keep last month data in active table, not "up to two" untill cron fires...
run it on the first of every month and write it like this:
... WHERE date >= date_trunc('month', current_timestamp) - INTERVAL '1 month'
AND date < date_trunc('month', current_timestamp)
If your table contains a lot of data, you might want to look into partitioning.

Select Data over time period

I'm a bit of newbie when it comes to postgres, so bear with me a wee bit and i'll see if i can put up enough information.
i insert weather data into a table every 10 mins, i have a time column that is stamped with an epoch date.
I Have a column of the last hrs rain fall, and every hr that number changes of course with the running total (for that hour).
What i would like to do is skim through the rows to the end of each hour, and get that row, but do it over the last 4 hours, so i would only be returning 4 rows say.
Is this possible in 1 query? Or should i do multiple queries?
I would like to do this in 1 query but not fussed...
Thanks
Thanks guys for your answers, i was/am a bit confused by yours gavin - sorry:) comes from not knowing this terribly well.
I'm still a bit unsure about this, so i'll try and explain it a bit better..
I have a c program that inserts data into the database every 10 mins, it reads the data fom a device that keeps the last hrs rain fall, so every 10 mins it could go up by x amount.
So i guess i have 6 rows / hr of data.
My plan was to go back (in my php page) every 7, which would be the last entry for every hour, and just grab that value. Hence why i would only ever need 4 rows.. just spaced out a bit!
My table (readings) has data like this
index | time (text) | last hrs rain fall (text)
1 | 1316069402 | 1.2
All ears to better ways of storing it too :) I very much appreciate your help too guys thanks.
You should be able to do it in one query...
Would something along the lines of:
SELECT various_columns,
the_hour,
SUM ( column_to_be_summed )
FROM ( SELECT various_columns,
column_to_be_summed,
extract ( hour FROM TIME ) AS the_hour
FROM readings
WHERE TIME > ( NOW() - INTERVAL '4 hour' ) ) a
GROUP BY various_columns,
the_hour ;
do what you need?
SELECT SUM(rainfall) FROM weatherdata WHERE time > (NOW() - INTERVAL '4 hour' );
I don't know column names but that should do it the ones in caps are pgsql types. Is that what you are after?
I am not sure if this is exactly what you are looking for but perhaps it may serve as a basis for adaptation.
I often have a requirment for producing summary data over time periods though I don't use epoch time so there may be better ways of manipulating the values than I have come up with.
create and populate test table
create table epoch_t(etime numeric);
insert into epoch_t
select extract(epoch from generate_series(now(),now() - interval '6 hours',interval '-10 minutes'));
To divide up time into period buckets:
select generate_series(to_char(now(),'yyyy-mm-dd hh24:00:00')::timestamptz,
to_char(now(),'yyyy-mm-dd hh24:00:00')::timestamptz - interval '4 hours',
interval '-1 hour');
Convert epoch time to postgres timestamp:
select timestamptz 'epoch' + etime * '1 second'::interval from epoch_t;
then truncate to hour :
select to_char(timestamptz 'epoch' + etime * '1 second'::interval,
'yyyy-mm-dd hh24:00:00')::timestamptz from epoch_t
To provide summary information by hour :
select to_char(timestamptz 'epoch' + etime * '1 second'::interval,
'yyyy-mm-dd hh24:00:00')::timestamptz,
count(*)
from epoch_t
group by 1
order by 1 desc;
If you might have gaps in the data but need to report zero results use a generate_series to create period buckets and left join to data table.
In this case I create sample hour buckets back prior to the data population above - 9 hours instead of 6 and join on the conversion of epoch time to timestamp truncated to hour.
select per.sample_hour,
sum(case etime is null when true then 0 else 1 end) as etcount
from (select generate_series(to_char(now(),
'yyyy-mm-dd hh24:00:00')::timestamptz,
to_char(now(),'yyyy-mm-dd hh24:00:00')::timestamptz - interval '9 hours',
interval '-1 hour') as sample_hour) as per
left join epoch_t on to_char(timestamptz 'epoch' + etime * '1 second'::interval,
'yyyy-mm-dd hh24:00:00')::timestamptz = per.sample_hour
group by per.sample_hour
order by per.sample_hour desc;