finding working days in sql - oracle

I have a table 'country_holiday' which has two columns country_id and holiday_dt , this table doesnt have entries for weekends.
I need to write a procedure that takes 3 inputs start_dt , end_dt and country_id and then iterate over all working dates between the two given dates for given country_id
I tried writing something like this which doesnt work ( i get a blank cursor)
create or replace procedure check_data(p_start_date in date, p_end_date in date, p_country_id in number)
IS
curr_date date;
CURSOR v_buss_days is select p_start_date + rownum -1
from all_objects where rownum <= p_end_date - p_start_date +1
and to_char(p_start_date+rownum-1,'DY') not in ('SAT','SUN')
and p_start_date + rownum -1 not in (select holiday_dt from country_holiday where country_id = p_country_id)
BEGIN
for curr_date in v_buss_days
LOOP
dbms_output.put_line(curr_date)
END LOOP;
END
I tried running the query
select p_start_date + rownum -1
from all_objects where rownum <= p_end_date - p_start_date +1
and to_char(p_start_date+rownum-1,'DY') not in ('SAT','SUN')
this gives me 0 rows with p_start_date='01 dec 2013' and p_end_date='31 dec 2013' , seems like my query to populate cursor is incorrect.
After populating the cursor correctly i face issue
thanks for your help , indeed it works .... but facing issue when i try to use in procedure ....
create or replace procedure check_data(p_start_date in date, p_end_date in date, p_cntry_id in number)
IS
curr_num_of_empoyee number;
curr_date date;
CURSOR v_buss_days is select work_date from
( with dates as
( select p_start_date dt_start, p_end_date dt_end from dual )
select dt_start + (level-1) work_date from dates
connect by level <= (dt_end - dt_start + 1 )
) wdates
where work_date not in ( select HOLIDAY_DATE
from country_holiday
where country_id = p_cntry_id)
and to_char(work_date,'DY') not in ('SAT','SUN')
order by work_date;
BEGIN
for curr_date in v_buss_days
LOOP
select count(*) into curr_num_of_empoyee from employee_details where country_id = p_cntry_id and data_dt = curr_date;
END LOOP;
END;
Error is
19/101 PLS-00382: expression is of wrong type
seems like issue is in part "data_dt = curr_date"

Here is a query I put on SQLFiddle. Remove the WITH clause and replace with your procedure date parameters. You can use the combination of CONNECT BY and LEVEL to generate a set of rows with increasing numeric values. Then, add that to your start date, and filter out from your holiday table and weekends.
select work_date from
(
with dates as
( select to_date('01/01/2014','MM/DD/YYYY') dt_start,
to_date('01/10/2014','MM/DD/YYYY') dt_end
from dual
)
select dt_start + (level-1) work_date
from dates
connect by level <= (dt_end - dt_start + 1 )
) wdates
where work_date not in ( select holiday_dt
from country_holiday
where country_id = 1)
and to_char(work_date,'DY') not in ('SAT','SUN')
order by work_date

Related

Oracle PLSQL escaping a single quote

