Oracle: Return multiple values in a function - sql

I'm trying to return a multiple values in a %rowtype from a function using two table(employees and departments), but it not working for me.
create or replace function get_employee
(loc in number)
return mv_emp%rowtype
as
emp_record mv_emp%rowtype;
begin
select a.first_name, a.last_name, b.department_name into emp_record
from employees a, departments b
where a.department_id=b.department_id and location_id=loc;
return(emp_record);
end;

The above function compiled without any error? What is the type of MV_EMP? Ideally, it should be something like below.
create or replace type emp_type
(
first_name varchar2(20)
, last_name varchar2(20)
, depart_name varchar2(20)
)
/
create or replace function get_employee
(loc in number)
return emp_type
as
emp_record emp_type;
begin
select a.first_name, a.last_name, b.department_name into emp_record
from employees a, departments b
where a.department_id=b.department_id and location_id=loc;
return(emp_record);
end;

create type t_row as object (a varchar2(10));
create type t_row_tab as table of t_row;
We will now create a function which will split the input string.
create or replace function get_number(pv_no_list in varchar2) return t_row_tab is
lv_no_list t_row_tab := t_row_tab();
begin
for i in (SELECT distinct REGEXP_SUBSTR(pv_no_list, '[^,]+', 1, LEVEL) no_list FROM dual
CONNECT BY REGEXP_SUBSTR(pv_no_list, '[^,]+', 1, LEVEL) IS NOT NULL)
loop
lv_no_list.extend;
lv_no_list(lv_no_list.last) := t_row(i.no_list);
end loop;
return lv_no_list;
end get_number;
Once the function is in place we can use the table clause of sql statement to get the desired result. As desired we got multiple values returned from the function.
SQL> select * from table(get_number('1,2,3,4'));
A
----------
1
3
2
4
So now our function is simply behaving like a table. There can be a situation where you want these comma separated values to be a part of "IN" clause.
For example :
select * from dummy_table where dummy_column in ('1,2,3,4');
But the above query will not work as '1,2,3,4' is a string and not individual numbers. To solve this problem you can simply use following query.
select * from dummy_table where dummy_column in ( select * from table(get_number('1,2,3,4')) );
References : http://www.oraclebin.com/2012/12/returning-multiple-values-from-function.html

CREATE OR replace FUNCTION Funmultiple(deptno_in IN NUMBER)
RETURN NUMBER AS v_refcursur SYS_REFCURSOR;
BEGIN
OPEN v_refcursor FOR
SELECT *
FROM emp
WHERE deptno = deptno_in;
retun v_refcursor;
END;
To call it, use:
variable x number
exec :x := FunMultiple(10);
print x

Related

Oracle: Using an array datav type in package

