PL/SQL procedure for loop though a table and change values - oracle

Basically I need to make a for loop that will loop though the amount of rows. In each row I need to check a value and change it if it meets the requirements.
I'm new to Oracle, so I just started building it one step at a time and I'm stuck on looping through the table rows. I need to first get the number count of the rows that a Boolean flag set to 0 (false). So then I can loop through only those rows, not every row in the table. Once I'm done with whatever I need to change in that row, set the flag to 1 (true), so when I run the procedure again it won't include that row.
Here's what I have so far:
My table:
CREATE TABLE test_table_results (
name varchar,
account number,
address varchar,
database_search NUMBER(1) DEFAULT 0 NOT NULL
CONSTRAINT searched_in_database CHECK (database_search IN (0,1))
);
The table in my database:
CREATE TABLE test_table_accounts (
name varchar,
account number,
address varchar,
);
Now the procedure will go though the results table and see if the address match, if they do it will copy the account number from the database table into the results account number, then change the flag from 0 to 1, so the next time I search though the table it won't include it because it was already searched.
create or replace PROCEDURE FIND_MATCH_ADDRESS AS
BEGIN
DECLARE
v_cnt NUMBER;
BEGIN
FOR i IN (SELECT rowid, r.* FROM test_table
WHERE database_searched = 0)
LOOP
LOOP
SELECT COUNT(1) INTO v_cnt
FROM test_table
WHERE database_searched = 0;
DBMS_OUTPUT.PUT_LINE(v_cnt);
END LOOP;
END LOOP;
END;
END FIND_MATCH_ADDRESS;
EDIT: Added the two tables in hopes to make my question/task more understandable.
Again thank you for your time!!

In your example I see some mistakes.
In the procedure you do not need Declare. Declaration block is between as and begin
create or replace PROCEDURE proc
AS
-- here variable declaration or local function or procedures
BEGIN
-- here you can write a business logic
END proc;
You can iterate over the records of a table with a For loop. In your example, you also tried to use them. You can iterate over the records of a table with a For loop. In your example, you also tried to use them.
FOR record IN (cursor)
LOOP
{...statements...}
END LOOP;
I did not quite understand why you used another loop in the loop. a loop statement is an endless loop.
loop
...
end loop;
In the loop you can now implement your logic. If you really want to use a loop, then your solution might look like this
create or replace PROCEDURE FIND_MATCH_ADDRESS
AS
v_cnt NUMBER;
BEGIN
FOR rec IN (SELECT r.name
,r.address
,r.account
,a.account as new_account
FROM test_table_results r
join test_table_accounts a on a.address = r.address
WHERE r.database_searched = 0)
LOOP
update test_table_results
set account = rec.new_account
, database_searched = true
where account = rec.account
and name = rec.name
and adress = rec.adress;
END LOOP;
END FIND_MATCH_ADDRESS;
Alternatively, you can also do that with an update. Since I do not know your tables, you should then optimize the where condition.
update test_table_results t
set database_searched = true
, account = (select account
from test_table_accounts a
where a.account = t.account
and a.name = t.name
and a.adress = t.adress)
where database_searched = false
and exists(select 1
from test_table_accounts a
where a.account = t.account
and a.name = t.name
and a.adress = t.adress);

Related

Getting the corresponding record of a Cursor/Select when in a Cursor LOOP statement

