Looping through a cursor with a condition on an updated field [PLSQL] - loops

I am currently implementing a PL/SQL procedure, which balances a value in two lists.
Consider this example as background:
Rec_1 | 2
Rec_2 | 1
Rec_3 | 2
Rec_A | -1
Rec_B | -3
Rec_C | -2
I want to loop through all these values one-by-one and settle as much as possible, i.e. that after the first settlement Rec_1 should be 1, Rec_A should be 0. Afterwards, Rec_1 will be settled with Rec_B such that it gets 1, Rec_B gets -2, and so on.
I want to use two cursors to do this, and update the values in its own procedure (or function, if that is necessary), since there is a little more to do than just update this value.
Now, here is my challenge:
How do I know which cursor to fetch after a settlement has happened?
Right now, my code for this function looks like this:
PROCEDURE SettleLists (
ListNegV SYS_REFCURSOR,
ListPosV SYS_REFCURSOR
) IS
currentNegV TABLENAME%ROWTYPE;
currentPosV TABLENAME%ROWTYPE;
BEGIN
FETCH ListNegV INTO currentNegV;
FETCH ListPosV INTO currentPosV;
LOOP
EXIT WHEN ListNegV%NOTFOUND;
EXIT WHEN ListPosV%NOTFOUND;
IF (currentNegV.NUMERICVALUE < 0)
THEN
IF (currentPosV.NUMERICVALUE > 0)
THEN
Settle(currentPosV, currentNegV);
ELSE
FETCH ListPosV INTO currentPosV;
END IF;
ELSE
FETCH ListNegV INTO currentNegV;
END IF;
END LOOP;
END;
Inside the settle procedure, there will be an UPDATE on both records. Since the variables and cursor values are not updated, this will produce an infinite loop. I could update the parameter of settle when the record is updated in the database as well, but since I am not used to cursors, you might have a better idea.
I could consider the cursor to be strongly typed, if that makes any difference. If there is a better way than using a cursor, feel free to suggest it.
Finally, I was playing around with SELECT FOR UPDATE and UPDATE WHERE CURRENT OF, but it did not seem to work when passing the cursor to a procedure in between. If anyone has some idea on this, I would also appreciate your help.

Here is a what I would do. I will surely add some comments.
This is running fine in Oracle 11Gr2 and I am hoping it will run fine even in 7i :).
declare
cursor c1 is
select 'REC_1' HEader,2 Val from dual
union
select 'REC_2' HEader,1 Val from dual
union
select 'REC_3' HEader,2 Val from dual;
cursor c2 is
select 'REC_A' HEader,-1 Val from dual
union
select 'REC_B' HEader,-3 Val from dual
union
select 'REC_C' HEader,-2 Val from dual;
num_bal1 number;
num_bal2 number;
num_settle_amt number;
rec_type_c1 c1%rowtype;
rec_type_c2 c2%rowtype;
begin
Open c1;
open c2;
fetch c1 into rec_type_c1;
fetch c2 into rec_type_c2;
num_bal1 := nvl(rec_type_c1.val,0);
num_bal2 := rec_type_c2.val;
Loop
exit when c1%notfound or c2%notfound;
Loop
dbms_output.put_line('Processing ' || rec_type_c1.header || ' with ' || num_bal1);
--In your example there are only +ve for 1 and -ve for 2. But if that is not correct, check for signs and next 3 statements
num_settle_amt := least(abs(num_bal1), abs(num_bal2) );
num_bal1 := num_bal1 - num_settle_amt;
num_bal2 := num_bal2 + num_settle_amt;
dbms_output.put_line('Setteled ' || num_settle_amt || ' of ' || rec_type_c1.header || ' with ' || rec_type_c2.header );
if num_bal1 = 0 then
--Update in the table. It will not impact variable.
fetch c1 into rec_type_c1;
num_bal1 := nvl(rec_type_c1.val,0);
end if;
if num_bal2 = 0 then
--Update in the table. It will not impact variable.
fetch c2 into rec_type_c2;
num_bal2 := nvl(rec_type_c2.val,0);
end if;
End loop;
end loop;
close c1;
close c2;
end;

Related

Working with nested loops in pl/sql but not displaying the proper output

