Oracle 11g PL/SQL cursor,intersect query - oracle

I have the following query
select distinct name from table1
intersect
select distinct name from table2;
I load the resultset into a cursor in a PL/SQL procedure, like so:
cursor c1 is (select distinct name from table1
intersect
select distinct name from table2);
For some reason the last value in the resultset is duplicated in the cursor. This does not happen when running the query by itself. Any ideas why this is happening?
Code for the loop:
var table.col%type;
BEGIN
OPEN c1;
LOOP
BEGIN
exit when c1%NOTFOUND;
FETCH c1 into var;
INSERT INTO table values (col1, var);
commit;
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
CONTINUE;
END;
END LOOP;
END;

EXIT WHEN .. clause should come after FETCH.
Let's say, your cursor had 10 records to return. Before the first fetch is fired, %NOTFOUND evaluated to NULL, and the processing moves to the next statement, which is FETCH in your case. Now, if we fast forward to the 10th iteration, FETCH will get the 10th record, and the same is inserted into your destination table. The loop will move ahead, and since your EXIT WHEN %NOTFOUND is before fetch, it still has the value from last iteration, and it lets the control move ahead, and there, fetch will not be able to get any record, but the code will anyhow insert the last row it retrieved in 10th iteration. Now in the next loop, c1%NOTFOUND will be evaluated to TRUE and the loop will terminate
var table.col%type;
BEGIN
OPEN c1;
LOOP
BEGIN
FETCH c1 into var;
exit when c1%NOTFOUND;
INSERT INTO table values (col1, var);
commit;
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
CONTINUE;
END;
END LOOP;
END;

This is typical of the problems you see with explicit cursors.
Your first choice should be a single SQL statement, nothing more.
If you had to use a cursor, you should be using an implicit one wherever possible.

Related

PL/SQL Cursor whose query is based on a variable

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;

why if block not working within a procedure while comparing two columns of two tables

I need to check whether std_id is present same as in students table if this matches with std_id in std_grace_marks then I need to add that grace_marks of that std_id with marks column in students table.
I have created a procedure and created two cursors to fetch records in loop and I wrote if condition to check whether std_id matches if it is then I am adding marks with grace_marks but if condition is not working here... so please can anyone tell where I am going wrong
My code:
create or replace procedure std_info
IS
CURSOR stdcur IS SELECT std_id,std_name, marks,mark_status FROM students;
CURSOR gracer IS SELECT std_id,grace_marks from student_grace_marks;
myvar stdcur%ROWTYPE;
mycur gracer%ROWTYPE;
BEGIN
OPEN stdcur;
OPEN gracer;
LOOP
FETCH stdcur INTO myvar;
FETCH gracer INTO mycur;
EXIT WHEN stdcur%NOTFOUND;
DBMS_OUTPUT.PUT_LINE( myvar.std_id || ' '|| myvar.std_name||' '||myvar.marks );
if(myvar.std_id=mycur.std_id) then
update students set marks=myvar.marks+mycur.grace_marks;
end if;
END LOOP;
CLOSE stdcur;
CLOSE gracer;
END;
I'm not sure this will ever work. You're fetching a row of each resultset in a row variable, assuming that row 1 for the first resultset will match the std_id of row 1 for the 2nd resultset. There are 2 things wrong with this assumption
There is no ORDER BY clause in the statements, so std_id for the firs row of cursor stdcur could be 10 while std_id for the first row of cursor gracer could be 99. Oracle does not guarantee the order of the a resultset unless an ORDER BY clause is included in the statement.
If table student_grace_marks has more than 1 row, or no rows for a std_id, it will start failing from that row onwards since the count will no longer match.
One solution is to use explicit cursor for loops:
DECLARE
BEGIN
FOR r_student IN (SELECT * FROM students) LOOP
FOR r_student_gm IN (SELECT * FROM student_grace_marks WHERE std_id = r_student.std_id) LOOP
UPDATE students SET marks = marks + r_student_gm.grace_marks WHERE std_id = r_student.std_id;
END LOOP;
END LOOP;
END;
/
.. or .. if you want to use explicit cursors:
In this case the CURSOR gracer will have a where clause to only select the relevant row(s) for that particular student.
Note that I fixed some errors as well
DECLARE
--declare variable to be used in the where clause of the select for cursor gracer.
l_std_id students.std_id%TYPE;
CURSOR stdcur IS SELECT std_id,std_name, marks FROM students;
CURSOR gracer IS SELECT std_id,grace_marks from student_grace_marks WHERE std_id = l_std_id;
myvar stdcur%ROWTYPE;
mycur gracer%ROWTYPE;
BEGIN
OPEN stdcur;
LOOP
FETCH stdcur INTO myvar;
EXIT WHEN stdcur%NOTFOUND;
l_std_id := myvar.std_id;
OPEN gracer;
LOOP
FETCH gracer INTO mycur;
EXIT WHEN gracer%NOTFOUND;
-- use marks, not myvar.marks. If there is >1 record in student_grace_marks it will only add the last value.
-- add the where clause or every update will update all rows and every grace_marks will be added for every student in the table.
UPDATE students set marks= marks+mycur.grace_marks WHERE std_id = l_std_id;
END LOOP;
CLOSE gracer;
END LOOP;
CLOSE stdcur;
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;
/

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

How can I find the number of records in an Oracle PL/SQL cursor?