The question seems easy. I have built a package, where there is a quite massive cursor, let's say on all invoices of my company for the whole year.
CURSOR c_invoices(p_year IN INTEGER) IS
SELECT all_invoices.invoicenumber,
all_invoices.invoicedate,
all_invoices.customernumber
FROM all_invoices
WHERE all_invoices.year = p_year
;
After opening it and using a LOOP statement, I want to get some data from another table (forbidden_customers), but only if the customer is in this very last table.
What I'd like to do, is to open another cursor (or a SELECT ?) at the very beginning of my package, browsing the whole table(forbidden_customers), and then getting to the corresponding record when in my invoices LOOP.
So, something like :
CURSOR c_forbidden_customers IS
SELECT forbidden_customers.customernumber,
forbidden_customers.customeradress
FROM forbidden_customers
;
And then :
OPEN c_invoices(v_year);
LOOP FETCH c_invoices INTO invoices_cursor;
BEGIN
EXIT WHEN c_invoices%NOTFOUND;
*IF invoices_cursor.customernumber IS FOUND IN c_forbidden_customers ...
THEN ...*
This is what I do meanwhile (I know it is bad):
SELECT COUNT(*)
INTO v_exist /*INTEGER*/
FROM forbidden_customers
WHERE forbidden_customers.customernumber= p_customernumber
IF v_exist <> 0
THEN...
I tried to make it as clear as possible. Thank you for your time
Don't do it twice; join both tables in the same cursor and use it. Also, if you switch to a cursor FOR loop, you'll save yourself from some typing as Oracle will do most of boring stuff for you (declaring cursor variable, opening the cursor, closing it, exiting the loop ...):
create or replace procedure p_test (p_year in integer) is
begin
for c_invoices in
(select a.invoicenumber,
a.invoicedate,
a.customernumber,
c.customeraddress
from all_invoices a join forbidden_customers c on c.customernumber = a.customernumber
where a.year = p_year)
loop
-- do something
end loop;
end;
If the table forbidden_customers is not large and it will fit oracle's session memory, you can use a pl/sql table to store all id's from forbidden_customers and check it later. The check is done in memory only, so it is much faster than any regular select.
create table all_invoices
(id number,
year number,
customer_number number);
create table forbidden_customers
(customer_number number);
CREATE OR REPLACE TYPE t_number_table IS TABLE OF number
/
CREATE OR REPLACE PROCEDURE test23
IS
forbidden_customers_list t_number_table;
CURSOR c_invoices (p_year IN INTEGER)
IS
SELECT all_invoices.customer_number
FROM all_invoices
WHERE all_invoices.year = p_year;
BEGIN
SELECT customer_number
BULK COLLECT INTO forbidden_customers_list
FROM forbidden_customers;
FOR rec_invoices in c_invoices(2022) loop
if forbidden_customers_list.exists(rec_invoices.customer_number) then
null;
end if;
end loop;
end;
/

PL/SQL : Need to compare data for every field in a table in plsql

