How to use column name dynamically with bulk collect - sql

How can I use columns dynamically with bulk collect?
The code shown here is throwing errors:
SET serverout ON
DECLARE
r_emp SYS.ODCINUMBERLIST := SYS.ODCINUMBERLIST();
t_emp SYS.ODCINUMBERLIST := SYS.ODCINUMBERLIST('CUST_ID');
v_array SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST(
'CUST_TYPE',
'CERT_TYPE_NAME',
'CERT_NBR',
'NEW_PARENT_CUST_ID');
BEGIN
DBMS_OUTPUT.ENABLE;
FOR i IN 1..v_array.COUNT LOOP
r_emp.extend;
EXECUTE IMMEDIATE
'SELECT '||t_emp||' FROM CUSTOMER_PROFILE where '||v_array(i)||' is null'
BULK COLLECT INTO r_emp(i);
for k in 1..r_emp(i).count loop
dbms_output.put_line(v_array(i) || ': ' || r_emp(k));
end loop;
END LOOP;
END;
Error report:
ORA-06550: line 15, column 7:
PLS-00306: wrong number or types of arguments in call to '||'

r_emp is an array and not an array or arrays so you cannot BULK COLLECT INTO r_emp(i), instead, you need to BULK COLLECT INTO r_emp (and need to neither initialise nor extend it as that is done automatically by BULK COLLECT).
You also cannot concatenate a string with a VARRAY so 'SELECT '||t_emp will not work (and is where your error message comes from). Instead, you can concatenate two strings so 'SELECT '||t_emp(1) would work.
And you cannot initialise a SYS.ODCINUMBERLIST with the value 'CUST_ID' as it is a string and not a number.
Instead, you can use:
DECLARE
r_emp SYS.ODCINUMBERLIST;
t_emp SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST('CUST_ID');
v_array SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST(
'CUST_TYPE',
'CERT_TYPE_NAME',
'CERT_NBR',
'NEW_PARENT_CUST_ID'
);
BEGIN
DBMS_OUTPUT.ENABLE;
FOR i IN 1..v_array.COUNT LOOP
FOR j IN 1..t_emp.COUNT LOOP
EXECUTE IMMEDIATE
'SELECT '||t_emp(j)||' FROM CUSTOMER_PROFILE where '||v_array(i)||' is null'
BULK COLLECT INTO r_emp;
FOR k IN 1..r_emp.COUNT LOOP
dbms_output.put_line(v_array(i) || ': ' || r_emp(k));
END LOOP;
END LOOP;
END LOOP;
END;
/
Which, for the sample data:
create table customer_profile (
cust_id NUMBER,
cust_type VARCHAR2(20),
cert_type_name VARCHAR2(20),
cert_nbr VARCHAR2(20),
new_parent_cust_id VARCHAR2(20)
);
INSERT INTO customer_profile
SELECT 1, NULL, 'a', 'a', 'a' FROM DUAL UNION ALL
SELECT 2, 'b', NULL, 'b', 'b' FROM DUAL UNION ALL
SELECT 3, 'c', 'c', NULL, 'c' FROM DUAL UNION ALL
SELECT 4, 'd', 'd', 'd', NULL FROM DUAL UNION ALL
SELECT 5, NULL, NULL, NULL, NULL FROM DUAL;
Outputs:
CUST_TYPE: 1
CUST_TYPE: 5
CERT_TYPE_NAME: 2
CERT_TYPE_NAME: 5
CERT_NBR: 3
CERT_NBR: 5
NEW_PARENT_CUST_ID: 4
NEW_PARENT_CUST_ID: 5
fiddle

Related

SQL Oracle object%rowtype dynamic

