PL/SQL Cursor whose query is based on a variable - oracle

I've got a PL/SQL block that's basically
DECLARE
PIDM NUMBER(8);
CLM_TEST_SCORE NUMBER(5);
CURSOR C_STUDENT IS
select PIDM
from SOSC.DW_ALL_COLLECTOR
order by PIDM;
CURSOR C_CLM_SCORES IS
select max(to_number(SORTEST_TEST_SCORE))
from SATURN.SORTEST
where SORTEST_PIDM = pidm;
BEGIN
OPEN C_STUDENT;
LOOP
CLM_TEST_SCORE := '';
FETCH c_Student INTO pidm;
EXIT WHEN c_Student%notfound;
OPEN C_CLM_SCORES;
FETCH C_CLM_SCORES INTO CLM_TEST_SCORE;
CLOSE C_CLM_SCORES;
insert into some_table (CLM_TEST_SCORE)
values (CLM_TEST_SCORE);
END LOOP
END
As far as I'm aware, the pidm referred to in C_CLM_SCORES is the PIDM NUMBER(8) declared in line 2. That would mean that the query the cursor refers to mutates every iteration of the LOOP, depending on the current value of pidm. That doesn't jive with my understanding of cursors as a query-in-progress, as the underlying query changes every LOOP. Maybe it's the original author taking advantage of a clever DB algorithm?
This code works. I just have absolutely no idea why. What the heck is going on here?

You have an overly confusing block of code that is a nightmare to debug as you have:
SQL statements that refer to column name and local variables with the same identifier (PIDM and CLM_TEST_SCORE).
Cursors that change every iteration because they contain a bind variable referring to local variables (PIDM).
Highly inefficient use of loops.
If you want to make it clearer, you can rewrite the PL/SQL block so that you do not have duplicate identifiers and use a parameterised cursor:
DECLARE
v_PIDM SOSC.DW_ALL_COLLECTOR.PIDM%TYPE;
v_CLM_TEST_SCORE some_table.CLM_TEST_SCORE%TYPE;
CURSOR C_STUDENT IS
select PIDM
from SOSC.DW_ALL_COLLECTOR
order by PIDM;
CURSOR C_CLM_SCORES(p_pidm NUMBER) IS
select max(to_number(SORTEST_TEST_SCORE))
from SATURN.SORTEST
where SORTEST_PIDM = p_pidm;
BEGIN
OPEN C_STUDENT;
LOOP
FETCH c_Student INTO v_pidm;
EXIT WHEN c_Student%notfound;
OPEN C_CLM_SCORES(v_pidm);
FETCH C_CLM_SCORES INTO v_CLM_TEST_SCORE;
CLOSE C_CLM_SCORES;
insert into some_table (CLM_TEST_SCORE)
values (v_CLM_TEST_SCORE);
END LOOP;
END;
/
However, that is still very inefficient as each iteration performs a SELECT and an INSERT and will generate log entries. You can make it much simpler and more efficient to rewrite the whole thing as a single SQL statement:
INSERT INTO some_table (clm_test_score)
SELECT ( select max(to_number(SORTEST_TEST_SCORE))
from SATURN.SORTEST s
where s.SORTEST_PIDM = c.pidm )
FROM SOSC.DW_ALL_COLLECTOR c;
db<>fiddle here

The code in the question is an advertisement for "Why should implicit cursors be used?". If you rewrite your code as below it becomes much easier to understand:
BEGIN
FOR rowStudent IN (select PIDM
from SOSC.DW_ALL_COLLECTOR
order by PIDM)
LOOP
FOR rowScores IN (select max(to_number(SORTEST_TEST_SCORE)) AS CLM_TEST_SCORE
from SATURN.SORTEST
where SORTEST_PIDM = rowStudent.PIDM)
LOOP
insert into some_table (CLM_TEST_SCORE)
values (rowScores.CLM_TEST_SCORE);
END LOOP; -- rowScores
END LOOP; -- rowStudent
END;
This eliminates all of the variables and cursor definitions, and all the code is right in front of you where you can see it at a glance.
If you wanted to tighten it up a bit further you could use a join to get down to just one cursor:
BEGIN
FOR rowStudent_scores IN (SELECT d.PIDM, MAX(TO_NUMBER(s.SORTEST_TEST_SCORE)) AS CLM_TEST_SCORE
FROM SOSC.DW_ALL_COLLECTOR d
INNER JOIN SATURN.SORTEST s
ON s.SORTEST_PIDM = d.PIDM
GROUP BY d.PIDM)
LOOP
insert into some_table (CLM_TEST_SCORE)
values (rowStudent_scores.CLM_TEST_SCORE);
END LOOP; -- rowStudent_scores
END;

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;
/

Errors when I create Oracle cursor

