Using function inside a cursor (by using a variable) - sql

I would like to confirm the correct use of the following:
1) Use a global variable to get return values from a function only once
(since my function will be returning some Sequence values)
2) Use that variable inside a cursor multiple times
3) All of these will be inside a procedure
Below shows a sample.
CREATE OR REPLACE Procedure insert_myTable is
--declare variables for insert
v_firstNO VARCHAR2(10);
v_secondNO VARCHAR2(6);
--declare variable to store the sequence number
var_ASeqno varchar2(6);
-- Validation
v_check VARCHAR2 (10 Byte);
v_table_name varchar2(50):='myTable';
cursor c1 is
select distinct firstNO,
secondNO
from (SELECT hdr.someNum firstNO,
-- using variable to assign the sequence no
var_ASeqno secondNO
FROM someOtherTable hdr
WHERE -- some condition
union
SELECT hdr.someNum firstNO,
-- using variable to assign the sequence no
var_ASeqno secondNO
FROM someOtherTable hdr
WHERE -- some other conditions
union
SELECT hdr.someNum firstNO,
-- using variable to assign the sequence no
var_ASeqno secondNO
FROM someOtherTable hdr
WHERE -- some other other conditions
begin
if c1%isopen then
close c1;
end if;
v_check:=null;
FOR i IN c1 LOOP
--assign variables for insert
v_firstNO := i.firstNO ;
v_secondNO := i.secondNO ;
begin
-- calling the Function aSeqNoFunc and assign the
--Sequence Number into the variable var_ASeqno
var_ASeqno := aSeqNoFunc();
select firstNO
into v_check
from myTable a
where firstNO = i.firstNO
and secondNO =i.secondNO;
exception
when no_data_found then
--insert into target table
INSERT INTO myTable (firstNO, secondNO)
values (v_firstNO, v_secondNO);
end ;
end loop;
end;
As can be seen, the function 'aSeqNoFunc' is called before the Insert near the end. The values are assigned to the variable 'var_ApmSeqno' which in turn is used three times inside the cursor.
Thank you.

A few suggestions:
You have an END; statement after the declaration of cursor c1 which doesn't match up with anything. You should remove this from your code.
There's no need to check to see if the cursor is open when you enter the procedure. It won't be. Even better, don't use an explicit cursor declaration - use a cursor FOR-loop.
Use UNION ALL instead of UNION unless you know what the difference between the two is. (And go read up on that. 99.9% of the time you want UNION ALL...).
However, as it appears that all the rows are being selected from the same table you may be able to eliminate the UNION's altogether, as shown below.
There's no benefit to assigning NULL to a variable at the beginning of a function. Variables are initialized to NULL if there's no other explicit initialization value given.
IMO there's no benefit to having a function which returns the next value from a sequence. It just makes understanding the code more difficult. Get rid of FUNCTION aSeqNoFunc and just invoke SOME_SEQUENCE.NEXTVAL where appropriate - so in the above I suggest you use var_ASeqno := SOME_SEQUENCE.NEXTVAL.
You need to assign the value to var_ASeqno before cursor c1 is opened. As written abovevar_ASeqno will be null at the time the cursor is opened, so the cursor will probably not return what you expect. But more to the point I don't see that there's any reason to have the cursor return the value of var_ASeqno. Just use the value of var_ASeqno in your INSERT statements or wherever else they're needed.
Use a MERGE statement to insert data if it doesn't already exist. This avoids the awkward "SELECT...catch the NO_DATA_FOUND exception...INSERT in the exception handler" logic.
And as #boneist points out in her comment, by the time we've gone this far there's really no point to the cursor. You might as well just use the MERGE statement to perform the INSERTs without using a cursor.
So I'd try rewriting this procedure as:
CREATE OR REPLACE Procedure insert_myTable is
begin
MERGE INTO MYTABLE m
USING (SELECT FIRSTNO,
SOME_SEQUENCE.NEXTVAL AS SECONDNO
FROM (SELECT DISTINCT hdr.someNum AS FIRSTNO
FROM someOtherTable hdr
WHERE (/* some condition */)
OR (/* some other conditions */)
OR (/* some other other conditions */))) d
ON d.FIRSTNO = m.FIRSTNO AND
d.SECONDNO = m.SECONDNO
WHEN NOT MATCHED THEN INSERT (FIRSTNO, SECONDNO)
VALUES (d.FIRSTNO, d.SECONDNO);
end INSERT_MYTABLE;

Taking into account the fact that you want all the rows being inserted to have the same sequence number assigned, I think you could probably rewrite your procedure to something like:
create or replace procedure insert_mytable
is
v_seq_no number;
begin
v_seq_no := somesequence.nextval;
merge into mytable tgt
using (select firstno,
v_seq_no secondno
from (select hdr.somenum firstno
from someothertable1 hdr
where -- some condition
union
select hdr.somenum firstno
from someothertable2 hdr
where -- some other conditions
union
select hdr.somenum firstno
from someothertable3 hdr
where -- some other other conditions
)
) src
on (tgt.firstno = src.firstno and tgt.secondno = src.secondno)
when not matched then
insert (tgt.firstno, tgt.secondno)
values (src.firstno, src.secondno);
end insert_mytable;
/
If this doesn't match with what you're trying to do, please edit your question to provide more information on what the aim of the procedure is. Example input and output data would be appreciated, so that we have a better idea of what you're wanting (since we can't see your table structures, data, etc).
ETA: Info on performance considerations between set-based and row-by-row approaches.
Here's a simple script to insert of a million rows, both row-by-row and as a single insert statement:
create table test (col1 number,
col2 number);
set timing on;
-- row-by-row (aka slow-by-slow) approach
begin
for rec in (select level col1, level * 10 col2
from dual
connect by level <= 1000000)
loop
insert into test (col1, col2)
values (rec.col1, rec.col2);
end loop;
end;
/
commit;
truncate table test;
-- set based approach (keeping in an anonymous block for comparison purposes)
begin
insert into test (col1, col2)
select level, level*10
from dual
connect by level <= 1000000;
end;
/
commit;
drop table test;
Here's the output I get, when I run the above in Toad:
Table created.
PL/SQL procedure successfully completed.
Elapsed: 00:00:21.87
Commit complete.
Elapsed: 00:00:01.03
Table truncated.
Elapsed: 00:00:00.22
PL/SQL procedure successfully completed.
Elapsed: 00:00:01.96
Commit complete.
Elapsed: 00:00:00.03
Table dropped.
Elapsed: 00:00:00.18
Do you see the elapsed time of 21 seconds for the row-by-row approach, and 2 seconds for the set-based approach? Massive difference in performance, don't you agree? If that's not reason to consider writing your code as set based in the first instance, then I don't know what else will convince you/your boss!