I need to create a procedure which will take collection as an input and compare the data with staging table data row by row for every field (approx 50 columns).
Business logic :
whenever a staging table column value will mismatch with the corresponding collection variable value then i need to update 'FAIL' into staging table STATUS column and reason into REASON column for that row.
If matched then need to update 'SUCCESS' in STATUS column.
Payload will be approx 500 rows in each call.
I have created below sample script:
PKG Specification :
CREATE OR REPLACE
PACKAGE process_data
IS
TYPE pass_data_rec
IS
record
(
p_eid employee.eid%type,
p_ename employee.ename%type,
p_salary employee.salary%type,
p_dept employee.dept%type
);
type p_data_tab IS TABLE OF pass_data_rec INDEX BY binary_integer;
PROCEDURE comp_data(inpt_data IN p_data_tab);
END;
PKG Body:
CREATE OR REPLACE
PACKAGE body process_data
IS
PROCEDURE comp_data (inpt_data IN p_data_tab)
IS
status VARCHAR2(10);
reason VARCHAR2(1000);
cnt1 NUMBER;
v_eid employee_copy.eid%type;
v_ename employee_copy.ename%type;
BEGIN
FOR i IN 1..inpt_data.count
LOOP
SELECT ec1.eid,ec1.ename,COUNT(*) over () INTO v_eid,v_ename,cnt1
FROM employee_copy ec1
WHERE ec1.eid = inpt_data(i).p_eid;
IF cnt1 > 0 THEN
IF (v_eid=inpt_data(i).p_eid AND v_ename = inpt_data(i).p_ename) THEN
UPDATE employee_copy SET status = 'SUCCESS' WHERE eid = inpt_data(i).p_eid;
ELSE
UPDATE employee_copy SET status = 'FAIL' WHERE eid = inpt_data(i).p_eid;
END IF;
ELSE
NULL;
END IF;
END LOOP;
COMMIT;
status :='success';
EXCEPTION
WHEN OTHERS THEN
status:= 'fail';
--reason:=sqlerrm;
END;
END;
But in this approach i have below mentioned issues.
Need to declare all local variables for each column value.
Need to compare all variable data using 'and' operator. Not sure whether it is correct way or not because if there are 50 columns then if condition will become very heavy.
IF (v_eid=inpt_data(i).p_eid AND v_ename = inpt_data(i).p_ename) THEN
Need to update REASON column when any column data mismatched (first mismatched column name) for that row, in this approach i am not able to achieve.
Please suggest any other good way to achieve this requirement.
Edit :
There is only one table at my end i.e target table. Input will come from any other source as collection object.
REVISED Answer
You could load the the records into t temp table, but unless you want additional processing it's not necessary. AFAIK there is no way to identify the offending column (first one only) without slugging through column-by-column. However, your other concern having to declare a variable is not necessary. You can declare a single variable defined as %rowtype which gives you access to each column by name.
Looping through an array of data to find the occasional error is just bad (imho) with SQL available to eliminate the good ones in one fell swoop. And it's available here. Even though your input is a array we can use as a table by using the TABLE operator, which allows an array (collection) as though it were a database table. So the MINUS operator can till be employed. The following routine will set the appropriate status and identify the first miss matched column for each entry in the input array. It reverts to your original definition in package spec, but replaces the comp_data procedure.
create or replace package body process_data
is
procedure comp_data (inpt_data in p_data_tab)
is
-- define local array to hold status and reason for ecah.
type status_reason_r is record
( eid employee_copy.eid%type
, status employee_copy.status%type
, reason employee_copy.reason%type
);
type status_reason_t is
table of status_reason_r
index by pls_integer;
status_reason status_reason_t := status_reason_t();
-- define error array to contain the eid for each that have a mismatched column
type error_eids_t is table of employee_copy.eid%type ;
error_eids error_eids_t;
current_matched_indx pls_integer;
/*
Helper function to identify 1st mismatched column in error row.
Here is where we slug our way through each column to find the first column
value mismatch. Note: There is actually validate the column sequence, but
for purpose here we'll proceed in the input data type definition.
*/
function identify_mismatch_column(matched_indx_in pls_integer)
return varchar2
is
employee_copy_row employee_copy%rowtype;
mismatched_column employee_copy.reason%type;
begin
select *
into employee_copy_row
from employee_copy
where employee_copy.eid = inpt_data(matched_indx_in).p_eid;
-- now begins the task of finding the mismatched column.
if employee_copy_row.ename != inpt_data(matched_indx_in).p_ename
then
mismatched_column := 'employee_copy.ename';
elsif employee_copy_row.salary != inpt_data(matched_indx_in).p_salary
then
mismatched_column := 'employee_copy.salary';
elsif employee_copy_row.dept != inpt_data(matched_indx_in).p_dept
then
mismatched_column := 'employee_copy.dept';
-- elsif continue until ALL columns tested
end if;
return mismatched_column;
exception
-- NO_DATA_FOUND is the one error that cannot actually be reported in the customer_copy table.
-- It occurs when an eid exista in the input data but does not exist in customer_copy.
when NO_DATA_FOUND
then
dbms_output.put_line( 'Employee (eid)='
|| inpt_data(matched_indx_in).p_eid
|| ' does not exist in employee_copy table.'
);
return 'employee_copy.eid ID is NOT in table';
end identify_mismatch_column;
/*
Helper function to find specified eid in the initial inpt_data array
Since the resulting array of mismatching eid derive from a select without sort
there is no guarantee the index values actually match. Nor can we sort to build
the error array, as there is no way to know the order of eid in the initial array.
The following helper identifies the index value in the input array for the specified
eid in error.
*/
function match_indx(eid_in employee_copy.eid%type)
return pls_integer
is
l_at pls_integer := 1;
l_searching boolean := true;
begin
while l_at <= inpt_data.count
loop
exit when eid_in = inpt_data(l_at).p_eid;
l_at := l_at + 1;
end loop;
if l_at > inpt_data.count
then
raise_application_error( -20199, 'Internal error: Find index for ' || eid_in ||' not found');
end if;
return l_at;
end match_indx;
-- Main
begin
-- initialize status table for each input enter
-- additionally this results is a status_reason table in a 1:1 with the input array.
for i in 1..inpt_data.count
loop
status_reason(i).eid := inpt_data(i).p_eid;
status_reason(i).status :='SUCCESS';
end loop;
/*
We can assume the majority of data in the input array is valid meaning the columns match.
We'll eliminate all value rows by selecting each and then MINUSing those that do match on
each column. To accomplish this cast the input with TABLE function allowing it's use in SQL.
Following produces an array of eids that have at least 1 column mismatch.
*/
select p_eid
bulk collect into error_eids
from (select p_eid, p_ename, p_salary, p_dept from TABLE(inpt_data)
minus
select eid, ename, salary, dept from employee_copy
) exs;
/*
The error_eids array now contains the eid for each miss matched data item.
Mark the status as failed, then begin the long hard process of identifying
the first column causing the mismatch.
The following loop used the nested functions to slug the way through.
This keeps the main line logic clear.
*/
for i in 1 .. error_eids.count -- if all inpt_data rows match then count is 0, we bypass the enttire loop
loop
current_matched_indx := match_indx(error_eids(i));
status_reason(current_matched_indx).status := 'FAIL';
status_reason(current_matched_indx).reason := identify_mismatch_column(current_matched_indx);
end loop;
-- update employee_copy with appropriate status for each row in the input data.
-- Except for any cid that is in the error eid table but doesn't exist in the customer_copy table.
forall i in inpt_data.first .. inpt_data.last
update employee_copy
set status = status_reason(i).status
, reason = status_reason(i).reason
where eid = inpt_data(i).p_eid;
end comp_data;
end process_data;
There are a couple other techniques used you may want to look into if you are not familiar with them:
Nested Functions. There are 2 functions defined and used in the procedure.
Bulk Processing. That is Bulk Collect and Forall.
Good Luck.
ORIGINAL Answer
It is NOT necessary to compare each column nor build a string by concatenating. As you indicated comparing 50 columns becomes pretty heavy. So let the DBMS do most of the lifting. Using the MINUS operator does exactly what you need.
... the MINUS operator, which returns only unique rows returned by the
first query but not by the second.
Using that this task needs only 2 Updates: 1 to mark "fail", and 1 to mark "success". So try:
create table e( e_id integer
, col1 varchar2(20)
, col2 varchar2(20)
);
create table stage ( e_id integer
, col1 varchar2(20)
, col2 varchar2(20)
, status varchar2(20)
, reason varchar2(20)
);
-- create package spec and body
create or replace package process_data
is
procedure comp_data;
end process_data;
create or replace package body process_data
is
package body process_data
procedure comp_data
is
begin
update stage
set status='failed'
, reason='No matching e row'
where e_id in ( select e_id
from (select e_id, col1, col2 from stage
except
select e_id, col1, col2 from e
) exs
);
update stage
set status='success'
where status is null;
end comp_data;
end process_data;
-- test
-- populate tables
insert into e(e_id, col1, col2)
select (1,'ABC','def') from dual union all
select (2,'No','Not any') from dual union all
select (3,'ok', 'best ever') from dual union all
select (4,'xx','zzzzzz') from dual;
insert into stage(e_id, col1, col2)
select (1,'ABC','def') from dual union all
select (2,'No','Not any more') from dual union all
select (4,'yy', 'zzzzzz') from dual union all
select (5,'no e','nnnnn') from dual;
-- run procedure
begin
process_data.comp_date;
end;
-- check results
select * from stage;
Don't ask. Yes, you to must list every column you wish compared in each of the queries involved in the MINUS operation.
I know the documentation link is old (10gR2), but actually finding Oracle documentation is a royal pain. But the MINUS operator still functions the same in 19c;

