Is the use of SELECT COUNT(*) before SELECT INTO slower than using Exceptions? - oracle

My last question got me thinking.
1)
SELECT COUNT(*) INTO count FROM foo WHERE bar = 123;
IF count > 0 THEN
SELECT a INTO var FROM foo WHERE bar = 123;
-- do stuff
ELSE
-- do other stuff
END IF;
2)
BEGIN
SELECT a INTO var FROM foo where bar = 123;
-- do stuff
EXCEPTION
WHEN no_data_found THEN
--do other stuff
END ;
I assume number 2 is faster because it requires one less trip to the database.
Is there any situation where 1 would be superior, that I am not considering?
EDIT: I'm going to let this question hang for a few more days, to gather some more votes on the answers, before answering it.

If you use exact queries from the question then 1st variant of course slower because it must count all records in table which satisfies criteria.
It must be writed as
SELECT COUNT(*) INTO row_count FROM foo WHERE bar = 123 and rownum = 1;
or
select 1 into row_count from dual where exists (select 1 from foo where bar = 123);
because checking for record existence is enough for your purpose.
Of course, both variants don't guarantee that someone else don't change something in foo between two statements, but it's not an issue if this check is a part of more complex scenario. Just think about situation when someone changed value of foo.a after selecting it's value into var while performing some actions which refers selected var value. So in complex scenarios better to handle such concurrency issues on application logic level.
To perform atomic operations is better to use single SQL statement.
Any of variants above requires 2 context switches between SQL and PL/SQL and 2 queries so performs slower then any variant described below in cases when row found in a table.
There are another variants to check existence of row without exception:
select max(a), count(1) into var, row_count
from foo
where bar = 123 and rownum < 3;
If row_count = 1 then only one row satisfies criteria.
Sometime it's enough to check only for existence because of unique constraint on the foo which guarantees that there are no duplicated bar values in foo. E.g. bar is primary key.
In such cases it's possible to simplify query:
select max(a) into var from foo where bar = 123;
if(var is not null) then
...
end if;
or use cursor for processing values:
for cValueA in (
select a from foo where bar = 123
) loop
...
end loop;
Next variant is from link, provided by #user272735 in his answer:
select
(select a from foo where bar = 123)
into var
from dual;
From my experience any variant without exception blocks in most cases faster then a variant with exceptions, but if number of executions of such block is low then better to use exception block with handling of no_data_found and too_many_rows exceptions to improve code readability.
Right point to choose to use exception or don't use it, is to ask a question "Is this situation are normal for application?". If row not found and it's a expected situation which can be handled (e.g. add new row or take data from another place and so on) is better to avoid exception. If it's unexpected and there are no way to fix a situation, then catch exception to customize error message, write it to event log and re-throw, or just don't catch it at all.
To compare performance just make a simple test case on you system whith both variants called many times and compare.
Say more, in 90 percent of applications this question is more theoretical than practical because there are a lot of another sources of performance issues which must be taken into account first.
Update
I reproduced example from this page at SQLFiddle site with a little corrections (link).
Results prove that variant with selecting from dual performs best: a little overhead when most of queries succeed and lowest performance degradation when number of missing rows raises.
Surprisingly variant with count() and two queries showed best result in case if all queries failed.
| FNAME | LOOP_COUNT | ALL_FAILED | ALL_SUCCEED | variant name |
----------------------------------------------------------------
| f1 | 2000 | 2.09 | 0.28 | exception |
| f2 | 2000 | 0.31 | 0.38 | cursor |
| f3 | 2000 | 0.26 | 0.27 | max() |
| f4 | 2000 | 0.23 | 0.28 | dual |
| f5 | 2000 | 0.22 | 0.58 | count() |
-- FNAME - tested function name
-- LOOP_COUNT - number of loops in one test run
-- ALL_FAILED - time in seconds if all tested rows missed from table
-- ALL_SUCCEED - time in seconds if all tested rows found in table
-- variant name - short name of tested variant
Below is a setup code for test environment and test script.
create table t_test(a, b)
as
select level,level from dual connect by level<=1e5
/
insert into t_test(a, b) select null, level from dual connect by level < 100
/
create unique index x_text on t_test(a)
/
create table timings(
fname varchar2(10),
loop_count number,
exec_time number
)
/
create table params(pstart number, pend number)
/
-- loop bounds
insert into params(pstart, pend) values(1, 2000)
/
-- f1 - exception handling
create or replace function f1(p in number) return number
as
res number;
begin
select b into res
from t_test t
where t.a=p and rownum = 1;
return res;
exception when no_data_found then
return null;
end;
/
-- f2 - cursor loop
create or replace function f2(p in number) return number
as
res number;
begin
for rec in (select b from t_test t where t.a=p and rownum = 1) loop
res:=rec.b;
end loop;
return res;
end;
/
-- f3 - max()
create or replace function f3(p in number) return number
as
res number;
begin
select max(b) into res
from t_test t
where t.a=p and rownum = 1;
return res;
end;
/
-- f4 - select as field in select from dual
create or replace function f4(p in number) return number
as
res number;
begin
select
(select b from t_test t where t.a=p and rownum = 1)
into res
from dual;
return res;
end;
/
-- f5 - check count() then get value
create or replace function f5(p in number) return number
as
res number;
cnt number;
begin
select count(*) into cnt
from t_test t where t.a=p and rownum = 1;
if(cnt = 1) then
select b into res from t_test t where t.a=p;
end if;
return res;
end;
/
Test script:
declare
v integer;
v_start integer;
v_end integer;
vStartTime number;
begin
select pstart, pend into v_start, v_end from params;
vStartTime := dbms_utility.get_cpu_time;
for i in v_start .. v_end loop
v:=f1(i);
end loop;
insert into timings(fname, loop_count, exec_time)
values ('f1', v_end-v_start+1, (dbms_utility.get_cpu_time - vStartTime)/100) ;
end;
/
declare
v integer;
v_start integer;
v_end integer;
vStartTime number;
begin
select pstart, pend into v_start, v_end from params;
vStartTime := dbms_utility.get_cpu_time;
for i in v_start .. v_end loop
v:=f2(i);
end loop;
insert into timings(fname, loop_count, exec_time)
values ('f2', v_end-v_start+1, (dbms_utility.get_cpu_time - vStartTime)/100) ;
end;
/
declare
v integer;
v_start integer;
v_end integer;
vStartTime number;
begin
select pstart, pend into v_start, v_end from params;
vStartTime := dbms_utility.get_cpu_time;
for i in v_start .. v_end loop
v:=f3(i);
end loop;
insert into timings(fname, loop_count, exec_time)
values ('f3', v_end-v_start+1, (dbms_utility.get_cpu_time - vStartTime)/100) ;
end;
/
declare
v integer;
v_start integer;
v_end integer;
vStartTime number;
begin
select pstart, pend into v_start, v_end from params;
vStartTime := dbms_utility.get_cpu_time;
for i in v_start .. v_end loop
v:=f4(i);
end loop;
insert into timings(fname, loop_count, exec_time)
values ('f4', v_end-v_start+1, (dbms_utility.get_cpu_time - vStartTime)/100) ;
end;
/
declare
v integer;
v_start integer;
v_end integer;
vStartTime number;
begin
select pstart, pend into v_start, v_end from params;
--v_end := v_start + trunc((v_end-v_start)*2/3);
vStartTime := dbms_utility.get_cpu_time;
for i in v_start .. v_end loop
v:=f5(i);
end loop;
insert into timings(fname, loop_count, exec_time)
values ('f5', v_end-v_start+1, (dbms_utility.get_cpu_time - vStartTime)/100) ;
end;
/
select * from timings order by fname
/