I have an object which is a ROWTYPE from a table and need to recover some of the columns to an array list.
For example:
The ROWTYPE contents Name1, Last_Name1, Id1, Adress1...Name25,Last_Name25,Id25,Adress25 in columns.
I need to know if I can recover them by a dynamic way with a loop to an array type like this:
Name(1-25),Last_Name(1-25),Id(1-25),Adress(1-25).
Given a simplified example of the problem with the table:
CREATE TABLE table_name (id1, name1, id2, name2, id3, name3, id4, name4) AS
SELECT 1, 'N1', 2, 'N2', 3, 'N3', 4, 'N4' FROM DUAL;
I am assuming, that to get the %ROWTYPE variable, you are using something like:
DECLARE
data TABLE_NAME%ROWTYPE;
BEGIN
SELECT * INTO data FROM table_name FETCH FIRST ROW ONLY;
DBMS_OUTPUT.PUT_LINE('1: ' || data.id1 || ', ' || data.name1);
DBMS_OUTPUT.PUT_LINE('2: ' || data.id2 || ', ' || data.name2);
DBMS_OUTPUT.PUT_LINE('3: ' || data.id3 || ', ' || data.name3);
DBMS_OUTPUT.PUT_LINE('4: ' || data.id4 || ', ' || data.name4);
END;
/
Rather than trying to dynamically access the %ROWTYPE record, why do you not bypass the problem and separate the values in the SELECT statement using UNPIVOT in a cursor and then you can use %ROWTYPE on the cursor (rather than the table):
DECLARE
CURSOR cur IS
SELECT id, name
FROM (SELECT * FROM table_name FETCH FIRST ROW ONLY)
UNPIVOT (
(id, name)
FOR idx IN (
(id1, name1) AS 1,
(id2, name2) AS 2,
(id3, name3) AS 3,
(id4, name4) AS 4
)
);
TYPE cur_row_arr IS TABLE OF cur%ROWTYPE;
rw cur%ROWTYPE;
arr cur_row_arr := cur_row_arr();
BEGIN
OPEN cur;
LOOP
FETCH cur INTO rw;
EXIT WHEN cur%NOTFOUND;
arr.EXTEND;
arr(arr.COUNT) := rw;
END LOOP;
CLOSE cur;
FOR i IN 1 .. arr.COUNT LOOP
DBMS_OUTPUT.PUT_LINE( i || ': ' || arr(i).id || ', ' || arr(i).name );
END LOOP;
END;
/
db<>fiddle here
Don't try to do it dynamically, just hard-code the values.
Given the simplified example:
CREATE TABLE table_name (id1, name1, id2, name2, id3, name3, id4, name4) AS
SELECT 1, 'N1', 2, 'N2', 3, 'N3', 4, 'N4' FROM DUAL;
Then:
CREATE PROCEDURE test(
v_row IN TABLE_NAME%ROWTYPE
)
IS
TYPE details_type IS RECORD(
id TABLE_NAME.ID1%TYPE,
name TABLE_NAME.NAME1%TYPE
);
TYPE details_tab_type IS TABLE OF details_type;
details details_tab_type := details_tab_type();
BEGIN
details.EXTEND(4);
details(1) := details_type(v_row.id1, v_row.name1);
details(2) := details_type(v_row.id2, v_row.name2);
details(3) := details_type(v_row.id3, v_row.name3);
details(4) := details_type(v_row.id4, v_row.name4);
FOR i IN 1 .. details.COUNT LOOP
DBMS_OUTPUT.PUT_LINE(i || ': ' || details(i).id || ', ' || details(i).name);
END LOOP;
END;
/
All you need to do is write the first assignment statement details(1) := details_type(v_row.id1, v_row.name1); and then copy-paste multiple copies and increment the numbers by 1 each time.
Then you can call it using:
DECLARE
v_row TABLE_NAME%ROWTYPE;
BEGIN
SELECT * INTO v_row FROM table_name FETCH FIRST ROW ONLY;
test(v_row);
END;
/
Which outputs:
1: 1, N1
2: 2, N2
3: 3, N3
4: 4, N4
db<>fiddle here

select in a loop in oracle sql?