My apologies for the verbose post but the setup is necessary to show my problem and ask a question.
In the anonymous block below I'm trying to construct a string, which encapsulates the table in a single quote ie 'T1' but I've been struggling for the past hour and can use some help.
Secondly, I purposely left out a row in the table partition_rention for table name T2. I suspect a NULL will be returned into the variable when the statement is executed. Will this work?
if v_days is NULL
then
v_days := 30
END IF;
Thanks in advance to all who answer and your expertise
create table partition_rention
(
TABLE_NAME VARCHAR2(30) NOT NULL,
DAYS NUMBER(6),
CONSTRAINT Check_gt0
CHECK (DAYS> 0)
);
/
INSERT into partition_rention (TABLE_NAME, DAYS)
VALUES
('T1', 15);
/
INSERT into partition_rention (TABLE_NAME, DAYS)
VALUES
('T3', 15);
/
CREATE TABLE t1 (
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
dt DATE
)
PARTITION BY RANGE (dt)
INTERVAL (NUMTODSINTERVAL(7,'DAY'))
(
PARTITION OLD_DATA values LESS THAN (TO_DATE('2022-01-01','YYYY-MM-DD'))
);
/
INSERT /*+ APPEND */ into t1 (dt)
with dt (dt, interv) as (
select date '2022-01-01', numtodsinterval(30,'MINUTE') from dual
union all
select dt.dt + interv, interv from dt
where dt.dt + interv < date '2022-01-15')
select dt from dt;
/
create index ix_local on t1 (dt) local;
/
CREATE TABLE t2
(
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
dt DATE
)
PARTITION BY RANGE (dt)
INTERVAL (NUMTODSINTERVAL(1,'DAY'))
(
PARTITION OLD_DATA values LESS THAN (TO_DATE('2022-01-01','YYYY-MM-DD'))
);
/
INSERT /*+ APPEND */ into t2 (dt)
with dt (dt, interv) as (
select date '2022-01-01', numtodsinterval(30,'MINUTE') from dual
union all
select dt.dt + interv, interv from dt
where dt.dt + interv < date '2022-01-15')
select dt from dt;
/
create index ix_global on t2 (dt);
/
CREATE TABLE t3 (
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
dt TIMESTAMP)
PARTITION BY RANGE (dt)
INTERVAL ( NUMTODSINTERVAL (1, 'DAY') ) (
PARTITION OLD_DATA VALUES LESS THAN (TIMESTAMP '2022-01-01 00:00:00.000000')
);
/
INSERT /*+ APPEND */ into t3 (dt)
SELECT TIMESTAMP '2022-01-01 00:00:00'
+ (LEVEL - 1) * INTERVAL '5' MINUTE
+ MOD(LEVEL - 1, 10) * INTERVAL '0.1' SECOND
FROM DUAL
CONNECT BY
TIMESTAMP '2022-01-01 00:00:00'
+ (LEVEL - 1) * INTERVAL '5' MINUTE
+ MOD(LEVEL - 1, 10) * INTERVAL '0.1' SECOND < DATE '2022-01-15';
/
DECLARE
v_str VARCHAR2 (500);
v_days NUMBER := 0;
BEGIN
FOR cur_r IN(
SELECT TABLE_NAME, PARTITIONING_TYPE, COLUMN_NAME, DATA_TYPE
FROM USER_PART_TABLES
JOIN USER_PART_KEY_COLUMNS ON NAME = TABLE_NAME
JOIN USER_TAB_COLS USING (TABLE_NAME, COLUMN_NAME)
where OBJECT_TYPE = 'TABLE' AND
PARTITIONING_TYPE='RANGE' AND
regexp_like(DATA_TYPE,'^DATE$|^TIMESTAMP*')
)
LOOP
--DBMS_OUTPUT.put_line('Table '|| cur_r.table_name);
v_str := 'select DAYS FROM partition_rention into v_days where TABLE_NAME = '||cur_r.table_name||'';
DBMS_OUTPUT.put_line(v_str);
-- execute immediate v_str;
END LOOP;
END;
Statement processed.
select DAYS FROM partition_rention into v_days where TABLE_NAME = T1
select DAYS FROM partition_rention into v_days where TABLE_NAME = T2
select DAYS FROM partition_rention into v_days where TABLE_NAME = T3
There is no reason for dynamic SQL. It would be this:
begin
select DAYS
into v_days
FROM partition_rention
where TABLE_NAME = cur_r.table_name;
exception
when NO_DATA_FOUND THEN
v_days := 30;
end;
If you really insist for dynamic SQL then it would be this one
begin
v_str := 'select DAYS FROM partition_rention where TABLE_NAME = :t';
execute immediate v_str into v_days using cur_r.table_name;
exception
when NO_DATA_FOUND THEN
v_days := 30;
end;
NB, I guess the next step might be to drop outdated partitions. For this have a look at How to drop multiple interval partitions based on date?
If the LOOP statement comes between a BEGIN statement and its matching EXCEPTION or END; then the END LOOP statement has to come between them as well. If i want an EXCEPTION handler that catches errors that may occur within the loop, and then continues the loop then the exception handler doesn't appear to work.
The code was restructured to remove the expectation handler.
I already have a query that finds the tables and columns I'm interested in. Now, for each table in that result set, I want to get the matching days value from the partition_retention table if there is one, and if there is no matching row in partition_retention, I want the table_name anyway with a default value (30) for days. To do this I implemented an outer JOIN like this:
BEGIN
FOR td IN
(
SELECT table_name
, NVL (pr.days, 30) AS days
FROM user_part_tables pt
JOIN user_part_key_columns pkc ON pkc.name = pt.table_name
JOIN user_tab_cols tc USING (table_name, column_name)
LEFT JOIN partition_retention pr USING (table_name)
WHERE pkc.object_type = 'TABLE'
AND pt.partitioning_type = 'RANGE'
AND REGEXP_LIKE (tc.data_type, '^DATE$|^TIMESTAMP*')
ORDER BY table_name -- not needed, but could be handy in debugging
)
LOOP
-- For debugging:
dbms_output.put_line ( td.table_name
|| ' = table_name, '
|| TO_CHAR (td.days)
|| ' = days'
);
-- call procedure to remove old PARTITIONs here.
END LOOP;
END;
/
Output from my sample data:
T1 = table_name, 15 = days
T2 = table_name, 30 = days
T3 = table_name, 5 = days