SET SERVEROUTPUT ON SIZE 4000;
DECLARE
call_id COURSE.CALL_ID%type;
sec_num COURSE_SECTION.SEC_NUM%type;
fname STUDENT.S_FIRST%TYPE ;
lname STUDENT.S_LAST%TYPE;
CURSOR c_info is
SELECT CALL_ID , SEC_NUM
FROM COURSE_SECTION ,COURSE,TERM
WHERE COURSE_SECTION.COURSE_ID = COURSE.COURSE_ID
AND TERM.TERM_ID = COURSE_SECTION.TERM_ID
AND TERM.TERM_DESC = 'Summer 2007' ;
CURSOR S_NAME IS
SELECT DISTINCT S_FIRST, S_LAST
FROM STUDENT,COURSE_SECTION,TERM,ENROLLMENT
WHERE TERM.TERM_ID = COURSE_SECTION.TERM_ID
AND COURSE_SECTION.C_SEC_ID = ENROLLMENT.C_SEC_ID
AND COURSE_SECTION.TERM_ID=TERM.TERM_ID
AND ENROLLMENT.S_ID = STUDENT.S_ID
AND TERM.TERM_DESC LIKE 'Summer 2007';
BEGIN
OPEN c_info;
LOOP
FETCH c_info INTO call_id , sec_num ;
EXIT WHEN c_info%notfound;
DBMS_OUTPUT.PUT_LINE('==================================');
DBMS_OUTPUT.PUT_LINE(call_id || ' ' || 'Sec. ' || sec_num);
DBMS_OUTPUT.PUT_LINE('==================================');
OPEN S_NAME;
LOOP
FETCH S_NAME INTO fname , lname ;
EXIT WHEN S_NAME%notfound;
DBMS_OUTPUT.PUT_LINE(fname || ' ' || lname );
END LOOP;
CLOSE S_NAME ;
END LOOP;
CLOSE c_info;
END;
-- The output expected
-- I have having some issues, I am unable to display the proper output. I am trying to use a nested loop but i made some mistake when implementing it. Plus i think an explicit cursor is much better to be used.
Make use of the Northwood university database.
https://drive.google.com/file/d/1M_g7FbgOUahoFtE943OK28UxIFbUFgRk/view?usp=sharing
The script
I'm making a lot of assumptions here - I'm guessing you are getting all students for all courses in your inner loop, but you really just want get students for the particular course section you are dealing with in your outer loop.
So your second query will need to reference the right course section ID to limit the students to just that section.
You don't need to explicitly define cursors unless you need them for some reason - if you just iterating through them, its better to reference them directly in the FOR loop.
So that brings me to the following
set serveroutput on size 4000;
begin
for c_info in (
select call_id,
sec_num,
SEC_ID -- PK to link to enrollment later
from course_section,
course,
term
where course_section.course_id = course.course_id
and term.term_id = course_section.term_id
and term.term_desc = 'Summer 2007' ;
)
loop
dbms_output.put_line('==================================');
dbms_output.put_line(c_info.call_id || ' ' || 'Sec. ' || c_info.sec_num);
dbms_output.put_line('==================================');
for s_name in (
select distinct s_first, s_last
from student,
course_section,
term,
enrollment
where term.term_id = course_section.term_id
and course_section.c_sec_id = enrollment.c_sec_id
and course_section.term_id=term.term_id
and enrollment.s_id = student.s_id
and term.term_desc like 'Summer 2007'
AND ENROLLMENT.C_SEC_ID = C_INFO.SEC_ID -- get students just for THIS course section
)
loop
dbms_output.put_line(s_name.s_first || ' ' || s_name.s_last );
end loop;
end loop;
end;
where I've put query alterations in CAPS.
Since I cut/pasted your SQL there are no aliases in the first query - I'd recommend you correct that, as aliasing columns is always good practice.
Simiarly, I retained the DISTINCT in the second query, but I'd suspect its redundant, because I imagine a student wont enroll more than once for a single course section. (And in reality, if you had two different students named Sue Smith, you would probably want to print them out twice, no?)

How can print out the current time with DBMS_OUTPUT.PUT_LINE?