Related

How to write a procedure to display the contents of a table in sql

I have a created a procedure as
create or replace procedure availability(num in number) as
begin
delete from vehicle_count;
insert into vehicle_count from select engine_no,count(engine_no)
from vehicle
where engine_no = num
group by engine_no;
end;
/
The procedure was created successfully but now i have to write a separate query to view the contents of vehicle_count as
select * from vehicle_count;
I tried inserting the select statement into the procedure after insertion but it showed a error stating "an INTO clause is expected in the select statement".
How can I create procedure to select the required contents and display it in a single execute statement?
Table schema
vehicle(vehicle_no,engine_no,offence_count,license_status,owner_id);
vehicle_count(engine_no,engine_count);
Check this (MS SQL SERVER)-
create or alter procedure availability(#num as int) as
begin
delete from vehicle_count;
insert into vehicle_count
output inserted.engine_no,inserted.count_engine_no
select engine_no,count(engine_no) as count_engine_no
from vehicle
where engine_no=#num
group by engine_no;
end;
If you want to use a SELECT into a PL/SQL block you should use either a SELECT INTO or a loop (if you want to print more rows).
You could use something like this:
BEGIN
SELECT engine_no, engine_count
INTO v_engine, v_count
FROM vehicle_count
WHERE engine_no = num;
EXCEPTION
WHEN NO_DATA_FOUND THEN
v_engine := NULL;
v_count := NULL;
END;
v_engine and v_count are two variables. You can declare them in your procedure, and they will contain the values you want to print.
You said that the procedure you wrote (actually, you posted here) compiled successfully. Well, sorry to inform you - that's not true. This is not a valid syntax:
insert into vehicle_count from select engine_no,count(engine_no)
----
from? Here?
Consider posting true information.
As of your question (if we suppose that that INSERT actually inserted something into a table):
at the beginning, you delete everything from the table
as SELECT counts number of rows that share the same ENGINE_NO (which is equal to the parameter NUM value), INSERT inserts none (if there's no such NUM value in the table) or maximum 1 row (because of aggregation)
therefore, if you want to display what's in the table, all you need is a single SELECT ... INTO statement whose result is displayed with a simple DBMS_OUTPUT.PUT_LINE which will be OK if you're doing it interactively (in SQL*Plus, SQL Developer, TOAD and smilar tools). Regarding table description, I'd say that ENGINE_NO should be a primary key (i.e. that not more than a single row with that ENGINE_NO value can exist in a table).
create or replace procedure availability (num in number) as
l_engine_no vehicle_count.engine_no%type;
l_engine_count vehicle_count.engine_count%type;
begin
delete from vehicle_count;
insert into vehicle_count (engine_no, engine_count)
select engine_no, count(engine_no)
from vehicle
where engine_no = num
group by engine_no;
-- This query shouldn't return TOO-MANY-ROWS if ENGINE_NO is a primary key.
-- However, it might return NO-DATA-FOUND if there's no such NUM there, so you should handle it
select engine_no, engine_count
into l_engine_no, l_engine_count
from vehicle_count
where engine_no = num;
dbms_output.put_line(l_engine_no ||': '|| l_engine_count);
exception
when no_data_found then
dbms_output.put_line('Nothing found for ENGINE_NO = ' || num);
end;
/
There are numerous alternatives to that (people who posted their answers/comments before this one mentioned some of those), and the final result you'd be satisfied with depends on where you want to display that information.

How to store multiple rows in a variable in pl/sql function?

I'm writing a pl/sql function. I need to select multiple rows from select statement:
SELECT pel.ceid
FROM pa_exception_list pel
WHERE trunc(pel.creation_date) >= trunc(SYSDATE-7)
if i use:
SELECT pel.ceid
INTO v_ceid
it only stores one value, but i need to store all values that this select returns. Given that this is a function i can't just use simple select because i get error, "INTO - is expected."
You can use a record type to do that. The below example should work for you
DECLARE
TYPE v_array_type IS VARRAY (10) OF NUMBER;
var v_array_type;
BEGIN
SELECT x
BULK COLLECT INTO
var
FROM (
SELECT 1 x
FROM dual
UNION
SELECT 2 x
FROM dual
UNION
SELECT 3 x
FROM dual
);
FOR I IN 1..3 LOOP
dbms_output.put_line(var(I));
END LOOP;
END;
So in your case, it would be something like
select pel.ceid
BULK COLLECT INTO <variable which you create>
from pa_exception_list
where trunc(pel.creation_Date) >= trunc(sysdate-7);
If you really need to store multiple rows, check BULK COLLECT INTO statement and examples. But maybe FOR cursor LOOP and row-by-row processing would be better decision.
You may store all in a rowtype parameter and show whichever column you want to show( assuming ceid is your primary key column, col1 & 2 are some other columns of your table ) :
SQL> set serveroutput on;
SQL> declare
l_exp pa_exception_list%rowtype;
begin
for c in ( select *
from pa_exception_list pel
where trunc(pel.creation_date) >= trunc(SYSDATE-7)
) -- to select multiple rows
loop
select *
into l_exp
from pa_exception_list
where ceid = c.ceid; -- to render only one row( ceid is primary key )
dbms_output.put_line(l_exp.ceid||' - '||l_exp.col1||' - '||l_exp.col2); -- to show the results
end loop;
end;
/
SET SERVEROUTPUT ON
BEGIN
FOR rec IN (
--an implicit cursor is created here
SELECT pel.ceid AS ceid
FROM pa_exception_list pel
WHERE trunc(pel.creation_date) >= trunc(SYSDATE-7)
)
LOOP
dbms_output.put_line(rec.ceid);
END LOOP;
END;
/
Notes from here:
In this case, the cursor FOR LOOP declares, opens, fetches from, and
closes an implicit cursor. However, the implicit cursor is internal;
therefore, you cannot reference it.
Note that Oracle Database automatically optimizes a cursor FOR LOOP to
work similarly to a BULK COLLECT query. Although your code looks as if
it fetched one row at a time, Oracle Database fetches multiple rows at
a time and allows you to process each row individually.

How to insert one row at a time in SQL using cursor

Hi i am using the below PLSQL script to insert rows in new table new_table.
set serveroutput on SIZE 1000000;
DECLARE
CURSOR get_record IS
SELECT * from cycle_table ;
BEGIN
FOR rec IN get_record
LOOP
DBMS_OUTPUT.put_line('Inserting Record into new_table..');
EXECUTE IMMEDIATE('insert into new_table
select cycle_code,cycle_instance,cycle_start_date,cycle_end_date
from cycle_table');
END LOOP;
COMMIT;
END;
/
Now the table cycle_table consist only 4 rows. The loop runs only four times beacuse its printing 'Inserting Record into new_table..' 4 times only.
But when i see the new_table it consist 16 rows. Which means everytime the loop iterates it insert all the 4 rows and thus total 16 rows.
What i want is that it insert single row at a time.
So that i can perform other actions on that row also. Like if the row already exist, insert in some other table or anything.
Please suggest what can I do here? I am using SQL developer on oracle 10g
Thanks in advance
It is very simple:
set serveroutput on SIZE 1000000;
DECLARE
BEGIN
FOR rec in (select * from cycle_table)
LOOP
DBMS_OUTPUT.put_line('Inserting Record into new_table..');
insert into new_table (cycle_code,
cycle_instance,
cycle_start_date,
cycle_end_date)
values (rec.cycle_code,
rec.cycle_instance,
rec.cycle_start_date,
rec.cycle_end_date);
END LOOP;
COMMIT;
END;
/
I would discourage this approach, though, as you could run into a performance issue if there is a large number of records. You have only four, so it's fine.
The reason I'm against this is that there is context switching involved between Oracle's PL/SQL engine and SQL engine. I'd suggest you do an insert into .... select... or use a forall instead, as these are the least resource-consuming approaches.
A more efficient way is to eliminate all of the looping, and allow the SQL to handle everything. Here is my suggestion:
BEGIN
-- Handle matches first, because after you handle non-matches, everything matches
INSERT INTO match_table (cycle_code, cycle_instance, cycle_start_date
, cycle_end_date)
SELECT cycle_table.cycle_code, cycle_table.cycle_instance, cycle_table.cycle_start_date
, cycle_table.cycle_end_date
FROM cycle_table INNER JOIN new_table ON (new_table.cycle_code = cycle_table.cycle_code);
-- Single insert to insert all non matching records
INSERT INTO new_table (cycle_code, cycle_instance, cycle_start_date
, cycle_end_date)
SELECT cycle_code, cycle_instance, cycle_start_date
, cycle_end_date
FROM cycle_table
WHERE NOT EXISTS
(SELECT NULL
FROM new_table
WHERE new_table.cycle_code = cycle_table.cycle_code);
COMMIT;
END;

PLS-00436: implementation restriction: cannot reference fields of BULK In-BIND table of records

I get this error when I call the procedure 'archive_things' which in turns gives the error at INSERT INTO deleted_part_things
(id, part_id, file_name, file_type, thing, editable)
what does this mean?
PROCEDURE archive_things ( p_part_id IN NUMBER )
IS
thing_list bean_list;
BEGIN
thing_list := get_thingss_info(p_part_id);
insert_deleted_things(thing_list);
END archive_things;
FUNCTION get_things_info ( p_part_id IN NUMBER)
RETURN bean_list
IS
attachment_list bean_list;
BEGIN
SELECT file_thing_bean (id, hot_part_id, file_name, file_type, thing, editable)
BULK COLLECT INTO thing_list
FROM part_things
WHERE part_id =hot_part_id;
RETURN thing_list;
END get_things_info;
PROCEDURE insert_deleted_things( p_bean_list IN bean_list )
IS BEGIN
FORALL x IN INDICES OF p_bean_list
INSERT INTO deleted_part_things
(id, part_id, file_name, file_type, thing, auditable) <<<<< ERROR HERE!!!!!
VALUES
( p_bean_list(x).id, p_bean_list(x).parent_id, p_bean_list(x).file_name, p_bean_list(x).file_type,
p_bean_list(x).thing, p_bean_list(x).editable
);
END insert_deleted_things;
Two points:
In your last question you mentioned that you're running Oracle 10g. As Ollie pointed out in his answer you can't use the method you're using until 11g.
Why are you creating two procedures and a function? This could easily be one procedure. As the initial procedure you're calling calls the other two you gain nothing by splitting it and make it much more complicated.
PLS-00436 is a restriction prior to 11G whereby you're not able to reference columns in a rowtype in a forall. There are a few ways round it:
One, as Ollie suggested is to have the same number of columns in your table as in your bulk collect. This isn't always possible.
Tom Kyte suggests set operations. The restriction on this is the size of the bulk collect you're doing. If it's bigger than the amount of undo you're going to have problems. Also if you want to do something else with the data then you have to do that separately.
The last option ( that I know of I'm sure there are more ) is to collect your records into individual types rather than a rowtype as per the following. The downside of this is that it may not be as quick as Tom's method and it's by no mean as clear as Ollie's.
I've just noted Sathya's method, which would also work but requires a lot of SQL to be executed.
PROCEDURE archive_things ( p_part_id IN NUMBER ) IS
CURSOR c_get_all ( Cpart_id char) is
SELECT file_attachment_bean (id, hot_part_id, file_name
, file_type, attachment, auditable)
FROM hot_part_attachments
WHERE hot_part_id = Cpart_id;
t_id bean_list.id%type;
t_hot_part_id bean_list.hot_part_id%type;
t_file_name bean_list.file_name%type;
t_file_type bean_list.file_type%type;
t_attachment bean_list.attachment%type;
t_auditable bean_list.auditable%type;
BEGIN
OPEN c_get_all(p_part_id);
FETCH c_get_all bulk collect into
t_id, t_hot_part_id, t_file_name, t_file_type, t_attachment, t_auditable;
LOOP
EXIT WHEN t_id.count = 0;
FORALL x IN t_id.first .. t_id.last
INSERT INTO deleted_hot_part_attachments (id, hot_part_id, file_name, file_type
, attachment, auditable)
VALUES ( t_id(x), t_hot_part_id(x), t_file_name(x), t_file_type(x)
, t_attachment(x), t_auditable(x) );
COMMIT; -- You may want to do this outside the procedure.
END LOOP;
CLOSE c_get_all;
END;
SET SERVEROUTPUT ON;
declare
cursor c1 IS select
c_code,c_name,c_language
from t_country;
TYPE c1_tab is table of t_country%rowtype;
c1_insert c1_tab;
l_count number:=0;
begin
open c1;
loop
fetch c1
bulk collect into c1_insert
limit 10000;
forall i in 1 .. c1_insert.count
insert into t_country values (c1_insert(i).cCode,c1_insert(i).cName,c1_insert(i).cLanguage);
commit;
exit when c1%notfounD;
end loop;
CLOSE C1;
end;

PLSQL Insert into with subquery and returning clause

I can't figure out the correct syntax for the following pseudo-sql:
INSERT INTO some_table
(column1,
column2)
SELECT col1_value,
col2_value
FROM other_table
WHERE ...
RETURNING id
INTO local_var;
I would like to insert something with the values of a subquery.
After inserting I need the new generated id.
Heres what oracle doc says:
Insert Statement
Returning Into
OK i think it is not possible only with the values clause...
Is there an alternative?
You cannot use the RETURNING BULK COLLECT from an INSERT.
This methodology can work with updates and deletes howeveer:
create table test2(aa number)
/
insert into test2(aa)
select level
from dual
connect by level<100
/
set serveroutput on
declare
TYPE t_Numbers IS TABLE OF test2.aa%TYPE
INDEX BY BINARY_INTEGER;
v_Numbers t_Numbers;
v_count number;
begin
update test2
set aa = aa+1
returning aa bulk collect into v_Numbers;
for v_count in 1..v_Numbers.count loop
dbms_output.put_line('v_Numbers := ' || v_Numbers(v_count));
end loop;
end;
You can get it to work with a few extra steps (doing a FORALL INSERT utilizing TREAT)
as described in this article:
returning with insert..select
T
to utilize the example they create and apply it to test2 test table
CREATE or replace TYPE ot AS OBJECT
( aa number);
/
CREATE TYPE ntt AS TABLE OF ot;
/
set serveroutput on
DECLARE
nt_passed_in ntt;
nt_to_return ntt;
FUNCTION pretend_parameter RETURN ntt IS
nt ntt;
BEGIN
SELECT ot(level) BULK COLLECT INTO nt
FROM dual
CONNECT BY level <= 5;
RETURN nt;
END pretend_parameter;
BEGIN
nt_passed_in := pretend_parameter();
FORALL i IN 1 .. nt_passed_in.COUNT
INSERT INTO test2(aa)
VALUES
( TREAT(nt_passed_in(i) AS ot).aa
)
RETURNING ot(aa)
BULK COLLECT INTO nt_to_return;
FOR i IN 1 .. nt_to_return.COUNT LOOP
DBMS_OUTPUT.PUT_LINE(
'Sequence value = [' || TO_CHAR(nt_to_return(i).aa) || ']'
);
END LOOP;
END;
/
Unfortunately that's not possible. RETURNING is only available for INSERT...VALUES statements. See this Oracle forum thread for a discussion of this subject.
You can't, BUT at least in Oracle 19c, you can specify a SELECT subquery inside the VALUES clause and so use RETURNING! This can be a good workaround, even if you may have to repeat the WHERE clause for every field:
INSERT INTO some_table
(column1,
column2)
VALUES((SELECT col1_value FROM other_table WHERE ...),
(SELECT col2_value FROM other_table WHERE ...))
RETURNING id
INTO local_var;
Because the insert is based on a select, Oracle is assuming that you are permitting a multiple-row insert with that syntax. In that case, look at the multiple row version of the returning clause document as it demonstrates that you need to use BULK COLLECT to retrieve the value from all inserted rows into a collection of results.
After all, if your insert query creates two rows - which returned value would it put into an single variable?
EDIT - Turns out this doesn't work as I had thought.... darn it!
This isn't as easy as you may think, and certainly not as easy as it is using MySQL. Oracle doesn't keep track of the last inserts, in a way that you can ping back the result.
You will need to work out some other way of doing this, you can do it using ROWID - but this has its pitfalls.
This link discussed the issue: http://forums.oracle.com/forums/thread.jspa?threadID=352627