How to retrieve distinct values from nested table in Oracle - oracle

I have a problem where in I need to retrieve distinct values out of a nested table collection.
Example code:
FUNCTION get_part_atts
( p_gp_id IN dummy_di_parts.di_gp_id%TYPE
, p_attribute_name IN dummy_part_attr_def.attribute_name%TYPE
, p_sel1 IN dummy_di_part_atts.sel1%TYPE DEFAULT NULL
, p_sel2 IN dummy_di_part_atts.sel2%TYPE DEFAULT NULL
, p_sel3 IN dummy_di_part_atts.sel3%TYPE DEFAULT NULL )
RETURN dummy_pkg.part_atts_tabtype
RESULT_CACHE
IS
l_dummy_part_seq dummy_di_parts.dummy_part_seq%TYPE;
l_attribute_id dummy_part_attr_def.attribute_id%TYPE;
l_default_value dummy_part_attr_def.default_value%TYPE;
l_return part_atts_tabtype := part_atts_tabtype();
BEGIN
SELECT p.dummy_part_seq
INTO l_dummy_part_seq
FROM dummy_di_parts p
WHERE p.di_gp_id = p_gp_id
AND p.di_part_status = 'ACTIVE';
EXCEPTION
WHEN NO_DATA_FOUND
THEN RAISE_APPLICATION_ERROR(-20021,'No active parts found '||p_gp_id||'.');
WHEN TOO_MANY_ROWS
THEN RAISE_APPLICATION_ERROR(-20022,'More than one active part '||p_gp_id||'.');
END part_lookup;
BEGIN
SELECT pad.attribute_id
, pad.default_value
INTO l_attribute_id
, l_default_value
FROM dummy_part_attr_def pad
WHERE pad.attribute_name = p_attribute_name;
EXCEPTION
WHEN NO_DATA_FOUND
THEN RAISE_APPLICATION_ERROR(-20023,p_attribute_name||' is not a valid attribute name.');
END attribute_def_lookup;
SELECT pa.attribute_value
BULK COLLECT INTO l_return
FROM dummy_di_part_atts pa
WHERE pa.dummy_part_seq = l_dummy_part_seq
AND pa.attribute_id = l_attribute_id
AND ( p_sel1 IS NULL OR
pa.sel1 = p_sel1 )
AND ( p_sel2 IS NULL OR
pa.sel2 = p_sel2 )
AND ( p_sel3 IS NULL OR
pa.sel3 = p_sel3 );
RETURN l_return;
END get_part_atts;
Now I need to select Distinct from the collection l_return which is a nested table type
Please help

In Oracle 12C you can do this:
select distinct attribute_value
from table (get_part_atts(...))
Prior to 12C you could do similar, but only if the type was defined in the database using CREATE TYPE rather than declared in a package specification.

Try using "multiset union distinct". For example, a function that splits a delimited string into tokens and returns a nested table:
CREATE OR REPLACE function fn_split(i_string in varchar2, i_delimiter in varchar2 default ',', b_dedup_tokens in number default 0)
return sys.dbms_debug_vc2coll
as
l_tab sys.dbms_debug_vc2coll;
begin
select regexp_substr(i_string,'[^' || i_delimiter || ']+', 1, level)
bulk collect into l_tab
from dual
connect by regexp_substr(i_string, '[^' || i_delimiter || ']+', 1, level) is not null
order by level;
if (b_dedup_tokens > 0) then
return l_tab multiset union distinct l_tab;
end if;
return l_tab;
end;
Testing:
select * from table(fn_split('x,x,y,z', ',', 1));
Output:
COLUMN_VALUE
x
y
z

Related

Oracle SQL function - wrong data type in function call