I use the following Code to execute an export data pump.
set serveroutput on;
DECLARE
ind NUMBER; -- Loop index
h1 NUMBER; -- Data Pump job handle
percent_done NUMBER; -- Percentage of job complete
job_state VARCHAR2(30); -- To keep track of job state
le ku$_LogEntry; -- For WIP and error messages
js ku$_JobStatus; -- The job status from get_status
jd ku$_JobDesc; -- The job description from get_status
sts ku$_Status; -- The status object returned by get_status
>>>>>>>>>>>>>>v_systimestamp TIMESTAMP := SYSTIMESTAMP;<<<<<<<<<<<<<<
BEGIN
h1 := DBMS_DATAPUMP.OPEN('EXPORT','SCHEMA',NULL,'EXAMPLE3','LATEST');
DBMS_DATAPUMP.ADD_FILE(h1, 'dumpfile.dmp', 'EXPORT_DIRECTORY', NULL, DBMS_DATAPUMP.KU$_FILE_TYPE_DUMP_FILE, 1);
DBMS_DATAPUMP.METADATA_FILTER(h1,'SCHEMA_EXPR','IN (''SchemaName'')');
DBMS_DATAPUMP.START_JOB(h1);
percent_done := 0;
job_state := 'UNDEFINED';
while (job_state != 'COMPLETED') and (job_state != 'STOPPED') loop
DBMS_OUTPUT.PUT_LINE(v_systimestamp);
dbms_datapump.get_status(h1,
dbms_datapump.ku$_status_job_error +
dbms_datapump.ku$_status_job_status +
dbms_datapump.ku$_status_wip,-1,job_state,sts);
js := sts.job_status;
-- If the percentage done changed, display the new value.
if js.percent_done != percent_done
then
dbms_output.put_line('*** Job percent done = ' ||
to_char(js.percent_done));
percent_done := js.percent_done;
end if;
-- If any work-in-progress (WIP) or error messages were received for the job,
-- display them.
if (bitand(sts.mask,dbms_datapump.ku$_status_wip) != 0)
then
le := sts.wip;
else
if (bitand(sts.mask,dbms_datapump.ku$_status_job_error) != 0)
then
le := sts.error;
else
le := null;
end if;
end if;
if le is not null
then
ind := le.FIRST;
while ind is not null loop
>>>>>>>>>>>>>>DBMS_OUTPUT.PUT_LINE(v_systimestamp);<<<<<<<<<<<<<<
dbms_output.put_line(le(ind).LogText);
ind := le.NEXT(ind);
end loop;
end if;
end loop;
-- Indicate that the job finished and detach from it.
dbms_output.put_line('Job has completed');
dbms_output.put_line('Final job state = ' || job_state);
dbms_datapump.detach(h1);
END;
The problem is that the export takes too long. It takes 25 minutes with this SQL Code. The Size of the schema is 1.8 GB.
And I would like to find out how much time individual steps take. That's why I want to insert a timestamp in after each process step. Then I can see how long individual steps need.
I have marked the code for the timestamp with (>>>> <<<<) in the code.
The timestamp is not updating the time. I need the CURRENT time after each process. Can you help me?
In your code, you are setting the value of v_systimestamp at the beginning of your script - this will not change throughout the script run. You can reset the value before you log like:
v_systimestamp TIMESTAMP := SYSTIMESTAMP
DBMS_OUTPUT.PUT_LINE(v_systimestamp)
or doing something like the following before and after each section you want to monitor (no variable required):
DBMS_OUTPUT.PUT_LINE('Time Started: ' || TO_CHAR(SYSDATE, 'DD-MON-YYYY HH24:MI:SS'));
DBMS_OUTPUT.PUT_LINE('Time Ended: ' || TO_CHAR(SYSDATE, 'DD-MON-YYYY HH24:MI:SS'));
Here example 'CURRENT_TIMESTAMP'
curDateTime TIMESTAMP := CURRENT_TIMESTAMP ;
DBMS_OUTPUT.PUT_LINE('CUR TIME '|| curDateTime);
If a procedure you run takes a long time, DBMS_OUTPUT.PUT_LINE won't help much. Well, it will display something, but - as you use a loop, depending on number of iterations, DBMS_OUTPUT might run out of buffer size or exceed number of visible lines in SQL*Plus (if you use it) and you'll lose part of the output.
Besides, you won't see a single letter of the DBMS_OUTPUT.PUT_LINE until the procedure finishes.
Therefore, I'd suggest you to use another approach - a simple logging which requires a table, a sequence and an (autonomous transaction) procedure. Here's the script:
CREATE TABLE a1_log
(
id NUMBER,
datum DATE,
descr VARCHAR2 (500)
);
CREATE SEQUENCE seqlognap START WITH 1 INCREMENT BY 1;
CREATE OR REPLACE PROCEDURE a1_p_log (par_descr IN VARCHAR2)
IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
INSERT INTO a1_log
SELECT seqlognap.NEXTVAL, SYSDATE, par_descr FROM DUAL;
COMMIT;
END a1_p_log;
Then put A1_P_LOG calls into your own procedure; something like
begin
a1_p_log('selecting from a large table');
select ... into ... from ...;
a1_p_log('entering the loop');
while ... loop
a1_p_log('doing something');
end loop;
a1_p_log('the end');
end;
Here's where the autonomous transaction matters - it'll commit inserts into the log table (without affecting the main transaction), so you can (in another session, of course) trace the execution, simply by repeatedly issuing
SELECT *
FROM a1_log
ORDER BY id;
It'll show (imaginary example):
1 22.05.2018 18:13:00 selecting from a large table
2 22.05.2018 18:20:07 entering the loop
3 22.05.2018 18:20:07 doing something
4 22.05.2018 18:20:08 doing something
5 22.05.2018 18:20:09 doing something
6 22.05.2018 18:20:10 the end
and you'll see that step 1 takes 7 minutes to execute, so - that's what you need to investigate. It means that you don't have to wait the main procedure to finish - cancel it and start working on the bottleneck. Once you fix it, run everything again.