PL SQL iterate loop through date range

I have a date range, I am trying to take one date every week through a loop
DECLARE
start_date DATE := TO_DATE('06.01.2021', 'dd.MM.yyyy');
end_date DATE := TO_DATE('26.05.2021', 'dd.mm.yyyy');
active_date DATE;
start_number NUMBER;
end_number NUMBER;
BEGIN
start_number := TO_NUMBER(TO_CHAR(start_date, 'j'));
end_number := TO_NUMBER(TO_CHAR(end_date, 'j'));
active_date := start_date;
FOR cur_r IN start_number..end_number
LOOP
INSERT INTO test_tbl
SELECT snap_date FROM s_act
WHERE
snap_date = active_date;
active_date := TRUNC(active_date) + 7;
COMMIT;
END LOOP;
END;
When I execute this script, only one date 06.01.2021 is written to the table through all iterations.
Where am I making a mistake? How can this be fixed?
You do not need PL/SQL for this and can just use a recursive sub-query:
INSERT INTO test_tbl
WITH date_range ( start_date, end_date ) AS (
SELECT DATE '2021-01-06', DATE '2021-05-26' FROM DUAL
UNION ALL
SELECT start_date + INTERVAL '7' DAY,
end_date
FROM date_range
WHERE start_date + INTERVAL '7' DAY <= end_date
)
SELECT snap_date
FROM s_act s
WHERE EXISTS(
SELECT 1
FROM date_range r
WHERE r.start_date = s.snap_date
);
or a hierarchical query:
INSERT INTO test_tbl
SELECT snap_date
FROM s_act s
WHERE EXISTS(
WITH date_range ( start_date, end_date ) AS (
SELECT DATE '2021-01-06', DATE '2021-05-26' FROM DUAL
)
SELECT 1
FROM date_range r
WHERE r.start_date + ( LEVEL - 1 ) * INTERVAL '7' DAY = s.snap_date
CONNECT BY r.start_date + ( LEVEL - 1 ) * INTERVAL '7' DAY <= r.end_date
);
If you really want to use PL/SQL then you can make it much simpler and iterate by weeks rather than days (however, this will be much less efficient as you will have one INSERT per week and the associated context switch from PL/SQL to SQL compared to the SQL solution which is only a single INSERT for the entire operation and no context switches):
DECLARE
start_date DATE := DATE '2021-01-06';
end_date DATE := DATE '2021-05-26';
active_date DATE := start_date;
BEGIN
LOOP
EXIT WHEN active_date > end_date;
INSERT INTO test_tbl
SELECT snap_date FROM s_act
WHERE snap_date = active_date;
active_date := active_date + INTERVAL '7' DAY;
END LOOP;
END;
/
db<>fiddle here
To me, it looks as if everything is, actually, OK with code you wrote, because active_date gets its new value:
SQL> set serveroutput on;
SQL> declare
2 start_date date := to_date('06.01.2021', 'dd.MM.yyyy');
3 end_date date := to_date('26.05.2021', 'dd.mm.yyyy');
4 active_date date;
5 start_number number;
6 end_number number;
7 begin
8 start_number := to_number(to_char(start_date, 'j'));
9 end_number := to_number(to_char(end_date, 'j'));
10 active_date := start_date;
11
12 for cur_r in start_number..end_number
13 loop
14 dbms_output.put_line('Active_date = ' || to_char(active_date, 'dd.mm.yyyy'));
15 /* Commented, as I don't have your tables nor data
16 INSERT INTO test_tbl
17 SELECT snap_date
18 FROM s_act
19 WHERE snap_date = active_date;
20 */
21 active_date := trunc(active_date) + 7;
22 end loop;
23 -- move COMMIT out of the loop!
24 commit;
25 end;
26 /
Active_date = 06.01.2021
Active_date = 13.01.2021
Active_date = 20.01.2021
<snip>
Active_date = 06.09.2023
Active_date = 13.09.2023
PL/SQL procedure successfully completed.
SQL>
You said
When I execute this script, only one date 06.01.2021 is written to the table through all iterations.
This is piece of code responsible for that:
INSERT INTO test_tbl
SELECT snap_date
FROM s_act
WHERE snap_date = active_date;
I interpret it as:
s_act table contains rows only with snap_date equal to 06.01.2021, or
if it contains rows with other dates, maybe they contain a time component (hours, minutes, seconds) and where condition prevents them to be inserted. If that's so, try with
where trunc(snap_date) = active_date
and see what happens.