I have a package which contains a procedure in which I need to query a list of id numbers, which are varchar2. I will have to query this same list multiple times, and I'd rather not have redo the query. Ideally I'd like to make a function in my package that would return the list of id numbers. So I could load the array of id numbers into a variable, and then use that variable as a table throughout my procedure. I've been googling like crazy and it just doesn't seem like it's possible. Is there some way to do this?
Note: I'm not able to create a new type at the schema level, and it won't let me do this with a local collection type.
Also, I would prefer not to use dynamic sql; the main query in my procedure is enormous, and I don't want to deal with a string of that size.
I want to do something like this:
id_number_list array_type := my_function();
select *
from my_table mt
left join table(id_number_list) idl on mt.id_number = idl.column_value;
EDIT: Thanks for your help so far! MTO's answer works for a select statement join, like I described above. However, I also need to delete from the table where the id_number is in the list. This gives me an "invalid data type" error. What could explain this? The data type should always be the same: varchar2(10).
Here I create the type at package level:
type string_list is table of varchar2(10);
Then I create a function that returns the list of id numbers (for our purposes, the "action" is always c_action_refresh, so the if statement is true):
function get_modified_ids(scope in smallint, action in smallint) return string_list is
modified_ids string_list := string_list();
last_refreshed date;
begin
last_refreshed := get_last_refreshed_date(scope,action);
if action = c_action_refresh then
select id_number
bulk collect into modified_ids
from(
select id_number
from adv.hr_giving cg
join adv.pbi_dates d
on d.DATE_FULL = trunc(cg.processed_date)
where d.RELATIVE_DATE >= last_refreshed
and d.RELATIVE_DATE <= trunc(CURRENT_DATE)
and cg.fiscal_year >= adv.current_fiscal_year - 6
union
select gi.gift_donor_id as id_number
from adv.gift gi
where gi.date_added >= last_refreshed
or gi.date_modified >= last_refreshed
union
select p.pledge_donor_id as id_number
from adv.pledge_rev p
where p.date_added >= last_refreshed
or p.date_modified >= last_refreshed
union
select a.id_number
from adv.affiliation a
where a.date_added >= last_refreshed
or a.date_modified >= last_refreshed
);
end if;
return(modified_ids);
end get_modified_ids;
Then, in my procedure, I initialize a variable by calling the function:
modified_ids string_list := get_modified_ids(scope,action);
Then I try to use the list in a delete statement:
delete from advrpt.pbi_gvg_profile_ag p
where p.id_number in
(select column_value from table(modified_ids));
This gives the error ORA-00902: invalid datatype. The type of id_number is varchar2(10). And again, it works fine in a join in a select statement.
So why am I getting this error?
Don't use a variable in the package (as there would only be a single variable and if your procedure is called twice in short succession then the second set of values would overwrite the first and potentially cause issues if that happened mid-way through processing the first invocation).
Instead, create a user-defined collection type:
CREATE TYPE number_list IS TABLE OF NUMBER;
And pass a collection as an argument to the procedure:
CREATE PROCEDURE your_procedure (
i_numbers IN number_list,
o_cursor1 OUT SYS_REFCURSOR,
o_cursor2 OUT SYS_REFCURSOR
)
IS
BEGIN
OPEN o_cursor1 FOR
SELECT *
FROM your_table
WHERE id MEMBER OF i_numbers;
OPEN o_cursor2 FOR
SELECT y.*
FROM your_table y
INNER JOIN TABLE(i_numbers) n
ON (y.id = n.COLUMN_VALUE);
END;
/
Then call it using, for example:
DECLARE
v_cur1 SYS_REFCURSOR;
v_cur2 SYS_REFCURSOR;
v_id your_table.id%TYPE;
v_value your_table.value%TYPE;
BEGIN
your_procedure(number_list(1,5,13), v_cur1, v_cur2);
LOOP
FETCH v_cur1 INTO v_id, v_value;
EXIT WHEN v_cur1%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_id || ', ' || v_value);
END LOOP;
END;
/
Or create the type as part of your package:
CREATE PACKAGE your_package AS
TYPE number_list IS TABLE OF NUMBER;
PROCEDURE your_procedure (
i_numbers IN number_list,
o_cursor1 OUT SYS_REFCURSOR,
o_cursor2 OUT SYS_REFCURSOR
);
END;
/
Then create the package body:
CREATE PACKAGE BODY your_package AS
PROCEDURE your_procedure (
i_numbers IN number_list,
o_cursor1 OUT SYS_REFCURSOR,
o_cursor2 OUT SYS_REFCURSOR
)
IS
BEGIN
OPEN o_cursor1 FOR
SELECT *
FROM your_table
WHERE id IN (SELECT COLUMN_VALUE FROM TABLE(i_numbers));
OPEN o_cursor2 FOR
SELECT y.*
FROM your_table y
INNER JOIN TABLE(i_numbers) n
ON (y.id = n.COLUMN_VALUE);
END;
END;
/
Note: The MEMBER OF operator only works with collections defined in the SQL scope and not collections defined locally in a PL/SQL scope.
Then call it using, for example:
DECLARE
v_cur1 SYS_REFCURSOR;
v_cur2 SYS_REFCURSOR;
v_id your_table.id%TYPE;
v_value your_table.value%TYPE;
BEGIN
your_package.your_procedure(your_package.number_list(1,5,13), v_cur1, v_cur2);
LOOP
FETCH v_cur1 INTO v_id, v_value;
EXIT WHEN v_cur1%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_id || ', ' || v_value);
END LOOP;
END;
/
fiddle
To me, it looks as if you have it all. If not, here's an example.
Function:
SQL> create or replace function f_test
2 return sys.odcinumberlist
3 is
4 begin
5 return sys.odcinumberlist(10, 20);
6 end;
7 /
Function created.
How to use it?
SQL> set serveroutput on
SQL> declare
2 id_number_list sys.odcinumberlist := f_test;
3 begin
4 for cur_r in (select e.deptno, e.ename
5 from emp e join table(id_number_list) idl on idl.column_value = e.deptno
6 order by 1, 2
7 )
8 loop
9 dbms_output.put_line(cur_r.deptno ||' '|| cur_r.ename);
10 end loop;
11 end;
12 /
10 CLARK
10 KING
10 MILLER
20 ADAMS
20 FORD
20 JONES
20 SCOTT
20 SMITH
PL/SQL procedure successfully completed.
SQL>

