Bulk Collect Twice over same nested table - oracle

Is there any way that after the second bulk collect, data does not get override of the first bulk collect. I don't want to iterate in loop.
DECLARE
TYPE abc IS RECORD (p_id part.p_id%TYPE);
TYPE abc_nt
IS
TABLE OF abc
INDEX BY BINARY_INTEGER;
v_abc_nt abc_nt;
BEGIN
SELECT p_id
BULK COLLECT
INTO v_abc_nt
FROM part
WHERE p_id IN ('E1', 'E2');
SELECT p_id
BULK COLLECT
INTO v_abc_nt
FROM part
WHERE p_id IN ('E3', 'E4');
FOR i IN v_abc_nt.FIRST .. v_abc_nt.LAST
LOOP
DBMS_OUTPUT.put_line (
'p_id is ' || v_abc_nt (i).p_id
);
END LOOP;
END;
OUTPUT:
p_id is E3
p_id is E4
Note: E1 and E2 is present in part table.

You can't simply add the data to the collection, no.
You can, however, do a BULK COLLECT into a separate collection and then combine the collections assuming that you really just need/ want a nested table rather than an associative array...
DECLARE
TYPE abc IS RECORD (p_id part.p_id%TYPE);
TYPE abc_nt
IS
TABLE OF abc;
v_abc_nt abc_nt;
v_abc_nt2 abc_nt;
BEGIN
SELECT p_id
BULK COLLECT
INTO v_abc_nt
FROM part
WHERE p_id IN ('E1', 'E2');
SELECT p_id
BULK COLLECT
INTO v_abc_nt2
FROM part
WHERE p_id IN ('E3', 'E4');
v_abc_nt := v_abc_nt MULTISET UNION v_abc_nt2;
FOR i IN v_abc_nt.FIRST .. v_abc_nt.LAST
LOOP
DBMS_OUTPUT.put_line (
'p_id is ' || v_abc_nt (i).p_id
);
END LOOP;
END;
If you really want to use an associative array, you would need to write some code because there is no way for Oracle to know automatically how to remap the associations of one array when you combine it with another associative array that has some of the same keys.

You can write it like this
bad example:
declare
type t_numb is record(
numb number);
type t_numb_list is table of t_numb;
v_numb_list t_numb_list;
begin
with q as
(select 1 a from dual union select 2 from dual union select 3 from dual)
select q.a bulk collect into v_numb_list from q;
with w as
(select 4 a from dual union select 5 from dual union select 6 from dual)
select w.a bulk collect into v_numb_list from w;
for r in 1 .. v_numb_list.count loop
dbms_output.put_line(v_numb_list(r).numb);
end loop;
end;
and this works good:
declare
type t_numb is record(
numb number);
type t_numb_list is table of t_numb;
v_numb_list t_numb_list := t_numb_list();
v_numb t_numb;
begin
for q in (select 1 a
from dual
union
select 2
from dual
union
select 3
from dual) loop
v_numb.numb := q.a;
v_numb_list.extend;
v_numb_list(v_numb_list.count) := v_numb;
end loop;
for w in (select 4 a
from dual
union
select 5
from dual
union
select 6
from dual) loop
v_numb.numb := w.a;
v_numb_list.extend;
v_numb_list(v_numb_list.count) := v_numb;
end loop;
for r in 1 .. v_numb_list.count loop
dbms_output.put_line(v_numb_list(r).numb);
end loop;
end;

Related

Oracle View or Table Function that returns union of queries stored as text in another table