Error when running stored procedure : maximum number of object durations exceeded seems when variable not pass it works

I'm newbie in stored procedures and I create a stored procedure, but when I run it by user input, I get an error; but when get value to variable daynumber, it is working.
Suggetions from SQL Developer are:
*Cause: This typically happens if there is infinite recursion in the PL/SQLfunction that is being executed.
*Action: User should alter the recursion condition in order to prevent infinite recursion.
How can I solve it?
create or replace procedure P_SiteNumber_Range_D(Sitenum NUMBER) is
daynumber number;
begin
p_sitenumber_range_d(Sitenum => daynumber);
-- daynumber := 2;
for l in (select PROVINCE from v_sitenumber_D_province_range)
loop
update PM4h_db.IND_D_3102
set IND_D_3102_029 =
(select countsite from some table where l1.province=province );
end loop;
end P_SiteNumber_Range_D;
Run procedure as :
DECLARE
SITENUM NUMBER;
BEGIN
SITENUM := 3;
P_SITENUMBER_RANGE_D(
SITENUM => SITENUM
);
END;
This procedure doesn't make much sense (at least, to me).
you are passing sitenum and never do anything with it; should it be used in where clause in cursor for loop and/or update statement?
this is a procedure, and then - in line #4 of your original code - you are calling itself (which then calls itself which calls itself etc., until Oracle stops it and returns an error)
the most obvious "solution" is to remove that statement:
p_sitenumber_range_d(Sitenum => daynumber);
but that probably won't be all, because of my first objection
Furthermore, maybe you don't need the loop at all, as the whole code can be rewritten as
create or replace procedure p_sitenumber_range_d (par_sitenum in number)
is
begin
update pm4h_db.ind_d_3102 set
ind_d_3102_029 = (select countsite
from some_table
where province = (select province
from v_sitenumber_d_province_range
where sitenum = par_sitenum
)
);
end;
It might, or might not work - there's a possibility of TOO_MANY_ROWS if select returns more than a single value. I don't know, as I don't have your tables, so - that might need to be fixed.
If you insist on the loop, then consider such a code:
create or replace procedure p_sitenumber_range_d (par_sitenum in number)
is
begin
for cur_r in (select province
from v_sitenumber_d_province_range
where sitenum = par_sitenum
)
loop
update pm4h_db.ind_d_3102 set
ind_d_3102_029 = (select countsite
from some_table
where province = cur_r.province
);
end loop;
end;
Are you aware that you've built in a recursion?
The first thing you do during the procedure is to call up the procedure itself!