I have a function inside my package that is meant to split up a comma-separated varchar2 input into rows, ie. 'one, two, three' into:
one
two
three
I have declared the function as:
function unpack_list(
string_in in varchar2
) return result_str
is
result_rows result_str;
begin
with temp_table as (
SELECT distinct trim(regexp_substr(string_in, '[^,]+', 1, level)) str
FROM (SELECT string_in FROM dual) t
CONNECT BY instr(string_in, ',', 1, level - 1) > 0)
select str bulk collect into result_rows from temp_table;
RETURN result_rows;
end;
and the return type as:
type result_str is table of varchar2(100);
However, calling the function like:
select * from unpack_list('one1, two2')
gives the following error:
ORA-00902: Invalid datatype
any ideas what causes this?
You are calling a PL/SQL function that returns a PL/SQL collection type (both defined in your package) from a SQL context. You can't do that directly. You can call the function from a PL/SQL context, assigning the result to a variable of the same type, but that isn't how you're trying to use it. db<>fiddle showing your set-up, your error, and it working in a PL/SQL block.
You could declare the type at schema level instead, as #Littlefoot showed:
create type result_str is table of varchar2(100);
and remove the package definition, which would clash; that works for both SQL and PL/SQL (db<>fiddle).
Or if you can't create a schema-level type, you could use a built-in one:
function unpack_list(
string_in in varchar2
) return sys.odcivarchar2list
is
result_rows sys.odcivarchar2list;
begin
with temp_table as (
SELECT distinct trim(regexp_substr(string_in, '[^,]+', 1, level)) str
FROM (SELECT string_in FROM dual) t
CONNECT BY instr(string_in, ',', 1, level - 1) > 0)
select str bulk collect into result_rows from temp_table;
RETURN result_rows;
end;
which also works for both SQL and PL/SQL (db<>fiddle).
Or you could use a pipelined function, with your PL/SQL collection type:
function unpack_list(
string_in in varchar2
) return result_str pipelined
is
begin
for r in (
SELECT distinct trim(regexp_substr(string_in, '[^,]+', 1, level)) str
FROM (SELECT string_in FROM dual) t
CONNECT BY instr(string_in, ',', 1, level - 1) > 0)
loop
pipe row (r.str);
end loop;
RETURN;
end;
which works in SQL, or in SQL running within PL/SQL, but not with direct assignment to a collection variable (db<>fiddle).
Which approach you take depends on how you need to call the function really. there may be some performance differences, but you might not notice unless they are called repeatedly and intensively.
The reason of the error was described earlier, so I will post another possible solution. For Oracle 19c (version 19.7) and above you may skip creation of table type and use SQL_MACRO addition. Returned query will be integrated into the main query.
create function unpack_list (
string_in varchar2
)
return clob
sql_macro(table)
is
begin
return q'[
select distinct
trim(regexp_substr(
unpack_list.string_in,
'[^,]+', 1, level
)) as str
from dual
connect by
instr(
unpack_list.string_in,
',', 1, level - 1
) > 0
]';
end;
/
select *
from unpack_list(
string_in => 'one,two'
)
| STR |
| :-- |
| one |
| two |
db<>fiddle here

How to pass multiple values for a parameter in a PIPELINED table function in oracle