How to find record which throws an error in a SELECT (Oracle)

I have a problematic SQL Sentence, when it runs
SELECT code, my_date, my_time
FROM my_table
WHERE to_date(to_char(my_date, 'YYYY-MM-DD') || ' ' || my_time, 'YYYY-MM-DD HH24:MI:SS') > sysdate - 5
I always get: ORA-01841: (full) year must be between -4713 and +9999, and not be 0
This is the definition of my table:
CREATE TABLE my_table (code NUMBER(10), my_date DATE, my_time VARCHAR2(8));
ALTER TABLE my_table ADD CONSTRAINT pk_my_table PRIMARY KEY (code);
CREATE INDEX i_my_table_001 ON my_table (my_date);
But If I add an extra restriction, I never get the error:
SELECT code, my_date, my_time
FROM my_table
WHERE my_date > trunc(sysdate - 5)
AND to_date(to_char(my_date, 'YYYY-MM-DD') || ' ' || my_time, 'YYYY-MM-DD HH24:MI:SS') > sysdate - 5
All validations are ok. Not invalid dates, not invalid times
Is there any way to find which is the 'offending' record?
Brute force is not an option because table has more than 25M records.
If you are on Oracle 12.2 or higher, you can use the VALIDATE_CONVERSION function to see which rows could be giving you an error.
Example Query
WITH
my_table (code, my_date, my_time)
AS
(SELECT 1, date '2020-01-01', '08:00:00' FROM DUAL
UNION ALL
SELECT 2, date '2020-01-31', '25:00:00' FROM DUAL)
SELECT *
FROM my_table
WHERE validate_conversion (to_char(my_date, 'YYYY-MM-DD') || ' ' || my_time AS DATE, 'YYYY-MM-DD HH24:MI:SS') = 0;
Result
CODE MY_DATE MY_TIME
_______ ______________ ___________
2 2020-01-31 25:00:00
Update
One update that may be a bit easier than cloning your database and upgrading is to create your own function to validate the date and call that from a test SQL statement like the on below. I'm not sure if defining a function within a common table expression is supported in 12.1, but if it is not, you can make it a standalone function and call that from your query.
WITH
FUNCTION validate_date (p_date DATE, p_time VARCHAR2)
RETURN NUMBER
AS
l_test_date DATE;
BEGIN
l_test_date :=
TO_DATE (TO_CHAR (p_date, 'YYYY-MM-DD') || ' ' || p_time, 'YYYY-MM-DD HH24:MI:SS');
RETURN 1;
EXCEPTION
WHEN OTHERS
THEN
RETURN 0;
END;
SELECT *
FROM (SELECT 1 AS code, DATE '2020-01-01' AS my_date, '08:00:00' AS my_time FROM DUAL
UNION ALL
SELECT 2, DATE '2020-01-31', '25:00:00' FROM DUAL) my_table
WHERE validate_date (my_date, my_time) = 0;