Netezza while loop syntax

I want to make a statement in netezza so that it waits until a statement is correct before proceeding. Any help would be appreciated - something similar to the below
WHILE (
select count(*) EVENT_DESCRIPTION from TEST_DA_CONTROL.CTRL.C_DBA_MAINTENANCE_AUDIT
where EVENT_DESCRIPTION = 'STARTED' and DATETIME_LOGGED > (select add_months(current_date,0))) = 0
LOOP
wait 5
end loop;
but I don't know the correct syntax.
Best to assign that output to a variable. I seem to recall that getting data out of an execute immediate is a little arduous in nzplsql, but there are convenient variables already available for you to use. Here I'll use ROW_COUNT.
declare
event_descriptions int;
sql varchar;
begin
event_descriptions := 1;
while event_descriptions > 0 loop
--Actual work
sql := '
select * EVENT_DESCRIPTION from TEST_DA_CONTROL.CTRL.C_DBA_MAINTENANCE_AUDIT
where EVENT_DESCRIPTION = ''STARTED'' and DATETIME_LOGGED > (select add_months(current_date,0))) = 0;';
execute immediate sql;
event_descriptions := ROW_COUNT;
end loop;
end;

Nested table loops to format data, PL/SQL

I am trying to format data returned from a cursor to JSON by looping through the records and columns without having to explicitly call on each column name. From what I've researched this vary well may not be a simple task or at least as simple as I'm trying to make it. I'm wondering if anyone else has tried a similar approach and if they had any luck.
declare
type type_cur_tab is table of employees%rowtype
index by PLS_integer;
type type_col_tab is table of varchar2(1000)
index by binary_integer;
tbl_rec type_cur_tab;
tbl_col type_col_tab;
begin
select * BULK COLLECT INTO tbl_rec
from employees;
select column_name BULK COLLECT INTO tbl_col
from all_tab_columns
where UPPER(table_name) = 'EMPLOYEES';
for i IN 1..tbl_rec.COUNT Loop
for j IN 1..tbl_col.count Loop
dbms_output.put_line(tbl_rec(i).tbl_col(j));
end loop;
end loop;
end;
It throws an error saying 'tbl_col' must be declared. I'm sure this is bc it's looking for 'tbl_col' listed inside 'tbl_rec'. Any help is greatly appreciated.
NOTE: I'm aware of the built in JSON conversion but I haven't been able to get it to as fast as I'd like so I'm trying to loop through and add the appropriate formatting along the way.
It is impossible to specify field of tbl_rec(i) in this manner.
Try this:
declare
v_cur sys_refcursor;
col_cnt number;
desc_t dbms_sql.desc_tab;
c number;
vVarchar varchar2(32000);
vNumber number;
vDate date;
v_result clob:='';
rn number:=0;
begin
--Any sql query or pass v_cur as input parameter in function on procedure
open v_cur for
select * from dual;
--------
c:=dbms_sql.to_cursor_number(v_cur);
dbms_sql.describe_columns(c => c, col_cnt => col_cnt, desc_t => desc_t);
for i in 1 .. col_cnt
loop
case desc_t(i).col_type
when dbms_types.TYPECODE_DATE then
dbms_sql.define_column(c, i ,vDate);
when dbms_types.TYPECODE_NUMBER then
dbms_sql.define_column(c, i ,vNumber);
else
dbms_sql.define_column(c, i ,vVarchar,32000);
end case;
end loop;
v_result:='{"rows":{ "row": [';
while (dbms_sql.fetch_rows(c)>0)
loop
if rn > 1 then v_result:=v_result||','; end if;
v_result:=v_result||'{';
for i in 1 .. col_cnt
loop
if (i>1) then v_result:=v_result||','; end if;
case desc_t(i).col_type
--Date
when dbms_types.typecode_date then
dbms_sql.column_value(c,i,vDate);
v_result:=v_result||' "'||desc_t(i).col_name||'" :"'||to_char(vDate,'dd.mm.yyyy hh24:mi')||'"';
--Number
when dbms_types.typecode_number then
dbms_sql.column_value(c,i,vNumber);
v_result:=v_result||' "'||desc_t(i).col_name||'" :"'||to_char(vNumber)||'"';
--Varchar - default
else
dbms_sql.column_value(c,i,vVarchar);
v_result:=v_result||' "'||desc_t(i).col_name||'" :"'||vVarchar||'"';
end case;
end loop;
v_result:=v_result||'}';
end loop;
v_result:=v_result||']}}';
dbms_output.put_line (v_result);
end;
Also you can generate XML from ref cursor with DBMS_XMLGEN package and then translate xml into json with xslt transformation.

