I need some help in writing Oracle PL/SQL procedure that should do the following:
the procedure is called from a trigger after an update of the field in one table with the input parameter B-block or D-activate (this is already done)
the procedure should first open one cursor that will catch the account numbers of a client and open a loop that will process account by account
this one account should be forwarded to another loop that will catch card numbers of that client for that account (second cursor) and when into this loop, the card number should be used as an input parameter for a stored procedure that is called to block/unblock this card - this stored procedure already exists I just need to call it
the procedure don't need to return any parameters, the idea is just to block/activate card number of a client with the already written stored procedure for that
Should I write a package for this or just a procedure? And how can I write one loop in another?
I just realized that i can do this without cursors in a procedure. For simple example:
create or replace procedure blokiraj_proc (core_cust_id varchar2, kyc_blocked varchar2) as
type NumberArray is Array(100) of test_racuni.foracid%type;
type StringArray is Array (1000) of test_kartice.card_num%type;
accnt NumberArray;
card_number StringArray;
begin
select foracid bulk collect into accnt from test_racuni where cif_id = core_cust_id;
for i in accnt.first..accnt.last
loop
select card_num bulk collect into card_number from test_kartice where rbs_acct_num = accnt(i);
dbms_output.enable (100000);
dbms_output.put_line (accnt(i));
for j in 1..card_number.count
loop
dbms_output.put_line (card_number(j));
blokiraj_karticu (card_number(j));
end loop;
end loop;
end;
Is this a better approach then the curssors? And why is dbms_output not printing anything when i trigger the procedure?
As #EdStevens indicated you cannot avoid processing cursors. But you can avoid the looping structure of cursor within cursor. And the implicit open and close cursor for the inner one. The queries have combine into a simple JOIN then bulk collect into a single collection.
For this I created a RECORD to contain both the account number and card number; then a collection of that record. The cursor is then bulk collected into the collection. Your initial code allows for up to 100000 cards to be processed, and while I am a fan of bulk collect (when needed) I am not a fan of filling memory, therefore I limit the number of rows bulk collect gathers of each fetch. This unfortunately introduces a loop-within-loop construct, but the penalty is not near as great as cursor-within-cursor construct. The following is the result.
create or replace procedure blokiraj_proc (core_cust_id varchar2) as
type acct_card_r
is record(
acct_num test_kartice.rbs_acct_num%type
, card_num test_kartice.card_num%type
);
type acct_card_array is table of acct_card_r;
acct_card_list acct_card_array;
k_acct_card_buffer_limit constant integer := 997;
cursor c_acct_card(c_cust_id varchar2) is
select r.foracid
, k.card_num
from test_racuni r
left join test_kartice k
on (k.rbs_acct_num = r.foracid)
where r.cif_id = c_cust_id
order by r.foracid
, k.card_num;
begin
dbms_output.enable (buffer_size => null); -- enable dbms_output with size unlimited
open c_acct_card(core_cust_id);
loop
fetch c_acct_card
bulk collect
into acct_card_list
limit k_acct_card_buffer_limit;
for i in 1 .. acct_card_list.count
loop
dbms_output.put (acct_card_list(i).acct_num || ' ==> ');
if acct_card_list(i).card_num is not null
then
dbms_output.put_line (acct_card_list(i).card_num);
blokiraj_karticu (acct_card_list(i).card_num);
else
dbms_output.put_line ('No card for this account');
end if;
end loop;
-- exit buffer fetch when current buffeer is not full. As that means all rows
-- from cursor have been fetched/processed.
exit when acct_card_list.count < k_acct_card_buffer_limit;
end loop;
close c_acct_card;
end blokiraj_proc;
Well this is just another approach. If it's better for you, great. I also want to repeat and expand Ed Stevens warning of running this from a trigger. If either of the tables here is the table on which the trigger fired you will still get a mutating table exception - you cannot just hide it behind a procedure. And even if not its a lot of looping for trigger.
Related
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;
/
I have one table called EMP with 140000 rows and I need , to keep entire data into collection .How to extend collection and load entire data into collection using "BULK COLLECT ..LIMIT" clause feature.
The below logic not providing required result , since data has been overridden with new records.Please suggest me the logic.
DECLARE
CURSOR c_get_employee IS
SELECT empno,
ename,
deptno,
sal
FROM emp;
TYPE t_employee
IS TABLE OF c_get_employee%ROWTYPE INDEX BY inary_integer;
l_employee T_EMPLOYEE;
BEGIN
OPEN c_get_employee;
LOOP
FETCH c_get_employee bulk collect INTO l_employee limit 300;
EXIT WHEN l_employee.count = 0;
END LOOP;
CLOSE c_get_employee;
FOR i IN 1..l_employee.count LOOP
dbms_output.Put_line (L_employee(i).ename
||'<-->'
||L_employee(i).sal);
END LOOP;
EXCEPTION
WHEN OTHERS THEN
dbms_output.Put_line ('Unexpected error :- '
|| SQLERRM);
END;
You are exiting the loop too early. You need stop the fetch loop after the for loop and close cursor after that.
Also, as #APC pointed out, the exit condition should use count of fetched results instead of NOTFOUND on cursor. Otherwise, if the last fetch has lesser records than the fetch size, the NOTFOUND will be true and loop terminates incorrectly.
Try this:
DECLARE
CURSOR c_get_employee IS
SELECT empno,
ename,
deptno,
sal
FROM emp;
TYPE t_employee
IS TABLE OF c_get_employee%ROWTYPE INDEX BY binary_integer;
l_employee T_EMPLOYEE;
BEGIN
OPEN c_get_employee;
LOOP
FETCH c_get_employee bulk collect INTO l_employee limit 3;
EXIT WHEN l_employee.count = 0;
FOR i IN 1..l_employee.count LOOP
dbms_output.Put_line (L_employee(i).ename
||'<-->'
||L_employee(i).sal);
END LOOP;
END LOOP;
CLOSE c_get_employee;
EXCEPTION
WHEN OTHERS THEN
dbms_output.Put_line ('Unexpected error :- '
|| SQLERRM);
END;
The below logic is not giving required result
Wild guess: you're only getting twelve rows. This is a familiar gotcha with LIMIT clause. This line is the problem:
EXIT WHEN c_get_employee%NOTFOUND;
You have fourteen records in EMP: The limit of 3 means you collect four sets of records. The last FETCH only collects 2 records. PL/SQL interprets this as NOTFOUND. The solution is to check the size of the collection:
EXIT WHEN l_employee.count() = 0;
I want to load entire data into collection and close the cursor.After that I want to open collection and use data for business logic
That's not how BULK COLLECT ... LIMIT works. The point of the LIMIT clause is to, er, limit the number of records fetched at a time. We need to do this when the queried data is too big to handle in a single fetch. PL/SQL collections are memory structures held in the session's allocation of memory: if they get too big they will blow the PGA. (Definition of "too big" will depend on how your DBA has configured the PGA.)
So, if you have a small result set, ditch the LIMIT clause and populate the collection in a single fetch. But if you have sufficient data to require the LIMIT clause you need to include the business logic loop inside the fetch loop.
I want to create procedure, that will use cursor, which is the same for arbitrary tables. My current one looks like this:
create or replace
PROCEDURE
some_name(
p_talbe_name IN VARCHAR2,
p_chunk_size IN NUMBER,
p_row_limit IN NUMBER
) AS
CURSOR v_cur IS
SELECT common_column,
ora_hash(substr(common_column, 1, 15), p_chunk_size) as chunk_number
-- Here it can find the table!
FROM p_table_name;
TYPE t_sample IS TALBE OF v_cur%rowtype;
v_sample t_sample;
BEGIN
OPEN v_cur;
LOOP FETCH v_cur BULK COLLECT INTO v_sample LIMIT p_row_limit;
FORALL i IN v_sample.first .. v_sample.last
INSERT INTO chunks VALUES v_sample(i);
COMMIT;
EXIT WHEN v_cur%notfound;
END LOOP;
CLOSE v_cur;
END;
The problem is that it cannot find the table named p_table_name which I want to parametrize. The thing is that I need to create chunks based on hashes for common_column which exists in all intended tables. How to deal with that problem? Maybe there is the equivalent oracle code that will do the same thing? Then I need the same efficiency for the query. Thanks!
I would do this as a single insert-as-select statement, complicated only by the fact you're passing in the table_name, so we need to use dynamic sql.
I would do it something like:
CREATE OR REPLACE PROCEDURE some_name(p_table_name IN VARCHAR2,
p_chunk_size IN NUMBER,
p_row_limit IN NUMBER) AS
v_table_name VARCHAR2(32); -- 30 characters for the tablename, 2 for doublequotes in case of case sensitive names, e.g. "table_name"
v_insert_sql CLOB;
BEGIN
-- Sanitise the passed in table_name, to ensure it meets the rules for being an identifier name. This is to avoid SQL injection in the dynamic SQL
-- statement we'll be using later.
v_table_name := DBMS_ASSERT.ENQUOTE_LITERAL(p_table_name);
v_insert_sql := 'insert into chunks (common_column_name, chunk_number)'||CHR(10)|| -- replace the column names with the actual names of your chunks table columns.
'select common_column,'||CHR(10)||
' ora_hash(substr(common_column, 1, 15), :p_chunk_size) AS chunk_number'||CHR(10)||
'from '||v_table_name||CHR(10)||
'where rownum <= :p_row_limit';
-- Used for debug purposes, so you can see the definition of the statement that's going to be run.
-- Remove before putting the code in production / convert to proper logging code:
dbms_output.put_line(v_insert_sql);
-- Now run the statement:
EXECUTE IMMEDIATE v_insert_sql USING p_chunk_size, p_row_limit;
-- I've included the p_row_limit in the above statement, since I'm not sure if your original code loops through all the rows once it processes the
-- first p_row_limit rows. If you need to insert all rows from the p_table_name into the chunks table, remove the predicate from the insert sql and the extra bind variable passed into the execute immediate.
END some_name;
/
By using a single insert-as-select statement, you are using the most efficient way of doing the work. Doing the bulk collect (which you were using) would use up memory (storing the data in the array) and cause extra context switches between the PL/SQL and SQL engines that the insert-as-select statement avoids.
Say I have a stored procedure which accepts 2 varchars, does some processing and updates my business tables. Is there a way that I can run the stored procedure for the results from a select query?
Like,
execute my_stored_proc select varchar_1,varchar_2 from an_ip_table;
You can iterate over results by loop
BEGIN
FOR RECS IN (SELECT varchar_1, varchar_2 FROM an_ip_table)
LOOP
my_stored_proc (RECS.varchar_1, RECS.varchar_2);
END LOOP;
END
This could be a simple way:
begin
for i in (
select varchar_1, varchar_2
from an_ip_table
)
loop
my_stored_proc(i.varchar_1, i.varchar_2);
end loop;
end;
Initially, I thought of just to put a comment, but this needs some explanation, so I'm writing an answer. You are actually doing it the wrong way. Ideally, you should be passing a cursor to your my_stored_proc and fetching the cursor inside the procedure. Your method actually causes multiple calls to procedure for every row from the query result. The processing will be very slow if you have huge volume of data. It is a bad idea even if there are few rows.
Here is a sample procedure that does a dml operation using FORALL.It is just a sample, but you should be able to convert your select query such that you should be able to do dml this way.
CREATE OR REPLACE PROCEDURE my_stored_proc (
p_iptab_cur SYS_REFCURSOR
) AS
TYPE iprec IS RECORD ( col1 an_ip_table.col1%TYPE,
col2 an_ip_table.col1%TYPE );
TYPE iptype IS
TABLE OF iprec;
ips iptype;
BEGIN
FETCH p_iptab_cur BULK COLLECT INTO ips;
FORALL i IN ips.FIRST..ips.LAST
--Your DML-- using the collection of records.
END;
/
--Calling the procedure by passing the `CURSOR`
DECLARE
x SYS_REFCURSOR;
BEGIN
OPEN x FOR select col1, col2
from an_ip_table;
my_stored_proc(x);
END;
/
I am handling millions of rows and it takes hours, so I want some feedback to give me an idea about the status of the process. It won't be possible to have real time feedback from a stored procedure since the output will be available only after the complete execution. Any solution?
There are a couple of solutions.
One is to write to a log of some description. You can use a file (writing out with UTL_FILE) or a table (using autonomous transactions so that the records are visible in another session without affecting the main transaction).
The other solution is to write to the V$SESSION_LONGOPS view using DBMS_APPLICATION_INFO.SET_SESSION_LONGOPS. Find out more
I think logging is always a good idea with long-running background procedures. If something goes wrong your logs are the only source of info you have.
You could have a ReportStatus procedure that writes to a different table (one you can select from while your procedure is running).
It would need a PRAGMA AUTONOMOUS_TRANSACTION so it can commit independently of your main procedure.
Example:
CREATE OR REPLACE PROCEDURE ReportStatus(status NUMBER)
AS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
INSERT INTO StatusTable VALUES(SYSDATE, status);
COMMIT;
END ReportStatus;
/
One more way to get feedback from running PL/SQL block is to use inter-session communication. In order to use this solution you need at least two sessions.
In audit (first) session:
set head off arrays 1 pages 0 feedback off timing off
select column_value notification from table (notificationReceive);
It's going to be stuck there until first message arrives.
As the next step execute your PL/SQL block in the another (second) session:
begin
notificationSend ('START: '||'myBlockName');
/* UPDATE table1 SET col1 = value ... WHERE some condition;
*/ dbms_lock.sleep(3);
notificationSend ('Updated 1000000 rows'); -- sql%rowcount instead of literal
/* INSERT INTO table2 (val1, SYSDATE, v_user_id);
*/ dbms_lock.sleep(2);
notificationSend ('Inserted 1000000 rows');
notificationSend ('ENDTX');
end;
/
Now, you can go back to the first session and see the real-time feedback from running PL/SQL block:
11:13:39: START: myBlockName
11:13:42: Updated 1000000 rows
11:13:44: Inserted 1000000 rows
Below the code of almost one line functions:
create or replace procedure notificationSend (message varchar2) is
pragma autonomous_transaction;
begin
dbms_alert.signal ('feedback$_queue', message);
commit;
end;
/
create or replace function notificationReceive return sys.odciVarchar2List pipelined is
pragma autonomous_transaction;
message varchar2(1800) := 'NONE';
status number := -1;
begin
dbms_alert.register('feedback$_queue');
<<reading>> loop
dbms_alert.waitone('feedback$_queue', message, status);
if status = 0 and message != 'ENDTX' then
pipe row (to_char (sysdate, 'hh:mi:ss')||': '||message);
pipe row (null); -- dummy row for prefetch in sqlplus
else
exit reading;
end if;
end loop reading;
dbms_alert.remove('feedback$_queue');
return;
end;
/
Tested with releases 11.2.0.4.0, 12.2.0.1.0.