Let's say that I have a "Rules" table that has a column that contains oracle queries in a varchar2 column:
Row
Query
1
select 'Hardcoded_Tablename_1' tablename, request_id from table_1 where status >= 4 and resolve_date < *some date math goes here*
2
select 'Table_2' tablename, t2.request_id from table_2 t2 join table_1 t1 on t1.request_id = t2.parent_id where t1.status >= 4 and t1.resolve_date < *some date math goes here*
If this were never going to change, I'd just make a view with a union of these queries.
Our requirement is that we be able to add to or to modify these rules on-the-fly at the whims of leadership.
So, what I need is either:
a very smart view (I think impossible) that executes and unions all of these stored query strings
or
a table function that returns the results of the union of these stored query strings. (I think this is the more likely solution)
It will only ever be those two columns: The hardcoded name of the table and the ID of the record.
Can someone help get me started on this?
Thanks
You can use a PIPELINED function.
First create the types:
CREATE TYPE request_data IS OBJECT (tablename VARCHAR2(30), request_id NUMBER);
CREATE TYPE request_list IS TABLE OF request_data;
Then the function:
CREATE FUNCTION get_requests RETURN request_list PIPELINED
IS
BEGIN
FOR r IN (SELECT "QUERY" FROM table_name ORDER BY "ROW")
LOOP
DECLARE
c_cursor SYS_REFCURSOR;
v_tablename VARCHAR2(30);
v_request_id NUMBER;
BEGIN
OPEN c_cursor FOR r."QUERY";
LOOP
FETCH c_cursor INTO v_tablename, v_request_id;
EXIT WHEN c_cursor%NOTFOUND;
PIPE ROW (request_data(v_tablename, v_request_id));
END LOOP;
CLOSE c_cursor;
EXCEPTION
WHEN NO_DATA_NEEDED THEN
CLOSE c_cursor;
RETURN;
END;
END LOOP;
END;
/
Then, if you have the sample data:
CREATE TABLE table_name ("ROW", "QUERY") AS
SELECT 1, q'[select 'Hardcoded_Tablename_1' tablename, request_id from table_1 where status >= 4 and resolve_date < SYSDATE]' FROM DUAL UNION ALL
SELECT 2, q'[select 'Table_2' tablename, t2.request_id from table_2 t2 join table_1 t1 on t1.request_id = t2.parent_id where t1.status >= 4 and t1.resolve_date < SYSDATE]' FROM DUAL
CREATE TABLE table_1 (request_id, status, resolve_date) AS
SELECT 42, 4, SYSDATE - 1 FROM DUAL;
CREATE TABLE table_2 (request_id, parent_id) AS
SELECT 57, 42 FROM DUAL;
Then you can use the function in a table collection expression:
SELECT *
FROM TABLE(get_requests());
Which outputs:
TABLENAME
REQUEST_ID
Hardcoded_Tablename_1
42
Table_2
57
db<>fiddle here
One option might be a function that returns refcursor.
SQL> select * from rules;
CROW QUERY
---------- ----------------------------------------------------------------------------------------------------
1 select 'EMP' tablename, empno from emp where hiredate = (select max(hiredate) from emp)
2 select 'DEPT' tablename, d.deptno from emp e join dept d on d.deptno = e.deptno where e.hiredate = (
select min(hiredate) from emp)
Function creates union of all queries from the rules table and uses it as a source for the refcursor:
SQL> create or replace function f_test return sys_refcursor
2 is
3 l_rc sys_refcursor;
4 l_str clob;
5 begin
6 for cur_r in (select query from rules order by crow) loop
7 l_str := l_str || cur_r.query ||' union all ';
8 end loop;
9 l_str := rtrim(l_str, ' union all ');
10
11 open l_rc for l_str;
12 return l_rc;
13 end;
14 /
Function created.
Testing:
SQL> select f_test from dual;
F_TEST
--------------------
CURSOR STATEMENT : 1
CURSOR STATEMENT : 1
TABL EMPNO
---- ----------
EMP 7876
DEPT 20
SQL>

Stored procedure calling 2 values in parameter declaration