I have created a function in Oracle with 2 parameters. So when I run the query I want to pass multiple values to each parameter.
I tried using below changes in the query:
coul_1 in ('||par1||') and colu_2 in ('||par2||')
But it is not fetching the data.
How to fetch the data when I give multiple values to different declared parameters.Eg:
select * from table(fun_name('val1','val2'))
val1 will have a1,a2,a3
val2 will have b1,b2,b3
Here is the function code:
CREATE OR REPLACE FUNCTION JOBRUN_STATUS_MONITOR_F(
own_name IN VARCHAR2,
status IN VARCHAR2)
RETURN JOBRUN_STATUS_EDW_1 PIPELINED
IS
L_TAB JOBRUN_STATUS_MONITOR_EDW_1;
JR_STATUS NUMBER (38);
CURSOR jobrun_1_cr (OW_N VARCHAR2, STS VARCHAR2)
IS
SELECT *
FROM JOBRUN A,
JOBMST B,
owner C
WHERE A.JOBMST_ID = B.JOBMST_ID
AND C.OWNER_NAME = OW_N
AND A.JOBRUN_STATUS = STS ;
BEGIN V_OWN_NAME := own_name;
V_STATUS := status;
IF jobrun_1_cr%ISOPEN THEN
CLOSE jobrun_1_cr;
END IF;
OPEN jobrun_1_cr (own_name, JR_STATUS);
CLOSE jobrun_1_cr;
END JOBRUN_STATUS_MONITOR_F;
/
It sounds like you want to call the function three times, passing (a1, b1), (a2, b2) and (a3, b3). One way would be to generate an inline view containing the values you want to pass, and query it including a call to your function.
Demo pipelined function:
create or replace function demo_pipefunc
( p_own_name in varchar2
, p_status in varchar2 )
return sys.dbms_debug_vc2coll
pipelined
as
l_result long;
begin
for i in 1..3 loop
l_result := p_own_name ||';'|| p_status ||';'|| i;
pipe row (l_result);
end loop;
return;
end demo_pipefunc;
Demo call:
with params (own_name, status) as
( select 'a1', 'b1' from dual union all
select 'a2', 'b2' from dual union all
select 'a3', 'b3' from dual
)
select t.*
from params
cross join table(demo_pipefunc(own_name, status)) t
Output:
COLUMN_VALUE
a1;b1;1
a1;b1;2
a1;b1;3
a2;b2;1
a2;b2;2
a2;b2;3
a3;b3;1
a3;b3;2
a3;b3;3
try this:
CREATE OR REPLACE FUNCTION JOBRUN_STATUS_MONITOR_F (own_name IN VARCHAR2, status IN VARCHAR2) RETURN JOBRUN_STATUS_EDW_1 PIPELINED
IS L_TAB JOBRUN_STATUS_MONITOR_EDW_1;
JR_STATUS NUMBER (38);
CURSOR jobrun_1_cr (OW_N VARCHAR2, STS VARCHAR2)
IS SELECT *
FROM JOBRUN A, JOBMST B, owner C
WHERE A.JOBMST_ID = B.JOBMST_ID
AND C.OWNER_NAME = OW_N
AND A.JOBRUN_STATUS = STS;
BEGIN
V_OWN_NAME := REPLACE(own_name,'"','''');
V_STATUS := REPLACE(status,'"','''');
IF jobrun_1_cr%ISOPEN THEN
CLOSE jobrun_1_cr;
END IF;
OPEN jobrun_1_cr (own_name, JR_STATUS);
CLOSE jobrun_1_cr;
END JOBRUN_STATUS_MONITOR_F;
then call the procedure like this:
select * from table(fun_name('"a1","b1","c1"','"a1","b1","c1"'));
Your procedure may have other issues with the use of undeclared variables.

Oracle lookup function in loop returns rowtype how to get fields from rowtype

I have a procedure that runs a select ( I tested that is good returns 56 records )
then when I run a cursor I want to pass 3 fields to a function ( see above ) that will
lookup/select a record from a table that contains 15 million records ( 10 years worth ).
It returns a rowtype that I want to then extract the fields from this rowtype record to
run an insert with both the records from the 1st select and the additional fields acquired
from the lookup function.
If I run the procedure the console prints out my test msgs but when I try to run
select * bulk collect into v_tab_proc_claim_recs from v_processed_claim;
it doesn't compile due to Error(97,65): PL/SQL: ORA-00942: table or view does not exist
as if either of these are not Tables.
Am I doing this right... how can I do it, why can't it see the table I'm trying to extract to ?
Should I do this some other way..
Thanks for any help/suggestions :)
The function is below....
create or replace function get_processed_claim_rec(
p_provider VARCHAR2,
p_rx VARCHAR2,
p_record_no NUMBER
)
return i_idb.processed_claim%rowtype
as
l_claim_record i_idb.processed_claim%rowtype;
begin
select * into l_claim_record from i_idb.processed_claim
where source_date = p_provider
AND rx = p_rx
AND rec_no = p_record_no;
return(l_claim_record);
end;
And the procedure is....
create or replace PROCEDURE import_mailer_data
AS
-------------------------------
/**
for the lookup table
**/
v_processed_claim i_idb.processed_claim%rowtype;
TYPE proc_claim_recs IS TABLE OF v_processed_claim%ROWTYPE INDEX BY PLS_INTEGER;
v_tab_proc_claim_recs proc_claim_recs;
--------------------------------
CURSOR myCursor
IS
SELECT *
from
(
SELECT
j.create_date as open_date,
case when (j.create_date < (sysdate - 20) )
then 'POD'
else 'REG'
end as priority,
c.division,
c.unit,
--p.refill as days_supply,
--p.din_name,
'CM_JOHN' as log_code,
c.first_name,
c.last_name,
--p.UNLISTED_compound,
--p.intervention_code,
--p.substitution,
--p.confirm,
c.PROVIDER,
c.rx,
c.DISPENSE_DATE,
c.DIN,
c.QTY,
c.DIN_COST_PAID,
c.DISP_FEE_PAID,
c.PAID_AMOUNT,
c.SOURCE_DATE,
c.RECORD_NO,
c.RELATIONSHIP,
c.INSURER_NO,
c.GROUP_NO,
c.CERTIFICATE,
c.BIRTH_DATE,
c.USER_ID,
--p.rej_code --v_seq_no
rank() over
(
partition by c.provider, c.rx, c.record_no Order by c.provider desc, c.rx desc
) as RNK
FROM AUDITCOLLECTIONS.MAILER_CLAIMS c,
AUDITCOLLECTIONS.MAILER_JOBS j
WHERE MAILER_JOB_DETAIL_ID IN
(SELECT MAILER_JOB_DETAIL_ID
FROM AUDITCOLLECTIONS.MAILER_JOB_DETAILS
WHERE MAILER_JOB_ID IN
( SELECT MAILER_JOB_ID FROM AUDITCOLLECTIONS.MAILER_JOBS
)
)
AND ( c.PROVIDER, c.rx, c.record_no ) NOT IN
( SELECT provider, rx, rec_no FROM AUDITCOLLECTIONS.COLLECTION_AUDIT_STAGING
)
AND j.create_date > (sysdate - 30)
AND c.provider = '2010500042'
) A_Latest
where A_Latest.RNK = 1;
BEGIN
v_report_id := audit_load.create_loaded_report(v_report_type_id);
FOR curRec IN myCursor
LOOP
BEGIN
dbms_output.put_line ('===>>>> PRINTING TEST1 = ');
v_processed_claim := get_processed_claim_rec(curRec.PROVIDER, curRec.RX, curRec.RECORD_NO);
select * bulk collect into v_tab_proc_claim_recs from v_processed_claim;
END LOOP;
audit_load.update_status_to_loaded(v_report_id);
END import_mailer_data;
You can do this:
FOR curRec IN myCursor
LOOP
v_processed_claim :=
get_processed_claim_rec(curRec.PROVIDER, curRec.RX, curRec.RECORD_NO);
v_tab_proc_claim_recs (v_tab_proc_claim_recs.COUNT+1) := v_processed_claim;
END LOOP;
Or simplify to:
FOR curRec IN myCursor
LOOP
v_tab_proc_claim_recs (v_tab_proc_claim_recs.COUNT+1) :=
get_processed_claim_rec(curRec.PROVIDER, curRec.RX, curRec.RECORD_NO);
END LOOP;