How to tokenize semicolon separated column value to pass to IF statement in a function in Oracle DB

I have a table called 'config' and when I query it in following manner:
SELECT value FROM config WHERE property = 'SPECIAL_STORE_ID'
its response will be: 59216;131205;76707;167206 //... (1)
I want to tokenize the above values using semicolon as the delimiter and then use them in a user-defined Function's IF statement to compare, something like this:
IF in_store_id exists in (<delimited response from (1) above>)//...(2)
THEN do some stuff
where in_store_id is the parameter passed-in to the function
Is this possible to do as one-liner in (2) above ?
I'm on Oracle 12c
One-liner? I don't think so, but - if you're satisfied with something like this, fine.
SQL> select * From config;
VALUE PROPERTY
-------------- ----------------
7369;7499;7521 SPECIAL_STORE_ID
SQL> declare
2 in_store_id varchar2(20) := 7369;
3 l_exists number;
4 begin
5 select instr(value, ';' || in_store_id || ';')
6 into l_exists
7 from config
8 where property = 'SPECIAL_STORE_ID';
9
10 if l_exists > 0 then
11 dbms_output.put_line('that STORE_ID exists in the value');
12 else
13 dbms_output.put_line('that STORE_ID does not exist in the value');
14 end if;
15 end;
16 /
that STORE_ID exists in the value
PL/SQL procedure successfully completed.
SQL>
If the delimited response is a collection then you can use member of to check if the collection contains the ID or not like
create or replace procedure test_procedure2(p_property in varchar2, p_id in varchar2) is
type test_t is table of varchar2(20);
l_ids test_t;
begin
select regexp_substr(value, '[^;]+', 1, level) bulk collect into l_ids
from (select value from config where property = p_property)
connect by level <= regexp_count(value, ';')+1;
if(p_id member of (l_ids)) then
dbms_output.put_line('Do stuff for '||p_property||' '||p_id);
end if;
end;
/
or do it without the collection with intermediate select like
create or replace procedure test_procedure1(p_property in varchar2, p_id in varchar2) is
l_flag number(3);
begin
select count(1) into l_flag from dual where p_id in (
select regexp_substr(value, '[^;]+', 1, level)
from (select value from config where property = p_property)
connect by level <= regexp_count(value, ';')+1
);
if(l_flag > 0) then
dbms_output.put_line('Do stuff for '||p_property||' '||p_id);
end if;
end;
/
See fiddle

Calculate values from column having expression