I have a stored procedure written like this:
CREATE OR REPLACE PROCEDURE Proc_location_example(in_data_ID IN VARCHAR2,
in_Location_name IN VARCHAR2)
IS
v_Location_ID NUMBER;
v_data_id NUMBER;
BEGIN
---------------------------------------------------------------------------
lv_prog_name := 'PRoc_location_example';
ln_step := 1;
SELECT Location_ID
INTO v_Location_ID
FROM random.client
WHERE Location_name = in_Location_name;
.....
END;
This procedure is getting 'NY ' passed in as an example for in_location_name. I want to pass in 'NY' and 'nj' to the location_name.
In other words the location name supports 2 name so which will be easiest way to do it?
You can add an expression with an IN operator such as
.. WHERE Location_name = in_Location_name AND in_Location_name IN ('NY','nj')
if case-sensitivity doesn't matter, then use
.. WHERE Location_name = in_Location_name AND REGEXP_LIKE(in_Location_name, 'Ny|nJ','i')
in order to restrict the parameters to those two values.
There are two options that I can think of that would satisfy your question.
Option #1: More Parameters
By adding more parameters you can use those parameters in your query for location IDs. If you are going to potentially more than 2 locations and don't want to keep having to add parameters, Option #2 is probably a better choice.
CREATE OR REPLACE PROCEDURE Proc_location_example (in_location_name1 IN VARCHAR2 DEFAULT NULL,
in_location_name2 IN VARCHAR2 DEFAULT NULL)
IS
TYPE location_id_t IS TABLE OF NUMBER;
v_location_ids location_id_t;
BEGIN
SELECT location_id
BULK COLLECT INTO v_location_ids
FROM (SELECT 101 AS location_id, 'NJ' AS location_name FROM DUAL
UNION ALL
SELECT 102, 'NY' FROM DUAL
UNION ALL
SELECT 103, 'MA' FROM DUAL)
WHERE location_name IN (in_location_name1, in_location_name2);
FOR i IN 1 .. v_location_ids.COUNT
LOOP
DBMS_OUTPUT.put_line ('ID #' || i || ': ' || v_location_ids (i));
END LOOP;
END;
/
BEGIN
Proc_location_example (in_location_name1 => 'NJ', in_location_name2 => 'NY');
END;
/
Option #2: Table Type Parameter
By using a table as your parameter, you can pass an unlimited number of "location names". This does require you to use a pre-defined table type, but this solution is more flexible in the number of "parameters"
CREATE OR REPLACE TYPE location_name_t IS TABLE OF VARCHAR2 (2);
CREATE OR REPLACE PROCEDURE Proc_location_example (in_locations location_name_t)
IS
TYPE location_id_t IS TABLE OF NUMBER;
v_location_ids location_id_t;
BEGIN
SELECT location_id
BULK COLLECT INTO v_location_ids
FROM (SELECT 101 AS location_id, 'NJ' AS location_name FROM DUAL
UNION ALL
SELECT 102, 'NY' FROM DUAL
UNION ALL
SELECT 103, 'MA' FROM DUAL)
WHERE location_name IN (select * from table(in_locations));
FOR i IN 1 .. v_location_ids.COUNT
LOOP
DBMS_OUTPUT.put_line ('ID #' || i || ': ' || v_location_ids (i));
END LOOP;
END;
/
BEGIN
Proc_location_example (in_locations => location_name_t('NJ','NY'));
END;
/

How to assign multiple values to a variable using into clause in oracle query?