My stored procedure is used for getting string values of ids separate by ',' from a string contains names in table AUTH_GROUPS.
for example:
id name
1 role_1
2 role_2
3 role_3
the input is: 'role_1,role_2,role_3'
the output is: '1,2,3'
CREATE OR REPLACE PROCEDURE PROCEDURE1(P_ERROR_MSG OUT VARCHAR2, P_ROLE_STRING IN VARCHAR2 ) AS
BEGIN
DECLARE
lvOutPut VARCHAR2(2000);
vId varchar2(1000);
BEGIN
lvOutPut := '';
FOR i IN
(SELECT trim(regexp_substr(P_ROLE_STRING, '[^,]+', 1, LEVEL)) l
FROM dual
CONNECT BY LEVEL <= regexp_count(P_ROLE_STRING, ',')+1
)
LOOP
select id into vId from AUTH_GROUPS where NAME = i; --this line got error 'expression is of wrong type'
lvOutPut := lvOutPut || vId || ',';
END LOOP;
P_ERROR_MSG := lvOutPut;
P_ERROR_MSG := substr(P_ERROR_MSG, 1, LENGTH(P_ERROR_MSG) - 1);
END;
END PROCEDURE1;
But it has an error in the line that I commented. I tried i.1 or i.value but still got errors.
You need to use the actual column name. i is loop handle name.
....
BEGIN
lvOutPut := '';
FOR i IN
(SELECT trim(regexp_substr(P_ROLE_STRING, '[^,]+', 1, LEVEL)) l -- this is column name to be used inside the loop
FROM dual
CONNECT BY LEVEL <= regexp_count(P_ROLE_STRING, ',')+1
)
LOOP
select id into vId from AUTH_GROUPS where NAME = i.l; -- change here
...
/* Formatted on 6/23/2020 2:08:34 PM (QP5 v5.354) */
CREATE OR REPLACE PROCEDURE PROCEDURE1 (P_ERROR_MSG OUT VARCHAR2,
P_ROLE_STRING IN VARCHAR2)
AS
BEGIN
DECLARE
lvOutPut VARCHAR2 (2000);
vId VARCHAR2 (1000);
BEGIN
lvOutPut := '';
FOR i IN ( SELECT TRIM (REGEXP_SUBSTR (P_ROLE_STRING,
'[^,]+',
1,
LEVEL)) l
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT (P_ROLE_STRING, ',') + 1)
LOOP
SELECT id
INTO vId
FROM AUTH_GROUPS
WHERE NAME = i.l; --this line got error 'expression is of wrong type'
lvOutPut := lvOutPut || vId || ',';
END LOOP;
P_ERROR_MSG := lvOutPut;
P_ERROR_MSG := SUBSTR (P_ERROR_MSG, 1, LENGTH (P_ERROR_MSG) - 1);
END;
END PROCEDURE1;

Return Multiple Values from Oracle Function