PL SQL 'IF or CASE' using variable date

I am stuck with this and could use advice/help:
Basically, trying to set the date as a variable and then run select statements, using that date variable in the 'WHERE' section of the query. Not sure if I should be using IF or CASE, or neither? If its monday, i want to run 1 set of dates (prev thur and fri) any other day (just sysdate-2 and sysdate-1) Any help is much appreciated!
Code is below:
DECLARE
today_date number;
start_date date;
end_date date;
BEGIN
today_date := to_char(sysdate, 'D');
start_date := case when today_date ='2' then 'sysdate-4' else 'sysdate-2'
end;
end_date := case when today_date ='2' then 'sysdate-3' else 'sysdate-1' end;
SELECT COLUMN A, COLUMN B, COLUMN C, COLUMN D
FROM /*csv*/REPORT_NAME
WHERE COLUMN B between trunc(start_date)+21/24 and trunc(end_date)+21/24 and
BOOK_NAME = 'xxxxxx' and SERVER = 'xxxxxx' and EX_ACTION = 'xxxxx';
end;
You're mixing variables/functions and strings. This should work.
DECLARE
today_date number;
start_date date;
end_date date;
BEGIN
today_date := to_char(sysdate, 'D');
start_date := case when today_date ='2' then sysdate-4 else sysdate-2
end;
end_date := case when today_date ='2' then sysdate-3 else sysdate-1 end;
/* this won't work without declaring a cursor, and returning it to the client
SELECT COLUMN A, COLUMN B, COLUMN C, COLUMN D
FROM REPORT_NAME
WHERE COLUMN B between trunc(start_date)+21/24 and trunc(end_date)+21/24 and
BOOK_NAME = 'xxxxxx' and SERVER = 'xxxxxx' and EX_ACTION = 'xxxxx';
*/
end;
Note you also have some implicit type conversion happening. today_date should probably be char(1) instead.
You don't really need variables for this, and you don't need PL/SQL; you can calculate the dates as part of the where clause using case expressions in that instead:
SELECT /*csv*/ COLUMN_A, COLUMN_B, COLUMN_C, COLUMN_D
FROM REPORT_NAME
WHERE COLUMN_B >= trunc(sysdate) - case to_char(sysdate, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH')
when 'Mon' then 4 else 2 end + 21/24
AND COLUMN_B < trunc(sysdate) - case to_char(sysdate, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH')
when 'Mon' then 3 else 1 end + 21/24
AND BOOK_NAME = 'xxxxxx'
AND SERVER = 'xxxxxx'
AND EX_ACTION = 'xxxxx';
I've taken the liberty of changing the logic from being based on the day number being 2 to it having a specific day abbreviation, because the value D returns is based on your NLS settings so it could vary (and give unexpected results) for someone else running your code. As day names and abbreviations are also NLS-dependent I've specified the language to use. (You can't specify how D is used in the same way, unfortunately).
I've changed the time window slightly, so it goes from 9pm on one day to up to but not including 9pm the next day. If you use between then it's inclusive at both ends, so runs on consecutive days you both pick up any data at exactly 21:00:00 from the overlapping day. That probably isn't what you want (but if it really is, just change < to <=, or revert to between if you prefer...).
If you want a PL/SQL wrapper you can do the same thing, but you either have to select the results into something like a collection, or use a ref cursor to return the result set to the caller. It isn't clear if you actually need or want to do that though.
have you tried selecting into your variables?
DECLARE
TODAY_DATE NUMBER;
START_DATE DATE;
END_DATE DATE;
BEGIN
TODAY_DATE := TO_CHAR (SYSDATE, 'D');
SELECT CASE WHEN TODAY_DATE = '2' THEN SYSDATE - 4 ELSE SYSDATE - 2 END,
CASE WHEN TODAY_DATE = '2' THEN SYSDATE - 3 ELSE SYSDATE - 1 END
INTO START_DATE, END_DATE
FROM DUAL;
SELECT COLUMN_A, COLUMN_B, COLUMN_C, COLUMN_D
FROM REPORT_NAME /*csv*/
WHERE COLUMN_B BETWEEN TRUNC (START_DATE) + 21 / 24
AND TRUNC (END_DATE) + 21 / 24
AND BOOK_NAME = 'xxxxxx'
AND SERVER = 'xxxxxx'
AND EX_ACTION = 'xxxxx';
END;

Arithmetic operations on Date values in Oracle

I am struggling with some case...
there is table, in which I have employee attendance records, for example:
for empID=1;
empID time Type date
-------------------------------
1 9:22 in sameday
1 11:23 out sameday
1 14:35 in sameday
1 16:21 out sameday
particularly, I want some fn/procedure that will take an EmpID and DATE parameters, and then based on this data if I'll write: select proc(EmployeeID, Date) from dual(or maybe some other table?) it should do such a work:
take first couples in table (table is ordered be ascending as default order), then calculate FROM first OUT (11:23) to first IN(9:22) time, save that time somewhere (int tempResult) and then calculate second couple, and calculate second tempResult and in finalResult, it should count the total time, like finalResult+=finalResult+tempResult (if it has been an iterations in loop);
I think it would be done someway like, in foreach (or whatever it is in pl/sql oracle) take first select with top result, then, second.. and so forth... and on each iteration calculate desire goal.
so.. logics is ok with me I think :), but the problem is that I'm not that familiar with PL/SQL, if it had been written in Java it should have come easy to me.
I will pay lots of Thanks to some one who will help me...
its crucial for me to day.
thanks in advance.
I have Date and Time is separate columns, like:
date time
----------------------
11-09-2013 12:34
so, I made little change like this
FOR rec IN
( SELECT t.EID, to_char(t.devent_date, 'DD.MM.YY') ||' '|| t.RegTime, t.acttype from turnicate_ie t WHERE t.EID = i_emp_id ORDER BY t.EID
)
LOOP
but it states that package or function is in incorrect state...
(t.devent_date is 11.09.2013, t.RegTime is 16:23)
The below will give you some idea of using plsql:
you need to use many logic of calculating total working hours, like multiple inputs within same time, multiple empId etc.
create table my_test ( empId number, log_time date, type varchar2(3));
INSERT INTO my_test VALUES( 1, to_date('11/sep/2013 09:22:00 am', 'dd/mon/yyyy hh:mi:ss am'), 'in');
INSERT INTO my_test VALUES( 1, to_date('11/sep/2013 11:23:00 am', 'dd/mon/yyyy hh:mi:ss am'), 'out');
INSERT INTO my_test VALUES( 1, to_date('11/sep/2013 02:35:00 pm', 'dd/mon/yyyy hh:mi:ss pm'), 'in');
INSERT INTO my_test VALUES( 1, to_date('11/sep/2013 04:21:00 pm', 'dd/mon/yyyy hh:mi:ss pm'), 'out');
CREATE OR REPLACE
FUNCTION total_hours(
i_emp_id IN NUMBER)
RETURN VARCHAR2
IS
l_total_seconds NUMBER := 0;
in_time DATE;
l_total_time VARCHAR2(20);
BEGIN
FOR rec IN
( SELECT log_time, type FROM my_test WHERE empid = i_emp_id ORDER BY log_time
)
LOOP
IF rec.TYPE = 'in' AND in_time IS NULL THEN
in_time := rec.log_time;
END IF;
IF rec.TYPE = 'out' AND in_time IS NOT NULL THEN
l_total_seconds := (rec.log_time - in_time)*24*60*60 + l_total_seconds;
in_time := NULL;
END IF;
END LOOP;
SELECT TO_CHAR(TRUNC(l_total_seconds/3600), 'FM999999990')
|| 'hh '
|| TO_CHAR(TRUNC(mod(l_total_seconds,3600)/60), 'FM00')
|| 'mm '
|| TO_CHAR(mod(l_total_seconds,60), 'FM00')
||'ss'
INTO l_total_time
FROM dual;
RETURN l_total_time;
END;
/
SELECT total_hours(1) from dual;

Resources