I am new to Oracle programming (started a month ago).
I've created a cursor to retrieve a value from a table 'CDF_LU' and then use the cursor to insert into another table 'test_1'. However there is an error when I run it.
Here is my code:
DECLARE
c_cdf_table CDF_LU.PROD_COLUMN_NAME%type;
-- create cursor.
CURSOR c_CDF_Table_Name IS
SELECT PROD_COLUMN_NAME
FROM CDF_LU
ORDER BY CDF;
-- create record.
c_cdf_table c_CDF_Table_Name%ROWTYPE;
BEGIN
OPEN c_CDF_Table_Name;
LOOP
FETCH c_CDF_Table_Name INTO c_cdf_table;
EXIT WHEN c_CDF_Table_Name%NOTFOUND;
-- insert to table_1.
INSERT into test_1
select A,B,C from table_1 where some_conditions
END LOOP;
CLOSE c_CDF_Table_Name;
END;
When I run this code, there are following errors:
In line 'FETCH c_CDF_Table_Name INTO c_cdf_table;', SQL statement ignored.
In line 'FETCH c_CDF_Table_Name INTO c_cdf_table;', at most one declaration for "C_CDF_TABLE" is permitted.
In line 'INSERT into test_1', SQL statement ignored.
I wrote the SQL codes above by strictly following the syntax of cursors, so I'm not sure where the problem is.
Could you please advise? Thank you!
The way you wanted to do it is possible (of course) when errors are fixed; something like this:
declare
-- cursor
cursor c_cdf_table_name is
select prod_column_name
from cdf_lu
order by cdf;
-- cursor variable
c_cdf_table c_cdf_table_name%rowtype;
begin
open c_cdf_table_name;
loop
fetch c_cdf_table_name into c_cdf_table;
exit when c_cdf_table_name%notfound;
insert into test1 (col1, col2, co3)
select a, b, c from table1
where d = c_cdf_table.prod_column_name;
end loop;
close c_cdf_table_name;
end;
/
However, there's a way shorter & simpler option - a cursor FOR loop. As you can see, you don't have to declare a cursor variable, open the cursor, fetch from it, take care about exiting the loop nor closing the cursor - Oracle does all that for you:
begin
for cur_r in (select prod_column_name
from cdf_lu
order by cdf)
loop
insert into test1 (col1, col2, co3)
select a, b, c from table1
where d = c_cdf_table.prod_column_name;
end loop;
end;
/

Procedure to update column from function

I have a problem.
I need to create a procedure that fetches or_id loop from table1.
This id from table1 is a parameter calling function that returns two values d1 and d2.
The values d1 and d2 must be updated in table1.
How to do it? The function works correctly but I do not know how to loop it.
CURSOR rec_cur IS
SELECT s.or_id from table1 s;
id number;
a_BASKET_ID varchar(20);
a_ORDER_ID varchar (20);
BEGIN open rec_cur; loop
fetch rec_cur into number;
EXIT WHEN rec_cur%NOTFOUND;
SELECT cut(v_param,1,'#') a_BASKET_ID,
cut(v_param,2,'#') a_ORDER_ID
FROM (SELECT function1(or_id) v_param FROM dual);
UPDATE table1 b
SET BASKET_ID = a_BASKETID,
ORDER_ID = a_ORDERTYPE
WHERE b.or_id = s.or_id;
END LOOP;
This is one of the ugliest codes I've recently seen. Please, for your own sake (as well as ours), learn how to properly format & indent code and make it easier to read and follow.
Furthermore, it is invalid - lacks in DECLARE, there's no END, you declared some variables (a_basket_id, a_order_id) but used another ones (a_basketid, a_ordertype) ... quite a mess.
As of your question: as far as I understood, everything can be done in a single UPDATE statement, no PL/SQL is needed:
update table1 set
basket_id = cut(function1(or_id), 1, '#'),
order_id = cut(function1(or_id), 2, '#');
If you insist on PL/SQL, have a look at this: I've used cursor FOR loop as it is simpler to maintain than explicit cursor (as you don't have to create cursor variable(s), open the cursor, worry about exiting the loop, close the cursor - Oracle does it for you). Although you don't need local variables at all (nor PL/SQL, as I've already said), I let them be.
declare
a_basket_id table1.basket_id%type;
a_order_id table1.order_id%type;
begin
for cur_r in (select or_id from table1) loop
a_basket_id := cut(function1(cur_r.or_id), 1, '#');
a_order_id := cut(function1(cur_r.or_id), 2, '#');
update table1 set
basket_id = a_basket_id
order_id = a_order_id
where or_id = cur_r.or_id;
end loop;
end;

How to have my Oracle PL/SQL block update all entries related to the row_ids referenced by my cursor