I want to create a function that returns multiple rows into a table that is of object type.
I have created an object and a nested table object and now when I run the function there is an error which says
PL/SQL: SQL Statement ignored
PL/SQL: ORA-00947: not enough values
-- Object type creation
create or replace type test_object_sn as object
(
column_1 varchar2(30),
column_2 varchar2(30),
column_3 number
);
-- Table of object
create or replace type test_otable_sn as table of test_object_sn;
-- function (where I get an error)
create or replace function load_test_object_sn
return test_otable_sn
as
details test_otable_sn;
begin
with ad as (select 'a', 'b', 4 from dual
union all
select 'r', '5', 3 from dual
union all
select 'g', 's', 3 from dual)
select * into details from ad;
return details;
end;
I want to have the test_otable_sn table object loaded with the data and then query it using the table() function via my load_test_object_sn function
e.g. select * from table(load_test_object_sn);
Update:
do you know how to modify this for scenario whereby I have an sql
statement contained in a string variable to execute?
Yes, we can use a cursor reference (SYS_REFCURSOR) and OPEN/FETCH/CLOSE instead of a CURSOR and CURSOR FOR LOOP.
The syntax is OPEN <cursor-reference> FOR <string-containing-sql-statement> . See below.
CREATE OR REPLACE FUNCTION load_test_object_sn
RETURN test_otable_sn
AS
details test_otable_sn := test_otable_sn();
-- Variable stores SQL statement for cursor
l_sql CLOB :=
q'[with ad as (
select 'a' column_1, 'b' column_2, 4 column_3 from dual union all
select 'r', '5', 3 from dual union all
select 'g', 's', 3 from dual
)
select *
from ad]';
-- Cursor reference allows us to open cursor for SQL statement above
rc SYS_REFCURSOR;
-- Define object instance to store each row fetched from the cursor
l_obj test_object_sn := test_object_sn(NULL, NULL, NULL);
i PLS_INTEGER := 1;
BEGIN
-- Explicitly open, fetch from, and close the cursor
OPEN rc FOR l_sql;
LOOP
FETCH rc INTO l_obj.column_1, l_obj.column_2, l_obj.column_3;
EXIT WHEN rc%NOTFOUND;
details.extend();
details(i) := test_object_sn(l_obj.column_1, l_obj.column_2, l_obj.column_3);
i := i + 1;
END LOOP;
CLOSE rc;
RETURN details;
END;
Original answer:
Unfortunately, one can't use SELECT * INTO with a collection in this manner, so here's an alternative way to populate the table:
create or replace function load_test_object_sn
return test_otable_sn
as
details test_otable_sn := test_otable_sn();
cursor c_ad is
with ad as (select 'a' column_1, 'b' column_2, 4 column_3 from dual
union all
select 'r', '5', 3 from dual
union all
select 'g', 's', 3 from dual)
select * from ad;
i pls_integer := 1;
begin
for ad_rec in c_ad loop
details.extend();
details(i) := test_object_sn(ad_rec.column_1, ad_rec.column_2, ad_rec.column_3);
i := i + 1;
end loop;
return details;
end;
/
Output:
SQL> SELECT * FROM TABLE(load_test_object_sn);
COLUMN_1 COLUMN_2 COLUMN_3
---------- ---------- ----------
a b 4
r 5 3
g s 3

Use an array in a SELECT statement?

I want to use this array in 'select from..where..in(YYY)' statement.
I don't want to iterate through array values, I want to use it whole in my select statement.
Unfortunately, I found only how to iterate it:
1 declare
2 type array is table of varchar2(30) index by binary_integer;
3 a array;
4 procedure p( array_in array )
5 is
6 begin
7 for i in 1..array_in.count loop
8 dbms_output.put_line( array_in(i) );
9 end loop;
10 end;
11 begin
12 a(1) := 'Apple';
13 a(2) := 'Banana';
14 a(3) := 'Pear';
15 p( a );
16 end;
17 /
You can do this by creating a function returning your array. Then you can use it into a select:
Create external types and function
create or replace type t_array is table of varchar2(30);
create or replace function list_of_fruits
return t_array
is
l_ t_array:=t_array();
begin
l_.extend(); l_(l_.COUNT) := 'Apple';
l_.extend(); l_(l_.COUNT) := 'Banana';
l_.extend(); l_(l_.COUNT) := 'Pear';
return l_;
end list_of_fruits;
/
And here is how to use it:
select * from (
select 'Peter' this_and_that from dual
union all select 'Joy' from dual
union all select 'God' from dual
union all select 'Pear' from dual
union all select 'Man' from dual
)
where this_and_that in (
select column_value from (table( list_of_fruits() ))
);
The trick here is to use the table() function to make a SQL usable list for your select; also difficult for me was to discover the name of that column_value... which is some built-in constant from Oracle: how do you guess that?
You can use oracle defined collection to achieve this as well. Please see below and example.
declare
a sys.odcivarchar2list;
begin
a := sys.odcivarchar2list('Apple','Banana','Pear');
for r in ( SELECT m.column_value m_value
FROM table(a) m )
loop
dbms_output.put_line (r.m_value);
end loop;
end;

Oracle PL SQL remove duplicate data in string