Code not working. It takes an eternity to run

The code below runs for an eternity.
As you can see i have to take values from one table and use that value to check if the second table contains it or not and insert into the third table values from the first table.
Is there any other way of doing this?
create or replace PROCEDURE KPI_AVAILABILITY (
v_programid varchar2
)
AS
v_MASTER_KPI_ID number;
v_UDF varchar2(100);
v_count number;
cursor c1 is
(select MASTER_KPI_ID,UDF from KPI_MASTER
where UDF is not null
and ISACTIVE = 1
--order by MASTER_KPI_ID,udf
);
BEGIN
open c1 ;
fetch c1 into v_MASTER_KPI_ID,v_UDF;
while v_UDF is not null
loop
select count(v_UDF) into v_count
from vw_ticket
where v_UDF is not null
and amsprogramid = v_programid;
if v_count is not null or v_count <> 0 then
delete from program_kpi where amsprogramid = v_programid;
INSERT INTO PROGRAM_KPI (AMSPROGRAMID,MASTER_KPI_ID,LASTUPDATEDBYDATALOAD)
VALUES(V_PROGRAMID,v_MASTER_KPI_ID,to_char(sysdate,'dd-mon-yy hh.mi.ss'));
dbms_output.put_line('xyz');
end if;
end loop;
close c1;
END KPI_AVAILABILITY;
Reverse engineering business rules from another developer's code is always tricky, especially without understanding the wider domain. However, at the centre of the loop is DELETE from program_kpi followed by an INSERT into the same table. If there are no records matching on amsprogramid = v_programid then you're inserting a record, if there are matches then effectively you're just updating lastupdatedbydataload with the current SYSDATE.
In others, it appears to be the logic of a MERGE. So perhaps your code could be entirely replaced with a single statement. If so, this is likely to be a lot more efficient than the row-by-agonizing-row process within a cursor loop.
merge into program_kpi pkpi
using (select kpim.master_kpi_id
, kpim.udf
, v_programid
from kpi_master kpim
where kpim.udf is not null
and kpim.isactive = 1
and exists ( select null
from vw_ticket tkt
where tkt.amsprogramid = v_programid)
) kpim
on (kpim.v_programid = pkpi.programid
and kpim.master_kpi_id = pkpi.master_kpi_id)
when not matched then
insert values (kpim.v_programid, kpim.master_kpi_id, sysdate)
when matched then
update
set pkpi.lastupdatedbydataload = sysdate;
Please check the results of this code with your expected outcome. As I said, reverse-engineering business logic is hard, and matching on master_kpi_id as well as programid is not the same as just deleting on programid.
You do not change v_UDF after first fetch. Then loop compare it with same first value... compare and compare... compare and compare.

oracle stored procedure - select, update and return a random set of rows