First see: Oracle PL/SQL - Are NO_DATA_FOUND Exceptions bad for stored procedure performance? that is essentially the same question than yours. And after that see About the performance of exception handling.
In both scenarios you should be prepared also to handle too_many_rows exception unless your database schema enforces uniqueness of bar.
This is PL/SQL so you're on a constant database trip - instead you should be afraid/aware of PL/SQL - SQL context switches. See also what Tom says:
But don't be afraid at all to invoke SQL from PLSQL - that is what PLSQL does best.
In first place you shouldn't be worried about the performance but the correctness of a program. In this regard my vote goes for scenario #2.

I'm not sure about faster, but I'd say (2) is clearly superior because you're not considering the case where someone issues DELETE FROM foo where bar='123' in between your statements in (1).

Such a situation I usually do like this:
DECALRE
CURSOR cur IS
SELECT a FROM foo where bar = 123;
BEGIN
OPEN cur;
FETCH cur INTO var;
IF cur%FOUND THEN
-- do stuff, maybe a LOOP if required
ELSE
--do other stuff
END;
END;
This has some benfits:
You read only one record from database, the rest you skip. Should be the fastest way of doing it in case you just have to know if number of rows > 1.
You don't handle a "normal" situation with an "exception" handler, some people consider this as "more beautiful" coding.