I have an oracle stored procedure like this
CREATE OR REPLACE PROCEDURE DEMO (V_IN CHAR, V_OUT VARCHAR2)
IS
BEGIN
FOR ITEM IN LOOP (SELECT DISTINCT (NAME)
FROM TABLE1 INTO V_OUT
WHERE ID = V_IN
LOOP
--CODE TO PRINT V_OUT
END LOOP;
END;
Now how should I create that V_OUT variable so that it can hold all the values coming from query? I'm doing this in oracle12C.
You don't put the INTO clause in the cursor query. And even if you did, you have it in the wrong place in the SQL statement.
You deal with that when you fetch a row from the query:
CREATE OR REPLACE PROCEDURE
DEMO (V_IN CHAR, V_OUT
VARCHAR2)IS
BEGIN
FOR ITEM IN LOOP
(SELECT DISTINCT (NAME)
FROM TABLE1
WHERE ID= V_IN
)
LOOP
dbms_output.put_line(item.name);
v_out := item.name;
END LOOP;
END;
But then the problem is that we just keep overlaying the previous value, so that when your procedure actually exits, the only value of v_out is that last assigned. If you truely need a collection of values, you need to declare your output variable to be a ref cursor, and adjust code accordingly. I've never actually worked with them, so perhaps someone else will chime in.
You can work with collections, like this:
--declare the pakage type
CREATE OR REPLACE PACKAGE PKG_TYPES AS
TYPE LIST_VARCHAR IS TABLE OF VARCHAR2(2000);
END;
--create the proc that will assemble the value list
CREATE OR REPLACE PROCEDURE DEMO ( V_IN IN varchar2, V_OUT IN OUT PKG_TYPES.LIST_VARCHAR) IS
BEGIN
FOR ITEM IN (
SELECT DISTINCT (NAME) name
FROM (SELECT 'X' ID, 'A' name FROM dual
UNION
SELECT 'X' ID, 'b' name FROM dual
UNION
SELECT 'y' ID, 'c' name FROM dual
) TABLE1
WHERE ID= V_IN
)
LOOP
V_OUT.EXTEND;
V_OUT(V_OUT.LAST) := item.name;
--CODE TO PRINT V_OUT
END LOOP;
END;
--use the list. I separated this step but it can be in the demo proc as well
DECLARE
names PKG_TYPES.LIST_VARCHAR := PKG_TYPES.LIST_VARCHAR();
BEGIN
demo('X',names) ;
FOR i IN names.first..names.last LOOP
Dbms_Output.put_line(i);
END LOOP;
END;
You will have to handle exceptions for when no value is returned from the cursor (when no ID is found).
If you need a collection variable - you can use a nested table and bulk collect like below.
To be able to return the value from the procedure you will need to declare the nested table type in some package or on DB schema level.
declare
type test_type is table of varchar2(2000);
test_collection test_type;
begin
select distinct(name) bulk collect into test_collection
from (
select 1 id, 'AAA' name from dual
union all
select 1 id, 'BBB' name from dual
union all
select 1 id, 'AAA' name from dual
union all
select 2 id, 'CCC' name from dual
)
where id = 1;
for i in test_collection.first..test_collection.last loop
dbms_output.put_line(test_collection(i));
end loop;
end;
/
If you just need a string with concatenated values - you can use listagg to create it like below
declare
test_str varchar2(4000);
begin
select listagg(name, ', ') within group(order by 1)
into test_str
from (
select distinct name
from (
select 1 id, 'AAA' name from dual
union all
select 1 id, 'BBB' name from dual
union all
select 1 id, 'AAA' name from dual
union all
select 2 id, 'CCC' name from dual
)
where id = 1
);
dbms_output.put_line(test_str);
end;
/

ORA-00928: missing SELECT keyword

In MYTABLE there are courses and their predecessor courses.
What I am trying to is to find the courses to be taken after the specified course. I am getting missing SELECT keyword error. Why I am getting this error although I have SELECT statement in FOR statement ? Where am I doing wrong ?
DECLARE
coursename varchar2(200) := 'COURSE_101';
str varchar2(200);
BEGIN
WITH DATA AS
(select (select course_name
from MYTABLE
WHERE predecessors like ('''%' || coursename||'%''')
) str
from dual
)
FOR cursor1 IN (SELECT str FROM DATA)
LOOP
DBMS_OUTPUT.PUT_LINE(cursor1);
END LOOP;
end;
Unless I'm wrong, WITH factoring clause can't be used that way; you'll have to use it as an inline view, such as this:
declare
coursename varchar2(200) := 'COURSE_101';
str varchar2(200);
begin
for cursor1 in (select str
from (select (select course_name
from mytable
where predecessors like '''%' || coursename||'%'''
) str
from dual
)
)
loop
dbms_output.put_line(cursor1.str);
end loop;
end;
/
Apart from the fact that it doesn't work (wrong LIKE condition), you OVERcomplicated it. This is how it, actually, does something:
SQL> create table mytable(course_name varchar2(20),
2 predecessors varchar2(20));
Table created.
SQL> insert into mytable values ('COURSE_101', 'COURSE_101');
1 row created.
SQL>
SQL> declare
2 coursename varchar2(20) := 'COURSE_101';
3 begin
4 for cursor1 in (select course_name str
5 from mytable
6 where predecessors like '%' || coursename || '%'
7 )
8 loop
9 dbms_output.put_line(cursor1.str);
10 end loop;
11 end;
12 /
COURSE_101
PL/SQL procedure successfully completed.
SQL>
Also, is that WHERE clause correct? PREDECESSORS LIKE COURSENAME? I'm not saying that it is wrong, just looks somewhat strange.
To extend #Littlefoot's answer a bit: you can use a common table expression (WITH clause) in your cursor, but the WITH must be part of the cursor SELECT statement, not separate from it:
DECLARE
coursename varchar2(200) := 'COURSE_101';
BEGIN
FOR aRow IN (WITH DATA AS (select course_name AS str
from MYTABLE
WHERE predecessors like '''%' || coursename||'%''')
SELECT str FROM DATA)
LOOP
DBMS_OUTPUT.PUT_LINE(aRow.str);
END LOOP;
END;
Also note that the iteration variable in a cursor FOR-loop represents a row returned by the cursor's SELECT statement, so if you want to display whatever was returned by the cursor you must use dotted-variable notation (e.g. aRow.str) to extract fields from the row.
Best of luck.
CREATE TABLE product
(
PRODUCT_ID int Primary key,
NAME VARCHAR (20) not null,
Batchno int not null,
Rate int not null,
Tax int not null,
Expiredate date not null
);
INSERT INTO PRODUCT VALUSES(1 , 'vasocare', 32 , 15 , 2 , 01-JAN-2021);

Oracle PL/SQL: Returning specific selected columns from procedure

What is the best method for returning specific column values from a Procedure. For example, the below code doesn't work
/*DECLARATION*/
TYPE t_data IS TABLE OF Table1%ROWTYPE;
PROCEDURE get_values(data OUT t_data) AS
BEGIN
SELECT a.object_id, a.num, b.descrip
BULK COLLECT INTO data
FROM Table1 a INNER JOIN
Table2 b ON (a.id = b.id)
WHERE a.date IS NULL;
END get_values;
In the same scenario if I use a SELECT *, it works...
I assume this is an exercise on how to use a procedure to get a list of structured values; I would never recommend such an approach to get data from a table, preferring, if possible, a pure SQL method.
You seem to have tables like these:
create table table1(id ,object_id, num, "date") as (
select 1, 1, 100, sysdate from dual union all
select 2, 2, 200, null from dual
);
create table table2(id, descrip) as (
select 1, 'desc1' from dual union all
select 2, 'desc2' from dual
);
You're trying to create a procedure that return a set of rows, where each row contains elements from both tables; to do so, you need to build a type that matches the result of your select query.
You may want to define your package like this:
CREATE OR REPLACE PACKAGE yourPackage AS
TYPE tRec IS RECORD /* made to match the columns you want to extract in your query */
(
object_id NUMBER,
num NUMBER,
descrip VARCHAR2(100)
);
TYPE tTab IS TABLE OF tRec;
PROCEDURE get_values(data OUT tTab);
END yourPackage;
create or replace package body yourPackage as
PROCEDURE get_values(data OUT tTab) AS
BEGIN
SELECT a.object_id, a.num, b.descrip
BULK COLLECT INTO data
FROM Table1 a INNER JOIN
Table2 b ON (a.id = b.id)
WHERE a."date" IS NULL;
END get_values;
end yourPackage ;
You can call the procedure in the package this way:
declare
someVar yourPackage.tTab;
begin
yourPackage.get_values(someVar);
--
if someVar.first is not null then
for i in someVar.first .. someVar.last loop
dbms_output.put_line(someVar(i).object_id || ' - ' || someVar(i).num || ' - ' || someVar(i).descrip);
end loop;
end if;
end;
and this is the result you get:
2 - 200 - desc2
Firstly you cannot create a Type of any table%rowtype outside a PLSQL block. You need to create table as object and then have to create a type of that object. Then you can use it.
See below:
CREATE OR REPLACE TYPE Table11 AS OBJECT
(
id NUMBER,
num NUMBER,
description VARCHAR2 (20)
)
CREATE OR REPLACE TYPE t_data IS TABLE OF Table11;
CREATE OR REPLACE PROCEDURE get_values (v_data OUT t_data)
AS
BEGIN
SELECT Table11 (a.row_id, 222, 'hello')
BULK COLLECT INTO v_data
FROM Table1 a INNER JOIN Table2 b
ON (a.row_id = b.appid)
WHERE a.date IS NULL;
END get_values;
execution:
DECLARE
v_var t_data;
BEGIN
get_values (v_var);
FOR i IN 1 .. v_var.COUNT
LOOP
DBMS_OUTPUT.put_line (v_var (i).id ||' ' ||v_var(i).num ||' ' || v_var(i).description );
END LOOP;
END;
Output:
SQL> /
1 222 hello
2 222 hello
PL/SQL procedure successfully completed.

Resources