I currently have a Oracle stored procedure that is taking data from tables. The problem is I am aggregating /grouping , and I don't want to grab the IDs otherwise that will throw the grouping off. I want to update a column called 'correlated_flag_id' to '1' (done) in the value table after ive inserted the aggregated/grouped result set. I only want to grab IDs that are correlated to the
values that my first cursor grabbed to derive the results. Below is my attempt (which I don't think is correct):
Create or Replace PROCEDURE PROC is
CURSOR c1 is
select sum(v.value_tx) as sum_of_values
, max(v.create_dt) as latest_create_dt
, v.data_date
from value v
group by v.data_date, max(v.create_dt)
BEGIN
Open c1;
LOOP
Fetch c1 into l_var;
insert into value (value_id, value_tx, create_dt, data_date)
values (null, l_var.sum_of_values, l_var.latest_create_dt, l_var.data_Date);
END LOOP;
Close c1;
commit;
--- the bottom is not correct, but i've reached a roadblock
Update value
set correlated_flag_id = 777
where value_id in (select v.value_id from value where trunc(create_dt) <> trunc(sysdate)) (???));
commit;
END PROC;
Thanks in advance and please let me know if there is any more details that I need to provide.
cursor's select is kind of wrong; why are you grouping by a MAX function? It isn't allowed here
switch to cursor FOR loop as it is easier to maintain. You don't have to open a cursor, fetch, exit the loop (which you did not do at all), close the cursor
I'm not sure what VALUE_930 table is doing here, you never mentioned it
your words say "update correlated ID to 1", while code says "update it to 777"
don't commit in a procedure; let caller decide whether it should be done
I'd suggest you to either use a tool which offers code formatting, or format it yourself. Your procedure is not a result of a flood, so don't treat it that way
Finally, here's a suggestion which might (or might not) work as we don't have your tables nor data, but - at least - looks decent.
create or replace procedure proc is
begin
for cur_r in (select v.data_date,
sum(v.value_tx) as sum_of_values,
max(v.create_dt) as latest_create_dt
from value v
group by v.data_date)
loop
insert into value (value_id, value_tx, create_dt, data_date)
values (null, cur_r.sum_of_values, cur_r.latest_create_dt, cur_r.data_date);
update value set
correlated_flag_id = 777
where data_date = cur_r.data_date;
end loop;
end proc;
/

PLSQL IMPLICIT CURSOR No Data Found After CURSOR

I have a Main cursor that is working fine.
declare
v_firm_id number;
amount number;
v_total_sum TABLE_TEMP.TOTAL_SUM%TYPE;
CURSOR MT_CURSOR IS
SELECT firm_id FROM t_firm;
BEGIN
OPEN MT_CURSOR;
LOOP
FETCH MT_CURSOR INTO v_firm_id;
EXIT WHEN MT_CURSOR%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(to_char(sysdate, 'mi:ss') ||'--- '|| v_firm_id)
INSERT INTO TABLE_TEMP(TOTAL_SUM) VALUES(v_firm_id)
COMMIT;
END LOOP;
DBMS_LOCK.SLEEP(20);
BEGIN
FOR loop_emp IN
(SELECT TOTAL_SUM INTO v_total_sum FROM TABLE_TEMP)
LOOP
dbms_output.put_line(to_char(sysdate, 'mi:ss') ||'--- '|| v_total_sum || '-TEST--');
END LOOP loop_emp;
END;
end;
Everything Works fine except dbms_output.put_line(v_total_sum || '---');
I do not get any data there. I get the correct number of rows. which it inserted.
The problem is the cursor FOR loop has a redundant into clause which it appears the compiler silently ignores, and so v_total_sum is never used.
Try this:
begin
for r in (
select firm_id from t_firm
)
loop
insert into table_temp (total_sum) values (r.firm_id);
end loop;
dbms_lock.sleep(20);
for r in (
select total_sum from table_temp
)
loop
dbms_output.put_line(r.total_sum || '---');
end loop;
commit;
end;
If this had been a stored procedure rather than an anonymous block and you had PL/SQL compiler warnings enabled with alter session set plsql_warnings = 'ENABLE:ALL'; (or the equivalent preference setting in your IDE) then you would have seen:
PLW-05016: INTO clause should not be specified here
I also moved the commit to the end so you only commit once.
To summarise the comments below, the Cursor FOR loop construction declares, opens, fetches and closes the cursor for you, and is potentially faster because it fetches in batches of 100 (or similar - I haven't tested in recent versions). Simpler code has less chance of bugs and is easier to maintain in the future, for example if you need to add a column to the cursor.
Note the original version had:
for loop_emp in (...)
loop
...
end loop loop_emp;
This is misleading because loop_emp is the name of the record, not the cursor or the loop. The compiler is ignoring the text after end loop although really it should at least warn you. If you wanted to name the loop, you would use a label like <<LOOP_EMP>> above it. (I always name my loop records r, similar to the i you often see used in numeric loops.)

Resources