oracle i wish to select few rows at random from a table, update a column in those rows and return them using stored procedure
PROCEDURE getrows(box IN VARCHAR2, row_no IN NUMBER, work_dtls_out OUT dtls_cursor) AS
v_id VARCHAR2(20);
v_workname VARCHAR2(20);
v_status VARCHAR2(20);
v_work_dtls_cursor dtls_cursor;
BEGIN
OPEN v_work_dtls_cursor FOR
SELECT id, workname, status
FROM item
WHERE status IS NULL
AND rownum <= row_no
FOR UPDATE;
LOOP
FETCH v_work_dtls_cursor
INTO v_id ,v_workname,v_status;
UPDATE item
SET status = 'started'
WHERE id=v_id;
EXIT
WHEN v_work_dtls_cursor % NOTFOUND;
END LOOP;
close v_work_dtls_cursor ;
/* I HAVE TO RETURN THE SAME ROWS WHICH I UPDATED NOW.
SINCE CURSOR IS LOOPED THRU, I CANT DO IT. */
END getrows;
PLEASE HELP
Following up on Sjuul Janssen's excellent recommendation:
create type get_rows_row_type as object
(id [item.id%type],
workname [item.workname%type],
status [item.status%type]
)
/
create type get_rows_tab_type as table of get_rows_row_type
/
create function get_rows (box in varchar2, row_no in number)
return get_rows_tab_type pipelined
as
v_work_dtls_cursor dtls_cursor;
l_out_rec get_rows_row_type;
BEGIN
OPEN v_work_dtls_cursor FOR
SELECT id, workname, status
FROM item sample ([ROW SAMPLE PERCENTAGE])
WHERE status IS NULL
AND rownum <= row_no
FOR UPDATE;
LOOP
FETCH v_work_dtls_cursor
INTO l_out_rec.id, l_out_rec.workname, l_outrec.status;
EXIT WHEN v_work_dtls_cursor%NOTFOUND;
UPDATE item
SET status = 'started'
WHERE id=l_out_rec.id;
l_out_rec.id.status := 'started';
PIPE ROW (l_out_rec);
END LOOP;
close v_work_dtls_cursor ;
END;
/
A few notes:
This is untested.
You'll need to replace the bracketed section in the type declarations with appropriate types for your schema.
You'll need to come up with an appropriate value in the SAMPLE clause of the SELECT statement; it might be possible to pass that in as an argument, but that may require using dynamic SQL. However, if your requirement is to get random rows from the table -- which just filtering by ROWNUM will not accomplish -- you'll want to do something like this.
Because you're SELECTing FOR UPDATE, one session can block another. If you're in 11g, you may wish to examine the SKIP LOCKED clause of the SELECT statement, which will enable multiple concurrent sessions to run code like this.
Not sure where you are doing your committing, but based on the code as it stands all you should need to do is SELECT ... FROM ITEM WHERE STATUS='started'
If it is small numbers, you could keep a collection of ROWIDs.
if it is larger, then I'd do an
INSERT into a global temporary table SELECT id FROM item .. AND ROWNUM < n;
UPDATE item SET status = .. WHERE id in (SELECT id FROM global_temp_table);
Then return a cursor of
SELECT ... FROM item WHERE id in (SELECT id FROM global_temp_table);
Maybe this can help you to do what you want?
http://it.toolbox.com/blogs/database-solutions/returning-rows-through-a-table-function-in-oracle-7802
A possible solution:
create type nt_number as table of number;
PROCEDURE getrows(box IN VARCHAR2,
row_no IN NUMBER,
work_dtls_out OUT dtls_cursor) AS
v_item_rows nt_number;
indx number;
cursor cur_work_dtls_cursor is
SELECT id
FROM item
WHERE status IS NULL
AND rownum <= row_no
FOR UPDATE;
BEGIN
open cur_work_dtls_cursor;
fetch cur_work_dtls_cursor bulk collect into nt_number;
for indx in 1 .. item_rows.count loop
UPDATE item
SET status = 'started'
WHERE id=v_item_rows(indx);
END LOOP;
close cur_work_dtls_cursor;
open work_dtls_out for select id, workname, status
from item i, table(v_item_rows) t
where i.id = t.column_value;
END getrows;
If the number of rows is particularly large, the global temporary solution may be better.

Resources