.....
temp Varchar2 (20);
e_name Varchar2 (255);
.....
Begin
e_name := e_name || temp;
Dbms_Output.Put_Line('names: ' || e_name);
Output result
-------------
names: 'John', 'Sam', 'David', 'Sam', 'John', 'Alex'
How do I format my e_name to remove the duplicate names so that I have the output result
required result
-------------
names: 'John', 'Sam', 'David', 'Alex'
Well you can modify your program like this
Begin
e_name := e_name || temp;
SELECT listagg ( names, ',' ) within GROUP (ORDER BY rn )
INTO e_name
FROM
(
SELECT level rn,
regexp_substr ( e_name, '[^,]+', 1, level ) names,
row_number ( ) over ( partition BY regexp_substr ( e_name, '[^,]+', 1, level ) order by level ) rnn
FROM dual
CONNECT BY regexp_substr ( e_name, '[^,]+', 1, level ) IS NOT NULL
)
WHERE rnn = 1;
Dbms_Output.Put_Line('names: ' || e_name);
The inner most query will convert the list into rows and then outer queries will filter and create the string again.
Replace WM_CONCAT with LISTAGG - I'm running Oragle 10g, LISTAGG is 11g:
SELECT wm_concat(ename) AS employees
FROM emp_test
WHERE deptno = 20
/
Output - SMITH repeats twice:
SMITH,JONES,SCOTT,ADAMS,FORD,SMITH
SELECT wm_concat(distinct ename) AS employees
FROM emp_test
WHERE deptno = 20
/
The distinct fixes the problem:
ADAMS,FORD,JONES,SCOTT,SMITH
This can be done in pure SQL, but it gets a bit messy, especially if you're pre-11gR2 listagg(). Since you're already in PL/SQL territory, here's a solution that eliminates the dups in a simple PL/SQL fashion:
declare e_name varchar2(255) := q'"'John', 'Sam', 'David', 'Sam', 'John', 'Alex'"';
new_ename varchar2(255) := substr(e_name,1,instr(e_name,'''',2));
begin
dbms_output.put_line ('e_name: ' || e_name);
for i in 1..length(e_name) - length(replace(e_name,',')) loop
if instr(new_ename,
substr(e_name,instr(e_name,', ',1,i),instr(e_name||', ',', ',1,i+1) - instr(e_name,', ',1,i))) = 0
then
new_ename := new_ename || substr(e_name,instr(e_name,', ',1,i),instr(e_name||', ',', ',1,i+1) - instr(e_name,', ',1,i));
end if;
end loop;
dbms_output.put_line ('new_ename: ' || new_ename);
end;
e_name: 'John', 'Sam', 'David', 'Sam', 'John', 'Alex'
new_ename: 'John', 'Sam', 'David', 'John', 'Alex'
There is something called ASSOCIATIVE array,which act as Hash table in Java. We can put delimted text into a hash table, and thus can eliminate the duplicates.
We use EXISTS method of collection here, to check if the value is already present!
This is a simple to read NO SQL type of solution. So, performing one.
DECLARE
e_name VARCHAR2 (4000) := 'the text goes here';
L_TEMP_TEXT VARCHAR2(4000);
V_LOOPCOUNT NUMBER :=0;
T_WORD VARCHAR2(4000);
T_FINAL_TEXT VARCHAR2(4000) := ' ';
--Declare a DICT like a Hash table
TYPE DICT IS TABLE OF VARCHAR2(4000) INDEX BY VARCHAR(4000);
MYDICT DICT;
BEGIN
L_TEMP_TEXT := regexp_replace(e_name,'[,]+',','); -- Replace multiple consecutive commas as single
LOOP
v_LOOPCOUNT := v_LOOPCOUNT+1;
T_WORD := REGEXP_SUBSTR(e_name, '[^,]+', 1, V_LOOPCOUNT);
--In a loop we tokenize the String using comma as delimiter
EXIT WHEN T_WORD IS NULL;
IF NOT MYDICT.EXISTS(T_WORD) THEN
-- It is like a Hash Table, if not exists add to it.
MYDICT(T_WORD) := T_WORD;
T_FINAL_TEXT : T_FINAL_TEXT || ',' || T_WORD;
END;
END LOOP;
T_FINAL_TEXT := TRIM(BOTH ',' FROM TRIM(T_FINAL_TEXT));
-- Trimming the unwanted commas
DBMS_OUTPUT.PUT_LINE('after removing duplicates : ' || T_FINAL_TEXT);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(sqlerrm||chr(10)||dbms_utility.format_error_backtrace);
END;
/