How to have multiple colums in a variable/array - sql

I have this query
select expiration_date, driver_id from all_requirements_driver_documents where expiration_date <= (now() + interval '1 month')::DATE and expiration_date > (now() - interval '7 day')::DATE
union
select expiration_date, driver_id from all_requirements_vehicle_documents where expiration_date <= (now() + interval '1 month')::DATE and expiration_date > (now() - interval '7 day')::DATE
I want to insert it into a variable in which I can loop and perform actions.
i.e unsatified_documents driver_expiring_documents;
where
create table if not exists driver_expiring_documents (
driver_id text,
expiration_date date
);
and then loop through it
if(unsatified_documents != '{}') then
foreach expiring_doc in array unsatified_documents loop
-- -- Get Driver ID --
driver_id_arg := expiring_doc.driver_id;
expiring_date := expiring_doc.expiration_date;
end loop;
end if;
How can I do it?

You can store it in an array variable, but if you want to loop through the results, that would be unnecessarily complicated and inefficient. You'd have to pack it into a data structure that potentially uses a lot of RAM, only to unpack it right away.
Simply do
DECLARE
r record;
BEGIN
FOR r IN
select expiration_date, driver_id from ... WHERE ...
union
select expiration_date, driver_id from ... where ...
LOOP
/* do something with "r.expiration_date" and "r.driver_id" */
END LOOP;
END;

You can use a custom type, for example:
CREATE TYPE my_doc
AS
(
driver_id text,
expiration_date date
);
Then, if you want make an array of these values you can do:
SELECT array_agg((driver_id, expiration_date)::my_doc)
FROM driver_expiring_documents;
But you don't even need the array, which can be pretty big, you can just loop in a function with this simple query:
SELECT (driver_id, expiration_date)::my_doc
FROM driver_expiring_documents;
Or, you can skip the intermediate table and select with the union:
select (expiration_date, driver_id)::my_doc from all_requirements_driver_documents where expiration_date <= (now() + interval '1 month')::DATE and expiration_date > (now() - interval '7 day')::DATE
union
select (expiration_date, driver_id)::my_doc from all_requirements_vehicle_documents where expiration_date <= (now() + interval '1 month')::DATE and expiration_date > (now() - interval '7 day')::DATE
And just a consideration: if you can, use UNION ALL, it will be faster.

Related

How to get the next business date in postgres?