getting data from memory instead of table

I have a parameter table with 10 rows. Called parameter_table.
In my PL/SQL procedure, I do loop in 2 million records. And each time querying this parameter table too.
I want to load this parameter table in to the memory and decrease the I/O process.
What is the best way to do this?
FOR cur_opt
IN (SELECT customer_ID,
NVL (customer_type, 'C') cus_type
FROM invoice_codes
WHERE ms.invoice_type='RT')
LOOP
....
...
Select data From parameter_table Where cus_type = cur_opt.cus_type AND cr_date < sysdate ; -- Where clause is much complex than this..
....
...
END LOOP;
You can just join it to your main query:
select customer_id, data
from parameter_table t, invoice_codes c
where t.cus_type = nvl(c.customer_type, 'C')
and t.cr_date < sysdate
However, if you've got 2 million records in invoice_codes, then joining to the parameter table is the least of your concerns - looping through this will take some time (and is probably the real cause of your I/O problems).
I Think you may change the query ,joining to parameter_table, so there will be no need to hit the select statement inside the loop. (like what #Chris Saxon solution)
But as a way to use cashed data,
You could fill a dictionary like, array and then refer it when necessary
Something like this may help:
you have to call Fill_parameters_cash before starting the main process and call get_parameter to fetch the data, the input parameter to call get_parameter is the dictionary key
TYPE ga_parameter_t IS TABLE OF parameter_table%ROWTYPE INDEX BY BINARY_INTEGER;
ga_parameter ga_parameter_t;
procedure Fill_parameters_cash is
begin
ga_parameter.DELETE;
SELECT * BULK COLLECT
INTO ga_parameter
FROM parameter_table;
end Fill_parameters_cash;
FUNCTION get_parameter(cus_type invoice_codes.cus_type%TYPE,
is_fdound OUT BOOLEAN)
RETURN parameter_table%ROWTYPE IS
result_value parameter_table%ROWTYPE;
pos NUMBER;
BEGIN
result_value := NULL;
is_fdound := FALSE;
IF cus_type IS NULL THEN
RETURN NULL;
END IF;
pos := ga_parameter.FIRST;
WHILE pos IS NOT NULL
LOOP
EXIT WHEN ga_parameter(pos).cus_type = cus_type;
pos := ga_parameter.NEXT(pos);
END LOOP;
IF pos IS NOT NULL THEN
is_fdound := TRUE;
result_value := ga_parameter(pos);
END IF;
RETURN result_value;
END get_parameter;
I'd guess looping through a million records is already causing issues. Not quite sure how this parameter table lookup is really worsening it.
Anyways, if this is really the only approach you can take, then you could do an inner or outer join in the cursor declaration.
----
FOR cur_opt
IN (SELECT customer_ID,
NVL (customer_type, 'C') cus_type
FROM invoice_codes codes,
parameter_table par
WHERE ms.invoice_type='RT'
and codes.cus_type = par.cus_type -- (or an outer join) maybe?
) loop
..........

Resources