Related

06533. 00000 - "Subscript beyond count"

I have this plsql block that populates a table with information of two other tables and I have to use a variable array:
DECLARE nombre_grupo VARCHAR2(15);
direccion_tipo direccion;
persona_tipo persona;
personas_array personas := personas();
CURSOR departamento IS
SELECT * FROM departamentos;
CURSOR empleado IS
SELECT * FROM empleados, departamentos
WHERE empleados.dept_no = departamentos.dept_no;
i INTEGER;
BEGIN
FOR departamento IN (SELECT * FROM departamentos) LOOP
nombre_grupo := departamento.dnombre;
i := 1;
personas_array := personas();
FOR empleado IN (SELECT * FROM empleados WHERE dept_no = departamento.dept_no) LOOP
direccion_tipo := DIRECCION(departamento.loc, 'NULL', empleado.dir);
personas_array(i) := PERSONA(empleado.emp_no, empleado.apellido,
direccion_tipo, empleado.fecha_alt);
i := i + 1;
END LOOP;
INSERT INTO grupos VALUES (nombre_grupo, personas_array);
END LOOP;
END;
Here's the type personas:
CREATE OR REPLACE TYPE personas AS VARRAY(5) OF PERSONA
So when I execute that block and it reaches the personas_array(i) bit, it exits the execution with "subscript beyond count" error, no matter what value of i. What am I missing?
I've already deleted and created the type personas again, I've also tried creating the type inside the procedure, but it can't insert into the table
A few tips for a SQL beginner:
Don't learn 30 years old Oracle join syntax. Use modern ANSI join syntax, i.e.
SELECT *
FROM empleados
JOIN departamentos ON empleados.dept_no = departamentos.dept_no;
Your cursors are redundant. Either use
DECLARE
CURSOR cur_departamento IS
SELECT *
FROM departamentos;
BEGIN
FOR departamento IN cur_departamento LOOP
...
END LOOP;
END;
or
DECLARE
BEGIN
FOR departamento IN (SELECT * FROM departamentos) LOOP
...
END LOOP;
END;
You can also use this:
DECLARE
CURSOR cur_empleados(d IN EMPLEADOS.DEPT_NO%TYPE) IS
SELECT *
FROM EMPLEADOS
WHERE dept_no = d;
/*
-- Do not use this!
CURSOR cur_empleados(dept_no IN EMPLEADOS.DEPT_NO%TYPE) IS
SELECT *
FROM EMPLEADOS
WHERE EMPLEADOS.dept_no = dept_no; -> will return all rows
*/
BEGIN
FOR departamento IN (SELECT * FROM departamentos) LOOP
FOR empleado IN cur_empleados(departamento.dept_no) LOOP
...
END LOOP;
END LOOP;
END;
According to my feelings, VARRAYs are often part of student material but hardly used in real life.
Using string 'NULL' is most likely not want you want. Use literal NULL, i.e.
DIRECCION(departamento.loc, NULL, empleado.dir)
Type VARRAY(5) OF PERSONA defines a varray with maximum size of 5 elements. When you initialize it with personas_array := personas(); then the actual size is 0. You need to extend the varray.
You code may look like this:
DECLARE
nombre_grupo VARCHAR2(15);
direccion_tipo direccion;
persona_tipo persona;
personas_array personas;
i INTEGER;
BEGIN
FOR departamento IN (SELECT * FROM departamentos) LOOP
nombre_grupo := departamento.dnombre;
i := 1;
personas_array := personas();
FOR empleado IN (SELECT * FROM empleados WHERE dept_no = departamento.dept_no AND ROWNUM <= 5) LOOP
direccion_tipo := DIRECCION(departamento.loc, NULL, empleado.dir);
personas_array.extend();
personas_array(i) := PERSONA(empleado.emp_no, empleado.apellido, direccion_tipo, empleado.fecha_alt);
i := i + 1;
END LOOP;
INSERT INTO grupos VALUES (nombre_grupo, personas_array);
END LOOP;
END;
Just a note, such procedure would have rather low performance. The professional way of doing it would be a Nested Table and then insert the data with a single command:
CREATE OR REPLACE TYPE personas_NT AS TABLE OF PERSONA;
INSERT INTO grupos VALUES (nombre_grupo, personas_array)
SELECT dnombre,
CAST(MULTISET(
SELECT
emp_no,
apellido,
DIRECCION(dept.loc, NULL, dir),
fecha_alt
FROM EMPLEADOS
WHERE dept_no = dept.dept_no
) AS personas_NT) AS personas_array
FROM DEPARTAMENTOS dept;
But maybe, that would be a chapter in the "advanced" SQL course.