Business days are Monday through Friday.
Given I have a datetime field scheduled_for, how can I find the next business date and return that in a column alias?
I've tried something from another SO answer but it doesn't work as intended.
EXTRACT(ISODOW FROM v.scheduled_for)::integer) % 7 as next_business_day,
Error:
Query 1 ERROR: ERROR: syntax error at or near ")"
LINE 3: EXTRACT(ISODOW FROM v.scheduled_for)::integer % 7) as next...
^
Edit:
Thanks for the suggestions, I've attempted this:
SELECT
v.id AS visit_id,
(IF extract(''dow'' from v.scheduled_for) = 0 THEN
return v.scheduled_for + 1::integer;
ELSIF extract(''dow'' from v.scheduled_for) = 6 THEN
return v.scheduled_for - 1::integer;
ELSE
return v.scheduled_for;
) as next_business_day,
'' as invoice_ref_code,
The error I get is:
Query 1 ERROR: ERROR: syntax error at or near ")"
LINE 1: ) as next_business_day,
^
To generalize you need to create a function to calculate the next business day from a given date.
create or replace function utl_next_business_day(date_in date default current_date)
returns date
language sql immutable leakproof strict
as $$
with cd as (select extract(isodow from date_in)::integer d)
select case when d between 1 and 4
then date_in + 1
else date_in + 1 + (7-d)
end
from cd;
$$;
--- any single date
select current_date, utl_next_business_day();
-- over time span (short)
select gdate::date for_date, utl_next_business_day(gdate::date) next_business_day
from generate_series( current_date, current_date + 14, interval '1 day') gdate;
-- around year end over a time span
with test_date (dt) as
( values (date '2019-12-31')
, (date '2020-12-31'), (date '2021-12-31'),(date '2022-12-31')
, (date '2021-01-01'), (date '2022-01-01'),(date '2023-01-01')
)
select dt, utl_next_business_day(dt) from test_date
order by dt;
Alternatively with the calendar table suggestion from #Eric we get.
-- create and populate work table
create table bus_day_calendar ( bus_day date);
insert into bus_day_calendar (bus_day)
select utl_next_business_day(gdate::date)
from generate_series( date '2018-12-31', date '2023-01-01', interval '1 day') gdate
where extract(isodow from gdate)::integer not in (6,7) ;
--- Function to return next business day
create or replace function utl_next_cal_business_day(date_in date default current_date)
returns date
language sql stable leakproof strict
as $$
select min(bus_day)
from bus_day_calendar
where bus_day > date_in;
$$;
--- any single date
select current_date, utl_next_cal_business_day();
-- over time span (short)
select gdate::date for_date, utl_next_cal_business_day(gdate::date) next_business_day
from generate_series( current_date, current_date + 14, interval '1 day') gdate;
-- around year end over a time span
with test_date (dt) as
( values (date '2019-12-31')
, (date '2020-12-31'), (date '2021-12-31'),(date '2022-12-31')
, (date '2021-01-01'), (date '2022-01-01'),(date '2023-01-01')
)
select dt, utl_next_cal_business_day(dt) from test_date
order by dt;
Neither of these as they currently stand handle a non-business day that falls on Mon-Fri, but both can be modified to do so. Since the calendar table requires only deleting roes I think that becomes the superior method if this is necessary.

How can we get the 15 minutes time interval

I am trying to fetch every 15min data in such a way that, if the current time is 23-10-19 11:11:30 then I need to get the data from 23-10-19 10:30:59 to 23-10-19 10:45:59 in the same way if the time is 23-10-19 11:15:30 then I need to get the data from 23-10-19 10:45:59 to 23-10-19 11:00:59.
I have tried forgetting the 15min delay but not the way I want to approach. Please suggest me how can we approach the scenario
select concat(to_char(current_timestamp - numtodsinterval(30,'MINUTE'),'yyyy-mm-dd hh24:mi'),':59') A,
concat(to_char(current_timestamp - numtodsinterval(15,'MINUTE'),'yyyy-mm-dd hh24:mi'),':59') B,
to_char(current_timestamp,'yyyy-mm-dd hh24:mi:ss') C from dual
below is the output that I was getting.
A B C
------------------- ------------------- -------------------
2019-10-23 13:03:59 2019-10-23 13:18:59 2019-10-23 13:33:22
You can truncate to the nearest minute to zero the seconds and then subtract the number of minutes to get back to the nearest 15 minute interval past the hour and then apply your offsets:
SELECT TRUNC( current_timestamp, 'MI' )
- MOD( EXTRACT( MINUTE FROM current_timestamp ), 15 ) * INTERVAL '1' MINUTE
- INTERVAL '30' MINUTE
+ INTERVAL '59' SECOND AS start_time,
TRUNC( current_timestamp, 'MI' )
- MOD( EXTRACT( MINUTE FROM current_timestamp ), 15 ) * INTERVAL '1' MINUTE
- INTERVAL '15' MINUTE
+ INTERVAL '59' SECOND AS end_time,
current_timestamp
FROM DUAL
Outputs:
START_TIME | END_TIME | CURRENT_TIMESTAMP
:------------------ | :------------------ | :----------------------------
2019-10-23 09:00:59 | 2019-10-23 09:15:59 | 2019-10-23 09:42:53.742684000
db<>fiddle here
Another solution is this one:
WITH t AS
(SELECT TRUNC(CURRENT_TIMESTAMP , 'hh') + TRUNC(EXTRACT(MINUTE FROM CURRENT_TIMESTAMP ) / 15) * INTERVAL '15' MINUTE - INTERVAL '30' MINUTE AS Base
FROM dual)
SELECT Base + INTERVAL '59' SECOND AS begin_time,
Base + INTERVAL '15:59' minute to SECOND AS end_time
FROM t;
It is based on my generic Interval function:
CREATE OR REPLACE FUNCTION MakeInterval(ts IN TIMESTAMP, roundInterval IN INTERVAL DAY TO SECOND) RETURN TIMESTAMP DETERMINISTIC IS
denom INTEGER;
BEGIN
IF roundInterval >= INTERVAL '1' HOUR THEN
denom := EXTRACT(HOUR FROM roundInterval);
IF MOD(24, denom) <> 0 THEN
RAISE VALUE_ERROR;
END IF;
RETURN TRUNC(ts) + TRUNC(EXTRACT(HOUR FROM ts) / denom) * denom * INTERVAL '1' HOUR;
ELSIF roundInterval >= INTERVAL '1' MINUTE THEN
denom := EXTRACT(MINUTE FROM roundInterval);
IF MOD(60, denom) <> 0 THEN
RAISE VALUE_ERROR;
END IF;
RETURN TRUNC(ts, 'hh') + TRUNC(EXTRACT(MINUTE FROM ts) / denom) * denom * INTERVAL '1' MINUTE;
ELSE
denom := EXTRACT(SECOND FROM roundInterval);
IF MOD(60, denom) <> 0 THEN
RAISE VALUE_ERROR;
END IF;
RETURN TRUNC(ts, 'mi') + TRUNC(EXTRACT(SECOND FROM ts) / denom) * denom * INTERVAL '1' SECOND;
END IF;
END MakeInterval;
You would invoke it as
SELECT MakeInterval(CURRENT_TIMESTAMP, INTERVAL '15' MINUTE) from dual;
plus/minus your constant offsets.
select :start_date + (1/96)*(level-1) from dual connect by level < :intervals