I have a table "test_calculate" this has a column "CONN_BY" having values
column can have more than 2 number to multiply and this table may contain millions of rows , I need to get the result of the calculation from "CONN_BY" to "MVP".
I have used xmlquery for the calculation and dynamic query but these are quite slow. Is there another way which is much faster .Please suggest.
You can try the dynamic query.
Create a function which returns the calculated value and use it in your insert or select queries.
CREATE OR REPLACE FUNCTION UFN_CALCULATE (CLM_VALUE VARCHAR2)
RETURN NUMBER IS
RES_VAL NUMBER;
BEGIN
EXECUTE IMMEDIATE 'select '||CLM_VALUE||' FROM DUAL' INTO RES_VAL;
RETURN RES_VAL;
END;
You can use that function like below.
SELECT UFN_CALCULATE('.0876543 * .09876') FROM DUAL;
SELECT UFN_CALCULATE(CONN_BY) FROM YOUR_TABLE;
One option is using select ... connect by level <= regexp_count(conn_by,'[^*]+')... query for the implicit cursor within a PL/SQL code block
SQL> set serveroutput on
SQL> declare
mvp owa.nc_arr; -- numeric array to initialize each multiplication to 1 for each id value
begin
dbms_output.put_line('ID MVP');
dbms_output.put_line('--------');
for c in
(
select id,
to_number( regexp_substr(conn_by,'[^*]+',1,level) ) as nr,
level as lvl , max( level ) over ( partition by id ) as mx_lvl
from test_calculate
connect by level <= regexp_count(conn_by,'[^*]+')
and prior sys_guid() is not null
and prior conn_by = conn_by
order by id, lvl
)
loop
if c.lvl = 1 then mvp(c.id) := 1; end if;
mvp(c.id) := c.nr * mvp(c.id);
if c.lvl = c.mx_lvl then
dbms_output.put_line(c.id||' '||mvp(c.id));
end if;
end loop;
end;
/
where test_calculate is assumed to have an identity column(id)
Demo

Oracle procedure insert into table from another table

I have this code from trigger and now i need to create procedure because i cant use trigger.
CREATE OR REPLACE TRIGGER LIVE_MATCHES_TO_MATCHES
instead of insert ON LIVE_MATCHES
for each row
declare
p_priority number:= 1;
p_sport number:=0;
begin
insert into matches(sub_list , priority , sport, created)
select :new.comp_name , p_priority, p_sport,sysdate
from dual
where not exists (
select 1 from matches
where sub_list = :new.comp_name);
end;
this is procedure :
CREATE OR REPLACE PROCEDURE LIVE_MATCHES_SOCCER_T IS
p_priority number := 1;
p_sport number:=0;
begin
INSERT INTO matches("sub_list","priority","sport","created")
SELECT LIVE_MATCHES.COMP_NAME,p_priority,p_sport, sysdate
FROM LIVE_MATCHES WHERE LIVE_MATCHES.COMP_NAME <> matches.SUB_LIST;
commit;
end;
but I m getting error that matches.sub_list is invalid identifier.
How will i create procedure that will insert into table only if sub_list is different from comp_name.. I will set up job that will call this procedure every 5 minutes..
You can use MERGE statement
CREATE OR REPLACE
PROCEDURE PR_INSRT_INTO_MATCHES
IS
P_PRIORITY NUMBER := 1;
P_SPORT NUMBER := 0;
BEGIN
MERGE INTO MATCHES M USING
(SELECT DISTINCT COMP_NAME AS COMP_NAME FROM LIVE_MATCHES
) LM ON (LM.COMP_NAME=M.SUB_LIST)
WHEN NOT MATCHED THEN
INSERT
(
M.SUB_LIST,
M.PRIORITY,
M.SPORT,
M.CREATED
)
VALUES
(
LM.COMP_NAME,
P_PRIORITY,
P_SPORT,
SYSDATE
)
COMMIT;
END;
I think you want:
INSERT INTO matches("sub_list","priority","sport","created")
SELECT lm.COMP_NAME, lm.p_priority, lm.p_sport, sysdate
FROM LIVE_MATCHES lm
WHERE NOT EXISTS (SELECT 1
FROM matches m
WHERE lm.COMP_NAME <> m.SUB_LIST
);
Unless your column names are lower-case (which can only be done in Oracle by using quotes when you create them - and which is not a particularly good idea in any case), then your stored procedure won't work, as you're quoting lower-case identifiers in it. Try this instead:
CREATE OR REPLACE PROCEDURE LIVE_MATCHES_SOCCER_T IS
p_priority number := 1;
p_sport number := 0;
begin
INSERT INTO matches
( sub_list, priority, sport, created )
SELECT LIVE_MATCHES.COMP_NAME, p_priority, p_sport, sysdate
FROM live_matches
WHERE NOT EXISTS ( SELECT 1 FROM matches WHERE LIVE_MATCHES.COMP_NAME = matches.SUB_LIST );
commit;
end;