PLS-00497: cannot mix between single row and multi-row (BULK) in INTO list

I have created a procure to display the data in two table using BULK COLLECT, but i keep getting this error.
PLS-00497: cannot mix between single row and multi-row (BULK) in INTO list
However it works if i remove the BULK COLLECT and include a where clause in the statement.
create or replace PROCEDURE sktReport IS
TYPE inventory_table_type is RECORD (
v_WH_ID INVENTORY.WH_ID%TYPE,
v_wa_Product_quantity_id Product_quantity.ST_ID%TYPE);
v_inventory_table inventory_table_type;
BEGIN
SELECT INVENTORY.WH_ID, Product_quantity.ST_ID,
BULK COLLECT INTO
v_inventory_table.v_WH_ID,
v_inventory_table.v_wa_Product_quantity_id,
FROM INVENTORY
INNER JOIN Product_quantity
ON Product_quantity.ST_ID = INVENTORY.ST_ID;
FOR i IN v_inventory_table.v_WH_ID..v_inventory_table.v_WH_ID
LOOP
DBMS_OUTPUT.PUT_LINE ('ID : ' || v_inventory_table.v_WH_ID
|| ' quantity ID : ' || v_inventory_table.v_in_Product_quantity_id);
END LOOP;
END;
Here's a simple showcase, I've just written that might help you :)
SET SERVEROUTPUT ON;
DECLARE
TYPE t_some_type IS RECORD (
the_id NUMBER
,the_name VARCHAR2(1)
);
TYPE t_some_type_tab IS TABLE OF t_some_type;
lt_some_record t_some_type;
lt_some_array t_some_type_tab := NEW t_some_type_tab();
BEGIN
WITH some_values AS (
SELECT
DECODE(LEVEL,1,1,2,2,3,3,4,4) AS the_id
,DECODE(LEVEL,1,'A',2,'B',3,'C',4,'D') AS the_name
FROM
dual
CONNECT BY LEVEL < 5
)
SELECT
sv.the_id
,sv.the_name
BULK COLLECT INTO -- use this to select into an array/collection type variable
lt_some_array
FROM
some_values sv;
DBMS_OUTPUT.PUT_LINE(lt_some_array.COUNT);
WITH some_values AS (
SELECT
DECODE(LEVEL,1,1,2,2,3,3,4,4) AS the_id
,DECODE(LEVEL,1,'A',2,'B',3,'C',4,'D') AS the_name
FROM
dual
CONNECT BY LEVEL < 5
)
SELECT
sv.the_id
,sv.the_name
INTO -- use this to select into a regular variables
lt_some_record.the_id
,lt_some_record.the_name
FROM
some_values sv
WHERE
sv.the_id = 1;
DBMS_OUTPUT.PUT_LINE(lt_some_record.the_id||': '||lt_some_record.the_name);
-- you can also insert such record into your array type variable
lt_some_array := NEW t_some_type_tab();
lt_some_array.EXTEND; -- extend the array type variable (so it could store one more element, than now - which was 0)
lt_some_array(lt_some_array.LAST) := lt_some_record; -- assign the first element of array type variable
DBMS_OUTPUT.PUT_LINE(lt_some_array.COUNT||' '||lt_some_array(lt_some_array.LAST).the_id||': '||lt_some_array(lt_some_array.LAST).the_name);
END;
/
Also, since you want to iterate through your results, you can just use cursor (implicit or explicit) e.g.
DECLARE
-- cursor declaration
CURSOR c_some_cursor IS
WITH some_values AS (
SELECT
DECODE(LEVEL,1,1,2,2,3,3,4,4) AS the_id
,DECODE(LEVEL,1,'A',2,'B',3,'C',4,'D') AS the_name
FROM
dual
CONNECT BY LEVEL < 5
)
SELECT
sv.the_id
,sv.the_name
FROM
some_values sv;
BEGIN
-- using explicit, earlier declared cursor
FOR c_val IN c_some_cursor
LOOP
DBMS_OUTPUT.PUT_LINE(c_val.the_id||': '||c_val.the_name);
END LOOP;
-- using implicit, not declared cursor
FOR c_val IN (
WITH some_values AS (
SELECT
DECODE(LEVEL,1,1,2,2,3,3,4,4) AS the_id
,DECODE(LEVEL,1,'A',2,'B',3,'C',4,'D') AS the_name
FROM
dual
CONNECT BY LEVEL < 5
)
SELECT
sv.the_id
,sv.the_name
FROM
some_values sv
)
LOOP
DBMS_OUTPUT.PUT_LINE(c_val.the_id||': '||c_val.the_name);
END LOOP;
END;
/
Here i have demonstrated a simple example to replicate your scenario. Please see below code. This may help you out.
SET serveroutput ON;
DECLARE
TYPE AV_TEST
IS
RECORD
(
lv_att1 PLS_INTEGER,
lv_att2 PLS_INTEGER );
type av_test_tab
IS
TABLE OF av_test;
av_test_tab_av av_test_tab;
BEGIN
NULL;
SELECT LEVEL,
LEVEL+1 BULK COLLECT
INTO av_test_tab_av
FROM DUAL
CONNECT BY LEVEL < 10;
dbms_output.put_line(av_test_tab_av.count);
FOR I IN av_test_tab_av.FIRST..av_test_tab_av.LAST
LOOP
dbms_output.put_line('working fine '||av_test_tab_av(i).lv_att1||' '||av_test_tab_av(i).lv_att2);
END LOOP;
END;