Rounding to Previous 2 hour Window Oracle SQL

I'm trying to round a date (date datatype which includes timestamp too) to the previous 2 hour block. e.g 13:23 -> 12:00, 18:12 -> 18:00
I had it working in MySQL using a MOD function as:
DATE_ADD(DATE(DATE_ADD(created_at, INTERVAL - 7 HOUR)), INTERVAL HOUR(DATE_ADD(created_at, INTERVAL - 6 HOUR)) - MOD(HOUR(DATE_ADD(created_at, INTERVAL - 6 HOUR)), 2) HOUR) AS Window_Start
**Complexity added as I'm also shifting the time a) to correct for a 7 hr time zone difference and b) because I need to offset the time by 1 hr before grouping it. But that's not where the issue is.
But I can't get it to work on an Oracle platform. Specifically, I can't seem to extract the hour of the time as a number which to feed into the MOD(). I've been trying with CAST, and TO_TIMESTAMP and TO_CHAR but nothing seems to work. The usual error is "inconsistent datatypes".
EXTRACT only works with with timestamp type, not date. And TO_TIMESTAMP only works on strings. But EXTRACT(TO_TIMESTAMP(TO_CHAR( doesn't work either.
I'm sure there's an easier way to do this...
In Oracle, one method is using date arithmetic. For example:
select date '2000-01-01' + floor((sysdate - date '2000-01-01') * (24 / 2)) / (24 / 2)
from dual;
The "24" is for hours in a day. The "2" is for the two hour period you want to truncate to.
I'm not sure that this is the answer but it'll give you a rabbit hole to chase:
select sysdate as curr_dt, to_date(to_char(sysdate - interval '2' hour, 'yyyy-mm-dd HH24')) as two_hours_ago from dual;
Hope it helps!
To get the hour in a number format this will work.
select TO_NUMBER(TO_CHAR(SYSDATE,'HH')) from dual
You can select each piece individually using the time stamp portion of this date format DD/MM/YYYY HH:MI:SS.
Some time ago I wrote this generic function:
CREATE FUNCTION MakeInterval(ts IN TIMESTAMP, roundInterval IN INTERVAL DAY TO SECOND) RETURN TIMESTAMP DETERMINISTIC IS
denom INTEGER;
BEGIN
IF roundInterval >= INTERVAL '1' HOUR THEN
denom := EXTRACT(HOUR FROM roundInterval);
IF MOD(24, denom) <> 0 THEN
RAISE VALUE_ERROR;
END IF;
RETURN TRUNC(ts) + TRUNC(EXTRACT(HOUR FROM ts) / denom) * denom * INTERVAL '1' HOUR;
ELSIF roundInterval >= INTERVAL '1' MINUTE THEN
denom := EXTRACT(MINUTE FROM roundInterval);
IF MOD(60, denom) <> 0 THEN
RAISE VALUE_ERROR;
END IF;
RETURN TRUNC(ts, 'hh') + TRUNC(EXTRACT(MINUTE FROM ts) / denom) * denom * INTERVAL '1' MINUTE;
ELSE
denom := EXTRACT(SECOND FROM roundInterval);
IF MOD(60, denom) <> 0 THEN
RAISE VALUE_ERROR;
END IF;
RETURN TRUNC(ts, 'mi') + TRUNC(EXTRACT(SECOND FROM ts) / denom) * denom * INTERVAL '1' SECOND;
END IF;
END MakeInterval;
In your case the usage would be
SELECT MakeInterval(created_at, INTERVAL '2' HOUR)
FROM ...
But perhaps this would be an overkill in your situation, usage without the function would be:
SELECT TRUNC(created_at) + TRUNC(EXTRACT(HOUR FROM created_at) / 2) * 2 * INTERVAL '1' HOUR
FROM ...

time interval in function not working

I have a function:
CREATE OR REPLACE FUNCTION a(b integer)
RETURNS integer AS
$BODY$
declare
c integer;
timevalue text;
begin
timevalue = b::text || ' days'; -- build string like '7 days'
select sum(value)
into c
from tablx
where createdate between current_date - interval timevalue and current_date;
return c;
end;
$BODY$
LANGUAGE plpgsql VOLATILE
function is simple... give summary of records which fits the date criteria.
for some reason it does not accept current_date - interval timevalue
what can I do?
Unfortunately you can't specify a "dynamic" interval value. But as you always use the same unit, you can use:
select sum(value)
into c
from tablx
where createdate between current_date - interval '1' day * b and current_date;
You can make that simpler, because you can subtract b directly from current_date
select sum(value)
into c
from tablx
where createdate between current_date - b and current_date;
In an expression date - integer the integer value is the number of days.
For more details see the manual: http://www.postgresql.org/docs/current/static/functions-datetime.html

creating timestamp from columns in postgres

i have 2 rows in a column, one feeding me the julian date (number of days since Jan 1, 1970, with that day being 1), and the second column is the number of minutes past midnight of the current day (why it was done this way, i have no idea).
i would like to get my sql query to create a timestamp out of these two columns.
if i had access to the data ahead of time, i could do something like SELECT timestamp '1970-01-01 00:00:00' + INTERVAL 'X DAYS' + INTERVAL 'Y MINUTES' AS my_time FROM mytable, but since X and Y are actual members of the table i'm querying, it's not that simple.
anyone have any suggestions?
essentially, i'm looking for the equivalent [legal] solution:
SELECT timestamp '1970-01-01 00:00:00' + INTERVAL 'my_col1 DAYS' + INTERVAL 'my_col2 MINUTES' AS my_time FROm mytable
i can get my server-side code to do it for me, but i would love it if i could build it into my query.
You can construct the interval strings and then cast them to the interval type:
SELECT
timestamp '1970-01-01 00:00:00' +
cast(my_col1 || ' DAYS' AS interval) +
cast(my_col2 || ' MINUTES' AS interval) my_time FROM mytable
Without resorting to string substitution or concatenation:
SELECT
timestamp '1970-01-01 00:00:00' +
my_col1 * interval '1 DAY' +
my_col2 * interval '1 MINUTE' FROM mytable