pl/sql function- pass multiple varchar2 values dynamically for SELECT ... NOT IN()

I have a table EMP with following definition:
EMP_ID NOT NULL NUMBER(6)
EMP_NAME NOT NULL VARCHAR2(25)
EMAIL NOT NULL VARCHAR2(25)
PHONE_NUMBER VARCHAR2(20)
HIRE_DATE NOT NULL DATE
JOB_ID NOT NULL VARCHAR2(10)
SALARY NUMBER(8,2)
If we want to count employees whose name not in 'King', 'Steve', 'John' we simply use this query :
SELECT count(*) FROM emp WHERE emp_name NOT IN('King','Steve','John');
Now Here is what I want above this:
What I want to do is, Create a PL/SQL Function which returns number of count according to the dynamic input, Like if we pass:
SELECT count_emp('King,Steve,John') FROM dual;
SELECT count_emp('William,Donald') FROM dual;
SELECT count_emp('Daniel') FROM dual;
needs to return appropriate count, how can I achieve this using PL/SQL FUNCTION
This is what I have tried and Needs guideline:
CREATE OR REPLACE FUNCTION count_emp(emp_nm IN varchar)
RETURN number
IS
cnt NUMBER;
BEGIN
SELECT count(*) INTO cnt FROM emp WHERE emp_name NOT IN(emp_nm);
RETURN cnt;
END;
it is giving result for single name, but how can I split/format multiple input(i.e. emp_nm) to pass in NOT IN()?
Try like this,
CREATE OR REPLACE
FUNCTION count_emp(emp_nm IN VARCHAR)
RETURN NUMBER
IS
cnt NUMBER;
BEGIN
SELECT count(*)
INTO cnt
FROM emp
WHERE ename NOT IN(
SELECT regexp_substr (emp_nm, '[^,]+', 1, ROWNUM)
FROM dual
CONNECT BY LEVEL <= LENGTH (regexp_replace (emp_nm, '[^,]+')) + 1);
RETURN cnt;
END;
You can try dynamic sql:
CREATE OR REPLACE FUNCTION count_emp(emp_nm IN varchar)
RETURN number
IS
cnt NUMBER;
BEGIN
Execute immediate 'SELECT count(*) FROM emp WHERE emp_name NOT IN(' || emp_nm || ')' returning into cnt;
RETURN cnt;
END;
You can also use MEMBER OF. Here is a snippet. Hope this helps!
-- Create a type in SQL
CREATE OR REPLACE TYPE t_emp_name AS TABLE OF VARCHAR2 (10);
-- use MEMBER OF to use your list as IN parameter
CREATE OR REPLACE FUNCTION count_emp (emp_nm IN t_emp_name)
RETURN NUMBER
IS
cnt NUMBER;
BEGIN
SELECT COUNT (*)
INTO cnt
FROM emp
WHERE emp_name NOT MEMBER OF (emp_nm);
RETURN cnt;
END;
-- Assign values to the list, you can do it dynamically as well. Call the function
DECLARE
l_emp_name_list t_emp_name;
lv_count NUMBER;
BEGIN
l_emp_name_list := t_emp_name ('King', 'Steve'); --add more names as needed
lv_count := count_emp (l_emp_name_list);
END;