Oracle PLSQL: Help Required: Parsing String Collection or Concatenated String

Whenever the length of string l_long_string is above 4000 characters, the following code is throwing an error:
ORA-01460: unimplemented or unreasonable conversion requested
Instead of the nested regexp_substr query, when I try to use
SELECT column_value
FROM TABLE(l_string_coll)
it throws:
ORA-22905: cannot access rows from a non-nested table item
How can I modify the dynamic query?
Notes:
- l_string_coll is of type DBMS_SQL.VARCHAR2S, and comes as input to my procedure (here, i have just shown as an anonymous block)
- I'll have to manage without creating a User-defined Type in DB schema, so I am using the in-built DBMS_SQL.VARCHAR2S.
- This is not the actual business procedure, but is close to this. (Can't post the original)
- Dynamic query has to be there since I am using it for building the actual query with session, current application schema name etc.
/*
CREATE TABLE some_other_table
(word_id NUMBER(10), word_code VARCHAR2(30), word VARCHAR2(255));
INSERT INTO some_other_table VALUES (1, 'A', 'AB');
INSERT INTO some_other_table VALUES (2, 'B', 'BC');
INSERT INTO some_other_table VALUES (3, 'C', 'CD');
INSERT INTO some_other_table VALUES (4, 'D', 'DE');
COMMIT;
*/
DECLARE
l_word_count NUMBER(10) := 0;
l_counter NUMBER(10) := 0;
l_long_string VARCHAR2(30000) := NULL;
l_dyn_query VARCHAR2(30000) := NULL;
l_string_coll DBMS_SQL.VARCHAR2S;
BEGIN
-- l_string_coll of type DBMS_SQL.VARCHAR2S comes as Input to the procedure
FOR i IN 1 .. 4100
LOOP
l_counter := l_counter + 1;
l_string_coll(l_counter) := 'AB';
END LOOP;
-- Above input collection is concatenated into CSV string
FOR i IN l_string_coll.FIRST .. l_string_coll.LAST
LOOP
l_long_string := l_long_string || l_string_coll(i) || ', ';
END LOOP;
l_long_string := TRIM(',' FROM TRIM(l_long_string));
dbms_output.put_line('Length of l_long_string = ' || LENGTH(l_long_string));
/*
Some other tasks in PLSQL done successfully using the concatenated string l_long_string
*/
l_dyn_query := ' SELECT COUNT(*)
FROM some_other_table
WHERE word IN ( SELECT TRIM(REGEXP_SUBSTR(str, ''[^,]+'', 1, LEVEL)) word
FROM ( SELECT :string str FROM SYS.DUAL )
CONNECT BY TRIM(REGEXP_SUBSTR(str, ''[^,]+'', 1, LEVEL)) IS NOT NULL )';
--WHERE word IN ( SELECT column_value FROM TABLE(l_string_coll) )';
EXECUTE IMMEDIATE l_dyn_query INTO l_word_count USING l_long_string;
dbms_output.put_line('Word Count = ' || l_word_count);
EXCEPTION
WHEN OTHERS
THEN
dbms_output.put_line('SQLERRM = ' || SQLERRM);
dbms_output.put_line('FORMAT_ERROR_BAKCTRACE = ' || dbms_utility.format_error_backtrace);
END;
/
How can I modify the dynamic query?
First of all. Based on the code you've provided, there is absolutely no need to use dynamic, native or DBMS_SQL dynamic SQL at all.
Secondly, SQL cannot operate on "strings" that are greater than 4K bytes in length(Oracle versions prior to 12c), or 32K bytes(Oracle version 12cR1 and up, if MAX_STRING_SIZE initialization parameter is set to EXTENDED).
PL/SQL, on the other hand, allows you to work with varchar2() character strings that are greater than 4K bytes (up to 32Kb) in length. If you just need to count words in a comma separated sting, you can simply use regexp_count() regular expression function(Oracle 11gr1 and up) as follows:
set serveroutput on;
set feedback off;
clear screen;
declare
l_str varchar2(100) := 'aaa,bb,ccc,yyy';
l_numOfWords number;
begin
l_numOfWords := regexp_count(l_str, '[^,]+');
dbms_output.put('Number of words: ');
dbms_output.put_line(to_char(l_numOfWords));
end;
Result:
Number of words: 4

Resources