Here's my cursor:
CURSOR C1 IS SELECT * FROM MY_TABLE WHERE SALARY < 50000 FOR UPDATE;
I immediately open the cursor in order to lock these records for the duration of my procedure.
I want to raise an application error in the event that there are < 2 records in my cursor. Using the C1%ROWCOUNT property fails because it only counts the number which have been fetched thus far.
What is the best pattern for this use case? Do I need to create a dummy MY_TABLE%ROWTYPE variable and then loop through the cursor to fetch them out and keep a count, or is there a simpler way? If this is the way to do it, will fetching all rows in my cursor implicitly close it, thus unlocking those rows, or will it stay open until I explicitly close it even if I've fetched them all?
I need to make sure the cursor stays open for a variety of other tasks beyond this count.
NB: i just reread you question.. and you want to fail if there is ONLY 1 record..
i'll post a new update in a moment..
lets start here..
From Oracle® Database PL/SQL User's Guide and Reference
10g Release 2 (10.2)
Part Number B14261-01
reference
All rows are locked when you open the cursor, not as they are fetched. The rows are unlocked when you commit or roll back the transaction. Since the rows are no longer locked, you cannot fetch from a FOR UPDATE cursor after a commit.
so you do not need to worry about the records unlocking.
so try this..
declare
CURSOR mytable_cur IS SELECT * FROM MY_TABLE WHERE SALARY < 50000 FOR UPDATE;
TYPE mytable_tt IS TABLE OF mytable_cur %ROWTYPE
INDEX BY PLS_INTEGER;
l_my_table_recs mytable_tt;
l_totalcount NUMBER;
begin
OPEN mytable_cur ;
l_totalcount := 0;
LOOP
FETCH mytable_cur
BULK COLLECT INTO l_my_table_recs LIMIT 100;
l_totalcount := l_totalcount + NVL(l_my_table_recs.COUNT,0);
--this is the check for only 1 row..
EXIT WHEN l_totalcount < 2;
FOR indx IN 1 .. l_my_table_recs.COUNT
LOOP
--process each record.. via l_my_table_recs (indx)
END LOOP;
EXIT WHEN mytable_cur%NOTFOUND;
END LOOP;
CLOSE mytable_cur ;
end;
ALTERNATE ANSWER
I read you answer backwards to start and thought you wanted to exit if there was MORE then 1 row.. not exactly one.. so here is my previous answer.
2 simple ways to check for ONLY 1 record.
Option 1 - Explicit Fetchs
declare
CURSOR C1 IS SELECT * FROM MY_TABLE WHERE SALARY < 50000 FOR UPDATE;
l_my_table_rec C1%rowtype;
l_my_table_rec2 C1%rowtype;
begin
open C1;
fetch c1 into l_my_table_rec;
if c1%NOTFOUND then
--no data found
end if;
fetch c1 into l_my_table_rec2;
if c1%FOUND THEN
--i have more then 1 row
end if;
close c1;
-- processing logic
end;
I hope you get the idea.
Option 2 - Exception Catching
declare
CURSOR C1 IS SELECT * FROM MY_TABLE WHERE SALARY < 50000 FOR UPDATE;
l_my_table_rec C1%rowtype;
begin
begin
select *
from my_table
into l_my_table_rec
where salary < 50000
for update;
exception
when too_many_rows then
-- handle the exception where more than one row is returned
when no_data_found then
-- handle the exception where no rows are returned
when others then raise;
end;
-- processing logic
end;
Additionally
Remember: with an explicit cursor.. you can %TYPE your variable off the cursor record rather then the original table.
this is especially useful when you have joins in your query.
Also, rememebr you can update the rows in the table with an
UPDATE table_name
SET set_clause
WHERE CURRENT OF cursor_name;
type statement, but I that will only work if you haven't 'fetched' the 2nd row..
for some more information about cursor FOR loops.. try
Here
If you're looking to fail whenver you have more than 1 row returned, try this:
declare
l_my_table_rec my_table%rowtype;
begin
begin
select *
from my_table
into l_my_table_rec
where salary < 50000
for update;
exception
when too_many_rows then
-- handle the exception where more than one row is returned
when no_data_found then
-- handle the exception where no rows are returned
when others then raise;
end;
-- processing logic
end;
If this is the way to do it, will
fetching all rows in my cursor
implicitly close it, thus unlocking
those rows
The locks will be present for the duration of the transaction (ie until you do a commit or rollback) irrespective of when (or whether) you close the cursor.
I'd go for
declare
CURSOR C1 IS SELECT * FROM MY_TABLE WHERE SALARY < 50000 FOR UPDATE;;
v_1 c1%rowtype;
v_cnt number;
begin
open c_1;
select count(*) into v_cnt FROM MY_TABLE WHERE SALARY < 50000 and rownum < 3;
if v_cnt < 2 then
raise_application_error(-20001,'...');
end if;
--other processing
close c_1;
end;
There's a very small chance that, between the time the cursor is opened (locking rows) and the select count, someone inserts one or more rows into the table with a salary under 50000. In that case the application error would be raised but the cursor would only process the rows present when the cursor was opened. If that is a worry, at the end do another check on c_1%rowcount and, if that problem was experienced, you'd need to rollback to a savepoint.
Create a savepoint before you iterate through the cursor and then use a partial rollback when you find there are < 2 records returned.
You can start transaction and check if SELECT COUNT(*) MY_TABLE WHERE SALARY < 50000 greater than 1.

Resources