How can I pass a PL/SQL cursor record created from multiple tables? - oracle

This is PL/SQL in a 12c database.
I need to be able to pass a cursor record to a function. The problem is that the cursor is from three different tables and one of them must use a * to select all of the (hundreds) of fields in that table.
It works if I select distinct fields (from a smaller test table) and it works if I use a select * from one table and no other fields (no other tables involved), but I can't find any way to make this work when selecting from three tables (examples only show two) and using a * (to select all fields) from one of the tables.
I've tried FETCH INTO and SELECT INTO with a pre-defined cursor. I've tried using a cursor record with a FOR myRecord IN (SELECT a.*, ...) and
creating a matching object (since records can't be created at the schema level)
These work
myVarA a%ROWTYPE;
SELECT a.* INTO myVarA
FROM a;
SELECT a.myAfield, b.myBfield INTO myVarA, myVarB
FROM a, b;
But I need this to work:
myVarA a%ROWTYPE;
SELECT a.*, b.myBfield INTO myVarA, myVarB
FROM a, b;
Using a cursor record
myRecord myObjectTypeWithAllFields; -- ojbect, not record as record can't be declared at schema level
FOR myRecord IN (SELECT a.*, b.myBfield FROM a, b);
newVar := myFunction(myRecord); -- this won't work
fails on the function call with PLS-00306: wrong number or types of arguments in call to myFunction;
The function will do a lot of grunt work that is similar in many packages, but there is also a large amount of unique work
in each package, so I can't just process the entire cursor loop in the function. I really need to pass one row at a time to the function.
Is there a way to do this?

You can define a cursor in a package - whether or not you will actually use it - with the same select list:
create package p as
cursor c is select a.*, b.myBField
from a, b; -- but use proper join syntax
end;
/
Then define your function argument using that cursor's %rowtype:
create function myFunction(p_record p.c%rowtype) return ... as ...
Then your block will work:
FOR myRecord IN (SELECT a.*, b.myBfield FROM a, b) LOOP
newVar := myFunction(myRecord);
END LOOP;
db<>fiddle demo
The cursor for loop could then use the cursor defined in the package if you wanted it to.
Incidentally, in your object version, the variable declaration:
myRecord myObjectTypeWithAllFields;
is redundant; the myRecord in FOR myRecord... is completely independent. So it doesn't even attempt to use the object type.

You are using a %rowtype anchor so I am assuming an exact fetch. Oracle does not allow to use of multiple records with "into" clauses. But we can have a workaround on that.
DECLARE
emprec EMPLOYEES%ROWTYPE;
deptname DEPARTMENTS.department_name%type;
BEGIN
select a.* into emprec
from EMPLOYEES a
WHERE EMPLOYEE_ID = 100;
SELECT DEPARTMENT_NAME into deptname
from DEPARTMENTS where DEPARTMENT_ID = emprec.DEPARTMENT_ID;
END;
This example shows how you can fetch your a.* in a record and have inner join with another table and have b.myBfield into another record.
And once again these are records so I am assuming it is an exact fetch.
If it is not what you want let me know in the comment.

Related

INTO error in SELECT while creating stored procedure in PL/SQL

I am trying to create stored procedure where I want to join two tables and save the result into one of the tables, but I am getting INTO clause is required error.
This is my code:
CREATE PROCEDURE DiorItemMaster
AS
SELECT *FROM pcdo_dior_item_master
INNER JOIN pcdo_itemdata on pcdo_itemdata.vpn = pcdo_dior_item_master.vpn;
GO;
ERROR:
Error(4,1): PLS-00428: an INTO clause is expected in this SELECT statement
You have a query that selects columns from a table in your pl/sql block. What do you want to do with the result of that query ? You cannot just select and not do anything with the results in pl/sql (you can in sql). Oracle expects that you to store the results of that select in variables. That can be done using the SELECT INTO clause.
Example (based on sample schema emp/dept):
DECLARE
l_emp emp%ROWTYPE;
BEGIN
SELECT e.* INTO l_emp FROM emp e WHERE e.ename = 'KING';
END;
/
Note that you can SELECT INTO individual columns and into rows. You cannot use SELECT INTO arrays.
A couple of other remarks about your code:
You perform a SELECT * from a table with a join to another table without using aliases. This will return all columns from both tables. It is a lot more readable to prefix the "*" with a table alias like in the example.
The GO; is not part of the oracle syntax - this will cause a compilation error.

Oracle access varray elements in SQL

I'm playing around with array support in Oracle and hit a roadblock regarding array access within a SQL query. I'm using the following schema:
create type smallintarray as varray(10) of number(3,0);
create table tbl (
id number(19,0) not null,
the_array smallintarray,
primary key (id)
);
What I would like to do is get the id and the first element i.e. at index 1 of the array. In PostgreSQL I could write select id, the_array[1] from tbl t but I don't see how I could do that with Oracle. I read that array access by index is only possible in PL/SQL, which would be fine if I could return a "decorated cursor" to achieve the same result through JDBC, but I don't know if that's possible.
DECLARE
c1 SYS_REFCURSOR;
varr smallintarray2;
BEGIN
OPEN c1 FOR SELECT t.id, t.THE_ARRAY from tbl t;
-- SELECT t.THE_ARRAY INTO varr FROM table_with_enum_arrays2 t;
-- return a "decorated cursor" with varr(1) at select item position 1
dbms_sql.return_result(c1);
END;
You can do this in plain SQL; it's not pretty, but it does work. You would prefer that Oracle had syntax to hide this from the programmer (and perhaps it does, at least in the most recent versions; I am still stuck at 12.2).
select t.id, q.array_element
from tbl t cross apply
( select column_value as array_element,
rownum as ord
from table(the_array)
) q
where ord = 1
;
EDIT If order of generating the elements through the table operator is a concern, you could do something like this (in Oracle 12.1 and higher; otherwise the function can't be part of the query itself, but it can be defined on its own):
with
function select_element(arr smallintarray, i integer)
return number
as
begin
return arr(i);
end;
select id, select_element(the_array, 1) as the_array_1
from tbl
/
First of all, please don't do that on production. Use tables instead of storing arrays within a table.
Answer to your question is to use column as a table source
SELECT t.id, ta.*
from tbl t,
table(t.THE_ARRAY) ta
order by column_value
-- offset 1 row -- in case if sometime you'll need to skip a row
fetch first 1 row only;
UPD: as for ordering the array I can only say playing with 2asc/desc" parameters provided me with results I've expected - it has been ordered ascending or descending.
UPD2: found a cool link to description of performance issues might happen

Declaring and using variables in PL-SQL

I am new to PL-SQL. I do not understand why I am getting the error "PLS-00428: an INTO clause is expected in this SELECT statement"
What I'm trying to accomplish is to create a variable c_limit and load it's value. I then want to use that variable later to filter data.
Basically I am playing around in the demo db to see what I can/can't do with PL-SQL.
The code worked up to the point that I added "select * from demo_orders where CUSTOMER_ID = custID;"
declare
c_limit NUMBER(9,2);
custID INT;
BEGIN
custID := 6;
-- Save the credit limit
select credit_limit INTO c_limit
from demo_customers cust
where customer_id = custID;
select * from demo_orders where CUSTOMER_ID = custID;
dbms_output.Put_line(c_limit);
END;
If you are using a SQL SELECT statement within an anonymous block (in PL/SQL - between the BEGIN and the END keywords) you must select INTO something so that PL/SQL can utilize a variable to hold your result from the query. It is important to note here that if you are selecting multiple columns, (which you are by "SELECT *"), you must specify multiple variables or a record to insert the results of your query into.
for example:
SELECT 1
INTO v_dummy
FROM dual;
SELECT 1, 2
INTO v_dummy, v_dummy2
FROM dual;
It is also worth pointing out that if your SELECT * FROM.... will return multiple rows, PL/SQL will throw an error. You should only expect to retrieve 1 row of data from a SELECT INTO.
Looks like the error is from the second select query.
select * from demo_orders where CUSTOMER_ID = custID;
PL-SQL won't allow a standalone sql select query for info.
http://pls-00428.ora-code.com/
You need to do some operation with the second select query

PLSQL - Parameter cursor with different return

I know the title is saying nothing... but the argument is a little "complex" to explain in a single row.
All the code i'm writing is just an example, my current code is from other table etc, but the behaviour is the same.
i have defined a cursor like this:
CURSOR emp_cur (l_type)
IS
with emp_general AS (select *
from emp
where type = l_type),
emp_active AS (select *
from emp_geral
where status = ACTIVE_STATUS),
emp_inactive AS (select *
from emp_general
where status = INACTIVE_STATUS)
select distinct name, department
from emp_active
minus
select distinct name, department
from emp_inactive;
This cursor take a parameter for filter emp type and make a minus to fetch ACTIVE - INACTIVE emp.
This cursor return name and department.
Now have to declare different cursor with different "select" statemant, for example:
select location
from emp_active
select location
from emp_active
I would like to dont duplicate my cursor just to change the select. There is a way to do this and avoid code duplication (withuout using DynamicSQL - Difficult to debug in production enviroment)?
You could create two Global temporary tables explicitly once(not on the fly):
emp_active_gtt
emp_inactive_gtt
Such that, each temp table will have the entire result set of active and inactive records respectively.
For example, in the code you would do:
insert into emp_active_gtt
select *
from ....
where status ='ACTIVE'
Similarly, for inactive records:
insert into emp_inactive_gtt
select *
from ....
where status ='INACTIVE'
You could now use the two tables in the scope of the session anywhere to get the required rows.
Read more about GTT in the documentation here https://docs.oracle.com/database/121/SQLRF/statements_7002.htm#i2153132

Can a table variable be used in a select statement where clause?

I have a stored procedure that is doing a two-step query. The first step is to gather a list of VARCHAR2 type characters from a table and collect them into a table variable, defined like this:
TYPE t_cids IS TABLE OF VARCHAR2(50) INDEX BY PLS_INTEGER;
v_cids t_cids;
So basically I have:
SELECT item BULK COLLECT INTO v_cids FROM table_one;
This works fine up until the next bit.
Now I want to use that collection in the where clause of another query within the same procedure, like so:
SELECT * FROM table_two WHERE cid IN v_cids;
Is there a way to do this? I am able to select an individual element, but I would like to use the table variable like a would use a regular table. I've tried variations using nested selects, but that doesn't seem to work either.
Thanks a lot,
Zach
You have several choices as to how you achieve this.
If you want to use a collection, then you can use the TABLE function to select from it but the type of collection you use becomes important.
for a brief example, this creates a database type that is a table of numbers:
CREATE TYPE number_tab AS TABLE OF NUMBER
/
Type created.
The next block then populates the collection and performs a rudimentary select from it using it as a table and joining it to the EMP table (with some output so you can see what's happening):
DECLARE
-- Create a variable and initialise it
v_num_tab number_tab := number_tab();
--
-- This is a collection for showing the output
TYPE v_emp_tabtype IS TABLE OF emp%ROWTYPE
INDEX BY PLS_INTEGER;
v_emp_tab v_emp_tabtype;
BEGIN
-- Populate the number_tab collection
v_num_tab.extend(2);
v_num_tab(1) := 7788;
v_num_tab(2) := 7902;
--
-- Show output to prove it is populated
FOR i IN 1 .. v_num_tab.COUNT
LOOP
dbms_output.put_line(v_num_tab(i));
END LOOP;
--
-- Perform a select using the collection as a table
SELECT e.*
BULK COLLECT INTO v_emp_tab
FROM emp e
INNER JOIN TABLE(v_num_tab) nt
ON (e.empno = nt.column_value);
--
-- Display the select output
FOR i IN 1 .. v_emp_tab.COUNT
LOOP
dbms_output.put_line(v_emp_tab(i).empno||' is a '||v_emp_tab(i).job);
END LOOP;
END;
You can see from this that the database TYPE collection (number_tab) was treated as a table and could be used as such.
Another option would be to simply join your two tables you are selecting from in your example:
SELECT tt.*
FROM table_two tt
INNER JOIN table_one to
ON (to.item = tt.cid);
There are other ways of doing this but the first might suit your needs best.
Hope this helps.
--Doesn't work.
--SELECT item BULK COLLECT AS 'mySelectedItems' INTO v_cids FROM table_one;
SELECT table_two.*
FROM table_two INNER JOIN v_cids
ON table_two.paramname = v_cids.mySelectedItems;
Unless I'm misunderstanding the question, this should only return results that are in the table variable.
Note: I've never used Oracle, but I imagine this case would be the same.

Resources