If condition in PL/SQL script with cursor and loop

I would like to ask for some help or advice in this particular case.
I have table called "Teams". The table contains 3 columns - Issue, Responsible_team and More_Info (all varchar2).
I have a PL/SQL script with cursor and loop for selecting as many teams as issue description you type (some random word you think it might help you find the responsible team). This part works great for me.
But I do not know how to compile the IF condition in there. If no team is found according to typed word description, I would like to get some basic output dbms_output.put_line('Responsible team is not found').
There are 2 ways how I wrote the script. Classic loop and while loop.
I would be happy for any advice.
1.script
set verify off
DECLARE
v_issue teams.issue%type; --variable for issue column from teams table
v_respteam teams.responsible_team%type; --variable for responsible_team column from teams table
v_info teams.more_info%type; --variable for more_info column from teams table
--cursor declaration
CURSOR c_respteam
RETURN teams%ROWTYPE
IS
SELECT issue, responsible_team, more_info
FROM teams
WHERE lower(issue) like '%&Describe_Issue%';
BEGIN
OPEN c_respteam;
LOOP
FETCH c_respteam into v_issue, v_respteam, v_info;
EXIT when c_respteam%NOTFOUND;
dbms_output.put_line('Responsible team is '|| v_respteam || ' --> ' || v_info);
END LOOP;
CLOSE c_respteam;
end;
/
2.script
-- cursor with while loop
set verify off
DECLARE
v_issue teams.issue%type; --variable for issue column from teams table
v_respteam teams.responsible_team%type; --variable for responsible_team column from teams table
v_info teams.more_info%type; --variable for more_info column from teams table
CURSOR c_respteam
RETURN teams%ROWTYPE IS
SELECT issue, responsible_team, more_info
FROM teams
WHERE lower(issue) like '%&Describe_Issue%';
BEGIN
OPEN c_respteam;
FETCH c_respteam INTO v_issue, v_respteam, v_info;
WHILE c_respteam%FOUND
LOOP
dbms_output.put_line('Responsible team is '|| v_respteam || ' --> ' || v_info);
FETCH c_respteam INTO v_issue, v_respteam, v_info;
END LOOP;
CLOSE c_respteam;
END;
/
You could rewrite to:
declare
l_found boolean :=false;
cursor c_respteam is
select issue
,responsible_team
,more_info
from teams
where lower(issue) like '%&Describe_Issue%';
begin
for r in c_respteam
loop
l_found := true;
dbms_output.put_line('Responsible team is ' || r.responsible_team || ' --> ' || r.more_info);
end loop;
if not l_found
then
dbms_output.put_line('No records found');
end if;
end;
/
You need to have a counter variable [ETA: ooh, I like Rene's boolean variable idea instead; either way, you need an extra variable!] to work out if any rows were returned or not. I'm not sure why you're using an explicit cursor fetch, rather than using the cursor-for-loop? Cursor-for-loops are not only easier to write, read and maintain, but Oracle have put some behind-the-scenes optimisation in, to aid performance.
Of course, depending on what you're actually doing with the data returned by your cursor (dbms_output.put_line being something that you should never have in your production code), it's debatable that you would need to loop through a cursor at all.
Anyway, with that said, here's an example demonstrating how I would handle your requirement to check for no rows returned by the cursor:
declare
cursor cur (p_val varchar2)
is
select dummy
from dual
where dummy like '%'||p_val||'%';
v_counter integer := 0;
begin
for rec in cur('Y')
loop
dbms_output.put_line('value of dummy = '||rec.dummy);
v_counter := v_counter + 1;
end loop;
if v_counter = 0 then
dbms_output.put_line('no rows returned');
end if;
end;
/
no rows returned
declare
cursor cur (p_val varchar2)
is
select dummy
from dual
where dummy like '%'||p_val||'%';
v_counter integer := 0;
begin
for rec in cur('X')
loop
dbms_output.put_line('value of dummy = '||rec.dummy);
v_counter := v_counter + 1;
end loop;
if v_counter = 0 then
dbms_output.put_line('no rows returned');
end if;
end;
/
value of dummy = X
To expand on what I said in my comment below, it sounds like you just need a single sql statement, rather than using PL/SQL and relying on dbms_output.
Eg., say you have the following statement:
select lvl
from (select 'X'||level lvl from dual connect by level <= 10)
where lvl like '%&val%';
with &val blank, you get:
LVL
-----------------------------------------
X1
X2
X3
X4
X5
X6
X7
X8
X9
X10
With &val = 2 you get:
LVL
-----------------------------------------
X2
With &val = 100 you get:
no rows selected.

Return N columns from a table function

I need to implement a table function, which I will submit a request with an unknown number of columns. It looks like:
SELECT * from TABLE (function())
where function, for example'SELECT x, y FROM z. I don't know how do this, so I'd like to hear some sort of way to solve, just as an idea.
I think what you are asking is you are getting multiple rows in the o/p when you are using
the function in select statement .
if i create a function as follows:
create or replace function get1job
(id in varchar2)
return varchar2 is
tittle jobs.JOB_TITLE%type;
begin
select job_title into tittle from jobs where job_id=id;
return tittle;
end get1job;
and use it in select statement .
i will write :
select get_job('AD_PRES') from dual;
i will get only one row
if i write :
select get_job('AD_PRES') from jobs;
the number of rows displayed will be equal to the number of rows in the table jobs.
Here is an example for a fully dynamic SQL, you can insert any SELECT statement and it prints out a corresponding HTML:
CREATE OR REPLACE PROCEDURE HtmlTable(sqlStr IN VARCHAR2) IS
cur INTEGER := DBMS_SQL.OPEN_CURSOR;
columnCount INTEGER;
describeColumns DBMS_SQL.DESC_TAB;
res INTEGER;
c INTEGER;
aCell VARCHAR2(4000);
BEGIN
DBMS_OUTPUT.PUT_LINE('<table>');
DBMS_SQL.PARSE(cur, sqlStr, DBMS_SQL.NATIVE);
DBMS_SQL.DESCRIBE_COLUMNS(cur, columnCount, describeColumns);
DBMS_OUTPUT.PUT_LINE('<thead><tr>');
FOR i IN 1..columnCount LOOP
DBMS_OUTPUT.PUT_LINE(' <td>'||describeColumns(i).COL_NAME||'</td>');
DBMS_SQL.DEFINE_COLUMN(cur, i, aCell, 4000);
END LOOP;
DBMS_OUTPUT.PUT_LINE('</tr></thead>');
res := DBMS_SQL.EXECUTE(cur);
DBMS_OUTPUT.PUT_LINE('<tbody>');
WHILE (DBMS_SQL.FETCH_ROWS(cur) > 0) LOOP
DBMS_OUTPUT.PUT_LINE('<tr>');
c := 1;
WHILE (c <= columnCount) LOOP
DBMS_SQL.COLUMN_VALUE(cur, c, aCell);
DBMS_OUTPUT.PUT_LINE(' <td>'||aCell||'</td>');
c := c + 1;
END LOOP;
DBMS_OUTPUT.PUT_LINE('</tr>');
END LOOP;
DBMS_OUTPUT.PUT_LINE('</tbody>');
DBMS_OUTPUT.PUT_LINE('</table>');
DBMS_SQL.CLOSE_CURSOR(cur);
END HtmlTable;
Use this as a base for your application. Then you can execute it like this:
BEGIN
HtmlTable('SELECT x, y FROM z');
END;

An elegant way to transform an index by table into simple table in PL/SQL, other then loop through every entry

I have two types
CREATE OR REPLACE TYPE my_record_type IS OBJECT
(
name varchar2(30)
)
;
CREATE OR REPLACE TYPE my_table_type AS TABLE OF my_record_type
and a function
create or replace my_function return my_table_type
is
type my_hash_type is table of my_record_type index by pls_integer;
v_hash my_hash_type;
v_table my_table_type;
i NUMBER;
begin
-- some business logic here
-- transformation part
v_table := my_table_type();
i := v_hash.first;
while i is not null loop
v_table.extend(1);
v_table(v_table.last) := v_hash(i);
i := v_hash.next(i);
end loop;
-- end transformation part
return v_table;
end;
/
Is there an elegant way in 10g to replace the transformation part with something like
v_table = CAST( v_hash as my_table_type )
You may use the SELECT my_record_type(column_value) BULK COLLECT INTO v_table from table(v_hash). But in order to use this, you will have to create my_hash_type outside of a function (either as a stand along type OR in a Package Specification so it will be visible to the SQL Engine) otherwise you will receive a PLS-00642: local collection types not allowed in SQL statements.
CREATE OR REPLACE TYPE my_hash_type is table OF VARCHAR2(10);
/
set serveroutput on
declare
--type my_hash_type is table OF VARCHAR2(10);
v_hash my_hash_type := my_hash_type();
v_table my_table_type;
i NUMBER;
begin
null ;
for n in 60..75 loop
V_hash.extend(1);
V_hash(v_hash.count) := chr(n) ;
end loop ;
select my_record_type(column_value)
bulk collect into v_table
from table(v_hash) ;
for n in 1..v_table.count loop
dbms_output.put_line( n || ':>' || v_table(n).name);
end loop ;
--PLS-00642: local collection types not allowed in SQL statements
end ;
1:><
2:>=
3:>>
4:>?
5:>#
6:>A
7:>B
8:>C
9:>D
10:>E
11:>F
12:>G
13:>H
14:>I
15:>J
16:>K
have a look here and here for some more examples and whatnot
timing differences (based on this methodology #s are in hundreths of a second)
pl/sql context switch (as described above)
44
42
43
42
loop fill (with type defined outside of block) --A distinct CREATE TYPE on Oracle level
18
18
18
18
loop fill (with type defined within block) --Type created within the Anon. block
23
22
24
22
(the above time trials were variations based on this code:
set serveroutput on
declare
--type my_hash_type is table of my_record_type -index by pls_integer;
v_hash my_hash_type := my_hash_type();
v_table my_table_type;
i NUMBER;
time_before BINARY_INTEGER;
time_after BINARY_INTEGER;
begin
time_before := DBMS_UTILITY.GET_TIME;
for n in 0..15000 loop
V_hash.extend(1);
V_hash(v_hash.count) := my_record_type(n) ;
end loop ;
select my_record_type(column_value)
bulk collect into v_table
from table(v_hash) ;
/*
v_table := my_table_type();
for n in 1..V_hash.count loop
v_table.extend(1);
v_table(v_table.count) := v_hash(n) ;
--dbms_output.put_line( n || ':>' || v_table(n).name);
end loop ;*/
--for n in 1..v_table.count loop
-- dbms_output.put_line( n || ':>' || v_table(n).name);
--end loop ;
time_after := DBMS_UTILITY.GET_TIME;
DBMS_OUTPUT.PUT_LINE (time_after - time_before);
--PLS-00642: local collection types not allowed in SQL statements
end ;
/
Thus the loop fill is 50% faster, but the time difference is still minuscule (here is the balance between premature optimization and avoiding something because it may be too long, I would recommend doing time trials on your real data to find the solution that best fits).
The only other 'elegant' solution I can think of is TREAT, but you'll notice it requires a subtype/supertype solution that must be on an object type (I couldn't get it to work on an Varray/Assoc. Array type -- hopefully I'm wrong!)

Selecting Values from Oracle Table Variable / Array?

Following on from my last question (Table Variables in Oracle PL/SQL?)...
Once you have values in an array/table, how do you get them back out again? Preferably using a select statement or something of the like?
Here's what I've got so far:
declare
type array is table of number index by binary_integer;
pidms array;
begin
for i in (
select distinct sgbstdn_pidm
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH'
)
loop
pidms(pidms.count+1) := i.sgbstdn_pidm;
end loop;
select *
from pidms; --ORACLE DOESN'T LIKE THIS BIT!!!
end;
I know I can output them using dbms_output.putline(), but I'm hoping to get a result set like I would from selecting from any other table.
Thanks in advance,
Matt
You might need a GLOBAL TEMPORARY TABLE.
In Oracle these are created once and then when invoked the data is private to your session.
Oracle Documentation Link
Try something like this...
CREATE GLOBAL TEMPORARY TABLE temp_number
( number_column NUMBER( 10, 0 )
)
ON COMMIT DELETE ROWS;
BEGIN
INSERT INTO temp_number
( number_column )
( select distinct sgbstdn_pidm
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH'
);
FOR pidms_rec IN ( SELECT number_column FROM temp_number )
LOOP
-- Do something here
NULL;
END LOOP;
END;
/
In Oracle, the PL/SQL and SQL engines maintain some separation. When you execute a SQL statement within PL/SQL, it is handed off to the SQL engine, which has no knowledge of PL/SQL-specific structures like INDEX BY tables.
So, instead of declaring the type in the PL/SQL block, you need to create an equivalent collection type within the database schema:
CREATE OR REPLACE TYPE array is table of number;
/
Then you can use it as in these two examples within PL/SQL:
SQL> l
1 declare
2 p array := array();
3 begin
4 for i in (select level from dual connect by level < 10) loop
5 p.extend;
6 p(p.count) := i.level;
7 end loop;
8 for x in (select column_value from table(cast(p as array))) loop
9 dbms_output.put_line(x.column_value);
10 end loop;
11* end;
SQL> /
1
2
3
4
5
6
7
8
9
PL/SQL procedure successfully completed.
SQL> l
1 declare
2 p array := array();
3 begin
4 select level bulk collect into p from dual connect by level < 10;
5 for x in (select column_value from table(cast(p as array))) loop
6 dbms_output.put_line(x.column_value);
7 end loop;
8* end;
SQL> /
1
2
3
4
5
6
7
8
9
PL/SQL procedure successfully completed.
Additional example based on comments
Based on your comment on my answer and on the question itself, I think this is how I would implement it. Use a package so the records can be fetched from the actual table once and stored in a private package global; and have a function that returns an open ref cursor.
CREATE OR REPLACE PACKAGE p_cache AS
FUNCTION get_p_cursor RETURN sys_refcursor;
END p_cache;
/
CREATE OR REPLACE PACKAGE BODY p_cache AS
cache_array array;
FUNCTION get_p_cursor RETURN sys_refcursor IS
pCursor sys_refcursor;
BEGIN
OPEN pCursor FOR SELECT * from TABLE(CAST(cache_array AS array));
RETURN pCursor;
END get_p_cursor;
-- Package initialization runs once in each session that references the package
BEGIN
SELECT level BULK COLLECT INTO cache_array FROM dual CONNECT BY LEVEL < 10;
END p_cache;
/
The sql array type is not neccessary. Not if the element type is a primitive one. (Varchar, number, date,...)
Very basic sample:
declare
type TPidmList is table of sgbstdn.sgbstdn_pidm%type;
pidms TPidmList;
begin
select distinct sgbstdn_pidm
bulk collect into pidms
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH';
-- do something with pidms
open :someCursor for
select value(t) pidm
from table(pidms) t;
end;
When you want to reuse it, then it might be interesting to know how that would look like.
If you issue several commands than those could be grouped in a package.
The private package variable trick from above has its downsides.
When you add variables to a package, you give it state and now it doesn't act as a stateless bunch of functions but as some weird sort of singleton object instance instead.
e.g. When you recompile the body, it will raise exceptions in sessions that already used it before. (because the variable values got invalided)
However, you could declare the type in a package (or globally in sql), and use it as a paramter in methods that should use it.
create package Abc as
type TPidmList is table of sgbstdn.sgbstdn_pidm%type;
function CreateList(majorCode in Varchar,
program in Varchar) return TPidmList;
function Test1(list in TPidmList) return PLS_Integer;
-- "in" to make it immutable so that PL/SQL can pass a pointer instead of a copy
procedure Test2(list in TPidmList);
end;
create package body Abc as
function CreateList(majorCode in Varchar,
program in Varchar) return TPidmList is
result TPidmList;
begin
select distinct sgbstdn_pidm
bulk collect into result
from sgbstdn
where sgbstdn_majr_code_1 = majorCode
and sgbstdn_program_1 = program;
return result;
end;
function Test1(list in TPidmList) return PLS_Integer is
result PLS_Integer := 0;
begin
if list is null or list.Count = 0 then
return result;
end if;
for i in list.First .. list.Last loop
if ... then
result := result + list(i);
end if;
end loop;
end;
procedure Test2(list in TPidmList) as
begin
...
end;
return result;
end;
How to call it:
declare
pidms constant Abc.TPidmList := Abc.CreateList('HS04', 'HSCOMPH');
xyz PLS_Integer;
begin
Abc.Test2(pidms);
xyz := Abc.Test1(pidms);
...
open :someCursor for
select value(t) as Pidm,
xyz as SomeValue
from table(pidms) t;
end;

Resources