unique output with select bulk and loop - oracle

I have data structure looking like this:
So in column_a I have figures form 1 to 5, but in column_b I have elements which a repeating. And I want to loop column_b elements so that only unique are shown.
Here my pl/sql:
SET SERVEROUTPUT ON SIZE 100000
DECLARE
var_column_name_a VARCHAR2(30);
var_column_name_b VARCHAR2(30);
BEGIN
SELECT
column_name_a,
column_name_b
BULK COLLECT
INTO
var_column_name_a,
var_column_name_b
FROM
table_name;
FOR i IN var_column_name_a.first..var_column_name_a.last LOOP
dbms_output.put_line(var_column_name_b(i));
END LOOP;
END;
/
My current output is this:
type_a
type_b
type_b
type_b
type_c
but I need this
type_a
type_b
type_c
How to achieve this?

No way that code you posted produces anything, it is invalid. You can't bulk insert into scalar variables, so either you faked code you wrote, or the output.
Anyway: to me, the simplest option seems to be a separate select of distinct values into another collection. Here's how:
Sample data:
SQL> SELECT * FROM test;
COLA COLB
---------- ------
1 type a
2 type b
3 type b
4 type b
5 type c
Procedure:
SQL> DECLARE
2 TYPE tr IS RECORD
3 (
4 cola NUMBER,
5 colb VARCHAR2 (10)
6 );
7
8 TYPE tt IS TABLE OF tr;
9
10 l_tab tt; --> for all rows in TEST table
11 l_tab_unique SYS.odcivarchar2list; --> just for COLB
12 BEGIN
13 SELECT cola, colb
14 BULK COLLECT INTO l_tab
15 FROM test;
16
17 SELECT DISTINCT colb
18 BULK COLLECT INTO l_tab_unique
19 FROM test;
20
21 DBMS_OUTPUT.put_line ('all rows -------------');
22
23 FOR i IN l_tab.FIRST .. l_tab.LAST
24 LOOP
25 DBMS_OUTPUT.put_line (l_tab (i).cola || ' ' || l_tab (i).colb);
26 END LOOP;
27
28 DBMS_OUTPUT.put_line ('unique rows ----------');
29
30 FOR i IN l_tab_unique.FIRST .. l_tab_unique.LAST
31 LOOP
32 DBMS_OUTPUT.put_line (l_tab_unique (i));
33 END LOOP;
34 END;
35 /
The result is:
all rows -------------
1 type a
2 type b
3 type b
4 type b
5 type c
unique rows ----------
type b
type c
type a
PL/SQL procedure successfully completed.
SQL>

First off a simple select distinct col_b bulk ... is sufficient for what you are asking for. But assuming you may want a list of the other column you can get it with LISTAGG function. Also for this you can use an older (but still valid) form of BULK COLLECT where each column in the select is collected into a independent collection. So you wind up with the structure:
select col_a, col_b
bulk collect
into collection_a, collection_b
... ;
Specifically here with LISTAGG for col_a: (see demo)
declare
type array_t is table of varchar2(256) ;
k_tab constant varchar2(1) := chr(09);
col_a_list array_t;
col_b_list array_t;
begin
select col_b, listagg( col_a, ',') within group (order by col_a) a_list
bulk collect
into col_b_list, col_a_list --<<< BULK COLLECT into separate collection
from t1
group by col_b
order by col_b;
dbms_output.put_line ('col_b') ;
dbms_output.put_line ('_____');
for list_cnt in 1 .. col_b_list.count
loop
dbms_output.put_line(col_b_list(list_cnt));
end loop;
dbms_output.put_line (' ') ;
dbms_output.put_line ('col_b' || k_tab || 'col_a_list') ;
dbms_output.put_line ('_____' || k_tab || '__________') ;
for list_cnt in 1 .. col_b_list.count
loop
dbms_output.put_line(col_b_list(list_cnt) || k_tab || col_a_list(list_cnt));
end loop;
end ;

Related

Oracle plsql error PLS-00642: local collection types not allowed in SQL statements

I Am writing and pl/sql procedure and getting the error as
PLS-00642: local collection types not allowed in SQL statements and
PL/SQL: ORA-00947: not enough values
first i called a cursor and iterate the cursor and again fetching into a record there error is coming.
declare
type t1 is record (
msisdn varchar2(20),
imsi varchar2(14),
date_time date,
mml_cmd varchar2(50),
cmd_executed varchar2(2500),
myrank number);
type t2 is table of t1;
l_k t2;
type t3 is table of hlr_agent_logs%rowtype;
l_tab t3;
cursor c1 is select * from hlr_agent_logs where date_time>trunc(sysdate-2) and mml_cmd='ADD USER' and cmd_executed like '%Profile=3%' order by date_time;
begin
open c1;
loop
fetch c1 bulk collect into l_tab limit 500;
exit when l_tab.count=0;
for i in l_tab.first .. l_tab.last loop
begin
select msisdn, imsi, date_time , mml_cmd,cmd_executed,rank() over (partition by imsi order by date_time asc) as myrank into l_k from hlr_agent_logs
where substr(msisdn,3)=l_tab(i).msisdn and imsi=l_tab(i).imsi and date_time>=l_tab(i).date_time;
for k in l_k.first..l_k.last loop
dbms_output.put_line(l_k(k).msisdn);
end loop;
exception when no_data_found then
null;
end;
end loop;
close c1;
end loop;
end;
/
What you can and can't do with PLSQL collections directly from SQL varies from version to version, but it might be as simply as you not having the BULK COLLECT because you are fetching into a table
SQL> declare
2 type rec is record (
3 c1 int,
4 c2 int );
5
6 type mylist is table of rec;
7 n mylist;
8 begin
9 select 1,2 into n from dual;
10 end;
11 /
select 1,2 into n from dual;
*
ERROR at line 9:
ORA-06550: line 9, column 21:
PLS-00642: local collection types not allowed in SQL statements
ORA-06550: line 9, column 23:
PL/SQL: ORA-00947: not enough values
ORA-06550: line 9, column 5:
PL/SQL: SQL Statement ignored
SQL>
SQL> declare
2 type rec is record (
3 c1 int,
4 c2 int );
5
6 type mylist is table of rec;
7 n mylist;
8 begin
9 select 1,2 bulk collect into n from dual;
10 end;
11 /
PL/SQL procedure successfully completed.
As #Connor McDonnal wisely said, depending on the version, you can or cannot use PLSQL types on SQL, but having them declared as schema objects. As you did not say which version of Oracle are you using, let me show you an example with Oracle 19c
declare
type t1 is record (
msisdn varchar2(20),
imsi varchar2(14),
date_time date,
mml_cmd varchar2(50),
cmd_executed varchar2(2500),
myrank number);
type t2 is table of t1;
l_k t2;
type t3 is table of hlr_agent_logs%rowtype;
l_tab t3;
cursor c1 is select * from hlr_agent_logs where date_time>trunc(sysdate-2) and mml_cmd='ADD USER' and cmd_executed like '%Profile=3%' order by date_time;
begin
open c1;
loop
fetch c1 bulk collect into l_tab limit 500;
exit when l_tab.count=0;
for i in l_tab.first .. l_tab.last loop
begin
select msisdn, imsi, date_time , mml_cmd,cmd_executed,rank() over (partition by imsi order by date_time asc) as myrank bulk collect into l_k
from hlr_agent_logs
where substr(msisdn,3)=l_tab(i).msisdn and imsi=l_tab(i).imsi and date_time>=l_tab(i).date_time;
for k in l_k.first..l_k.last loop
dbms_output.put_line(l_k(k).msisdn);
end loop;
exception when no_data_found then
null;
end;
end loop;
close c1;
end loop;
end;
/
Output
SQL> l
1 declare
2 type t1 is record (
3 msisdn varchar2(20),
4 imsi varchar2(14),
5 date_time date,
6 mml_cmd varchar2(50),
7 cmd_executed varchar2(2500),
8 myrank number);
9 type t2 is table of t1;
10 l_k t2;
11 type t3 is table of hlr_agent_logs%rowtype;
12 l_tab t3;
13 cursor c1 is select * from hlr_agent_logs where date_time>trunc(sysdate-2) and mml_cmd='ADD USER' and cmd_executed like '%Profile=3%' order by date_time;
14 begin
15 open c1;
16 loop
17 fetch c1 bulk collect into l_tab limit 500;
18 exit when l_tab.count=0;
19 for i in l_tab.first .. l_tab.last loop
20 begin
21 select msisdn, imsi, date_time , mml_cmd,cmd_executed,rank() over (partition by imsi order by date_time asc) as myrank bulk collect into l_k
22 from hlr_agent_logs
23 where substr(msisdn,3)=l_tab(i).msisdn and imsi=l_tab(i).imsi and date_time>=l_tab(i).date_time;
24 for k in l_k.first..l_k.last loop
25 dbms_output.put_line(l_k(k).msisdn);
26 end loop;
27 exception when no_data_found then
28 null;
29 end;
30 end loop;
31 close c1;
32 end loop;
33* end;
SQL> /
PL/SQL procedure successfully completed.
SQL>

How to get n element from nested table pl/sql?

I have problem with the compilation of my stored procedure.
create or replace type CartLine as object (
offeringId OfferingIdList
,productLine varchar2(50)
,equipment char(1)
,installment CHAR(1)
,cartItemProcess varchar2(50)
,minimalPrice decimal
);
create or replace type CartLineType is table of CartLine;
create or replace PROCEDURE GetOfferingRecommendation (
cartLineList IN CartLineType,
user IN UserType,
customer IN CustomerType,
processContext IN ProcessContextType,
recommendation out SYS_REFCURSOR )
IS
prodLine VARCHAR2(20);
prodPrice NUMBER(5,0);
BEGIN
FOR i IN cartLineList.FIRST .. cartLineList.LAST
LOOP
SELECT productLine, minimalPrice
INTO prodLine, prodPrice
FROM TABLE(cartLineList(i));
OPEN recommendation FOR
SELECT CAST(REKOM_ID_SEQ.NEXTVAL AS VARCHAR(10))
||'_'||cp.ID_REKOM_OFERTA
||'_'||TO_CHAR(SYSDATE, 'yyyymmdd') AS recommendationId
,cp.ID_REKOM_OFERTA AS offeringId
,cp.PRIORYTET AS priority
FROM REKOM_CROSS_PROM cp
WHERE cp.LINIA_PROD = prodLine
AND prodPrice BETWEEN cp.CENA_MIN AND cp.CENA_MAX
;
END LOOP;
END GetOfferingRecommendation;
It is not getting compiled cause the following statement is wrong:
SELECT productLine, minimalPrice
INTO prodLine, prodPrice
FROM TABLE(cartLineList(i));
I want to select only single value every all new iteration of my loop.
Can somebody help me to resolve my problem?
-- EDIT 1/9/2018 4:26 PM
According to topic:
How to return result of many select statements as one custom table
I tried to rebuild my procedure.
I created types for test:
create or replace TYPE tst AS OBJECT (
rekom_id varchar2(50)
,rekom_priorytet number(5,4)
);
/
create or replace TYPE tst_list IS TABLE OF tst;
After that, I changed my procedure like below:
CREATE OR REPLACE PROCEDURE GetOfferingRecommendation (cartLineList IN CartLineType, recommendation out SYS_REFCURSOR )
IS
CURSOR CUR_TAB IS SELECT productLine, minimalPrice FROM TABLE(cartLineList);
v_tst tst_list;
BEGIN
FOR i IN CUR_TAB
LOOP
EXECUTE IMMEDIATE 'SELECT tst_list(
CAST(REKOM_ID_SEQ.NEXTVAL AS VARCHAR(10))||''_''||cp.ID_REKOM_OFERTA||''_''||TO_CHAR(SYSDATE, ''yyyymmdd'')
,cp.PRIORYTET)
FROM REKOM_CROSS_PROM cp
WHERE cp.LINIA_PROD ='||i.productLine||' AND '||i.minimalPrice||' BETWEEN cp.CENA_MIN AND cp.CENA_MAX'
BULK COLLECT INTO v_tst;
EXIT WHEN CUR_TAB%NOTFOUND;
FOR REC IN 1 .. v_tst.COUNT
LOOP
PIPE ROW (v_tst(REC));
END LOOP;
END LOOP;
OPEN recommendation FOR SELECT * FROM TABLE(v_tst);
END IF;
END GetOfferingRecommendation;
But I can't compile because error occured: PLS-00629
Would you please told me what I do wrong?
You cannot assign variables using a select statement from a collection in a loop like below.
SELECT productLine, minimalPrice
INTO prodLine, prodPrice
FROM TABLE(cartLineList(i));
The collection elements cannot be referred inside a SELECT statement 1 by 1 using a loop. You can loop through the collection as
For i in 1..collection.count
loop
...
..
End loop;
Collection has a number of rows and when you do so, you try to assign many rows to a single variable, which is wrong. You can do either of the below explained. There relevant explanation is inline.
CREATE OR REPLACE PROCEDURE GETOFFERINGRECOMMENDATION (
CARTLINELIST IN CARTLINETYPE,
RECOMMENDATION OUT SYS_REFCURSOR)
IS
TYPE V_PRODLINE IS TABLE OF VARCHAR2 (20)
INDEX BY PLS_INTEGER;
TYPE V_PRODPRICE IS TABLE OF NUMBER (5, 0)
INDEX BY PLS_INTEGER;
PRODLINE V_PRODLINE;
PRODPRICE V_PRODPRICE;
BEGIN
--Putting the collection result to another collection
SELECT PRODUCTLINE,
MINIMALPRICE
BULK COLLECT INTO PRODLINE,
PRODPRICE
FROM TABLE (CARTLINELIST);
-- Assuming number of elements will be same in both prodLine, prodPrice colection, loop can be iterated as below
FOR I IN 1 .. PRODLINE.LAST
LOOP
OPEN RECOMMENDATION FOR
SELECT CAST (REKOM_ID_SEQ.NEXTVAL AS VARCHAR (10) )
|| '_'
|| CP.ID_REKOM_OFERTA
|| '_'
|| TO_CHAR (SYSDATE, 'yyyymmdd') AS RECOMMENDATIONID,
CP.ID_REKOM_OFERTA AS OFFERINGID,
CP.PRIORYTET AS PRIORITY
FROM REKOM_CROSS_PROM CP
WHERE CP.LINIA_PROD = PRODLINE (I)
AND PRODPRICE (I) BETWEEN CP.CENA_MIN AND CP.CENA_MAX;
END LOOP;
END GETOFFERINGRECOMMENDATION;
OR as per #krokodilko.. You can do as below:
CREATE OR REPLACE PROCEDURE GETOFFERINGRECOMMENDATION (
CARTLINELIST IN CARTLINETYPE,
RECOMMENDATION OUT SYS_REFCURSOR)
IS
PRODLINE VARCHAR2 (20);
PRODPRICE NUMBER (5, 0);
BEGIN
FOR I IN 1 .. CARTLINELIST.LAST
LOOP
--Assign the values of the collection to the variable declared.
PRODUCTLINE := CARTLINELIST (I).PRODUCTLINE;
MINIMALPRICE := CARTLINELIST (I).MINIMALPRICE;
OPEN RECOMMENDATION FOR
SELECT CAST (REKOM_ID_SEQ.NEXTVAL AS VARCHAR (10) )
|| '_'
|| CP.ID_REKOM_OFERTA
|| '_'
|| TO_CHAR (SYSDATE, 'yyyymmdd') AS RECOMMENDATIONID,
CP.ID_REKOM_OFERTA AS OFFERINGID,
CP.PRIORYTET AS PRIORITY
FROM REKOM_CROSS_PROM CP
WHERE CP.LINIA_PROD = PRODLINE
AND PRODPRICE BETWEEN CP.CENA_MIN AND CP.CENA_MAX;
END LOOP;
END GETOFFERINGRECOMMENDATION;
Demo:
SQL> CREATE OR REPLACE TYPE CARTLINE AS OBJECT (
2 PRODUCTLINE VARCHAR2 (50),
3 MINIMALPRICE DECIMAL
4 );
5 /
Type created.
SQL> CREATE OR REPLACE TYPE CARTLINETYPE IS TABLE OF CARTLINE;
2 /
Type created.
SQL> CREATE OR REPLACE PROCEDURE GETOFFERINGRECOMMENDATION (
2 CARTLINELIST IN CARTLINETYPE)
3 IS
4 TYPE V_PRODLINE IS TABLE OF VARCHAR2 (20)
5 INDEX BY PLS_INTEGER;
6
7 TYPE V_PRODPRICE IS TABLE OF NUMBER (5, 0)
8 INDEX BY PLS_INTEGER;
9
10 PRODLINE V_PRODLINE;
11 PRODPRICE V_PRODPRICE;
12 BEGIN
13 SELECT PRODUCTLINE,
14 MINIMALPRICE
15 BULK COLLECT INTO PRODLINE,
16 PRODPRICE
17 FROM TABLE (CARTLINELIST);
18
19 FOR I IN 1 .. PRODLINE.COUNT
20 LOOP
21 DBMS_OUTPUT.PUT_LINE ( 'Prod Line '
22 || PRODLINE (I)
23 || ' Prod Price '
24 || PRODPRICE (I) );
25 END LOOP;
26 END GETOFFERINGRECOMMENDATION;
27 /
Procedure created.
Output:
SQL> DECLARE
2 VAR CARTLINETYPE := CARTLINETYPE ();
3 BEGIN
4 --Popuating the collection
5 VAR.EXTEND (2);
6 VAR (1) := CARTLINE ('TypeA', 6.0);
7 VAR (2) := CARTLINE ('TypeB', 7.1);
8
9 --Calling the procedure
10 GETOFFERINGRECOMMENDATION (CARTLINELIST => VAR);
11 END;
12 /
Prod Line TypeA Prod Price 6
Prod Line TypeB Prod Price 7
PL/SQL procedure successfully completed.
SQL>
Use simple assignments instead of SELECT ... FROM TABLE(cartLineList(i));:
LOOP
/* SELECT productLine, minimalPrice INTO prodLine, prodPrice FROM TABLE(cartLineList(i)); */
productLine := cartLineList(i).productLine;
minimalPrice := cartLineList(i).minimalPrice;
.....
.....
END LOOP;

How to fetch cursor values from pivot SQL to fixed variables - Error ORA-01007

I have a SQL statement with the PIVOT operation. The maximum number of columns I will have in PIVOT is 5, but I can have less, 4, 3, 2.
How can I read these columns in my cursor and assign (fetch .. into...) to the fixed variables, without occurring the error ORA-01007.
...
sql_stmt := 'select * from
(select codcoligada,
idprd,
codcfo,
valnegociado
from tcitmorcamento
where codcoligada = ' || p_codcoligada || '
and codcotacao = ' || '''' || p_codcotacao || '''' || ')
pivot
(
sum(valnegociado) for codcfo in (' || pivot_clause || ')
)';
ret := t_tab_sesa_cotacao();
open vCursor for sql_stmt;
loop
/* If my cursor returns less than 5 columns in PIVOT the error occurs ORA-01007 */
fetch vCursor into vCodColigada, vIdProduto, vValor01, vValor02, vValor03, vValor04, vValor05;
exit when vCursor%NOTFOUND;
ret.extend;
ret(ret.count) := t_type_sesa_cotacao(vCodColigada, vIdProduto, vValor01, vValor02, vValor03, vValor04, vValor05);
end loop;
close vCursor;
...
If I return less than 5 colums, I want to fill in the remainder of the variables with a value of 0 or null.
The variables vCodColigada and vIdProduto are identified, only PIVOT columns that can vary between 1 and 5 (vValor1, vValor2, vValor3, vValor4, vValor5)
Result PIVOT SQL:
CODCOLIGADA IDPRD '000125' '002272' '002342'
----------------- ---------------- ---------------- ---------------- ----------------
1 15464 45 300 30
1 18460 35 200 20
1 57492 20 100 10
-------- End of Data --------
Example:
If the cursor returns 3 values in the PIVOT (above), the variables vValor01, vValor02, vValor03 will be filled in, and the variables vValor04, vValor05 must be 0 or null.
Example:
CODCOLIGADA IDPRD VALOR01 VALOR02 VALOR03 VALOR04 VALOR05
----------------- ---------------- ---------------- ---------------- ---------------- ---------------- ----------------
1 15464 45 300 30 0 0
1 18460 35 200 20 0 0
1 57492 20 100 10 0 0
-------- End of Data --------
As I only have 3 columns in PIVOT, and I have 5 variables, the ORA-01007 error occurs in (fetch .. into ...).
Hope this below snippet helps. What the basic understanding is we need to add excess variable as null or blank to make this work.
SET serveroutput ON;
DECLARE
lv_pivot VARCHAR2(100):='''Y'',''N''';
TYPE lv
IS
RECORD
(
flg_y VARCHAR2(100),
flg_n VARCHAR2(100),
flg_e VARCHAR2(100));
type lv_tab
IS
TABLE OF lv;
lv_num lv_tab;
lv_check VARCHAR2(1000);
BEGIN
lv_check :=regexp_count(lv_pivot,',',1);
IF lv_check < 3 THEN
FOR z IN 1..(2-lv_check)
LOOP
lv_pivot:=lv_pivot||',null as val'||z;
END LOOP;
ELSE
lv_pivot:=lv_pivot;
END IF;
dbms_output.put_line(lv_pivot);
EXECUTE IMMEDIATE ' SELECT * FROM
(SELECT col1 FROM <table> )
pivot ( COUNT(1) FOR col1 IN ('||lv_pivot||'))' BULK COLLECT INTO lv_num;
END;
---------------------------Refactoring in Function------------------------------
--Create Object Type
CREATE OR REPLACE TYPE lv_obj IS OBJECT
(
flg_y VARCHAR2(100),
flg_n VARCHAR2(100),
flg_e VARCHAR2(100)
);
--Create Table Type
CREATE OR REPLACE TYPE lv_tab IS TABLE OF lv_obj;
--Create Function
CREATE OR REPLACE
FUNCTION test_func
RETURN lv_tab
AS
lv_pivot VARCHAR2(100):='''Y'',''N''';
lv_num lv_tab;
lv_check VARCHAR2(1000);
BEGIN
lv_check :=regexp_count(lv_pivot,',',1);
IF lv_check < 3 THEN
FOR z IN 1..(2-lv_check)
LOOP
lv_pivot:=lv_pivot||',null as val'||z;
END LOOP;
ELSE
lv_pivot:=lv_pivot;
END IF;
dbms_output.put_line(lv_pivot);
EXECUTE IMMEDIATE ' SELECT * FROM
(SELECT col1 FROM <table> )
pivot ( COUNT(1) FOR col1 IN ('||lv_pivot||'))' BULK COLLECT INTO lv_num;
RETURN lv_tab;
END;
-------------------------------------------------Output-----------------------------------------------
SELECT * FROM TABLE(test_func);
-------------------------------------------------------------------------------------------------------
Try padding the results in the query. Add a join to a select from dual statement that passes back all five columns.

Bulk Collect Twice over same nested table

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;

How do you create a table with random number of fields in Oracle using PL/SQL?

I need to create a Oracle tables with random number of columns for load testing. I just want to specify number of columns with type NUMBER, number of columns with type VARCHAR2 etc and the fields should be generated automatically. Also, I will be filling up the tables with random data for which I will be using dbms_random.
How can I achieve this?
"I just want to specify number of
columns with type NUMBER, number of
columns with type VARCHAR2 etc and the
fields should be generated
automatically."
The following procedure does just that. Note that it is rather basic; you might want to make it more sophisticated, for example by varying the length of the varchar2 columns:
SQL> create or replace procedure bld_table
2 ( p_tab_name in varchar2
3 , no_of_num_cols in pls_integer
4 , no_of_var_cols in pls_integer
5 , no_of_date_cols in pls_integer
6 )
7 as
8 begin
9 execute immediate 'create table '||p_tab_name||' ('
10 ||' pk_col number not null'
11 ||', constraint '||p_tab_name||'_pk primary key (pk_col) using index)';
12 << numcols >>
13 for i in 1..no_of_num_cols loop
14 execute immediate 'alter table '||p_tab_name||' add '
15 ||' col_n'||trim(to_char(i))||' number';
16 end loop numcols;
17 << varcols >>
18 for i in 1..no_of_var_cols loop
19 execute immediate 'alter table '||p_tab_name||' add '
20 ||' col_v'||trim(to_char(i))||' varchar2(30)';
21 end loop varcols;
22 << datcols >>
23 for i in 1..no_of_date_cols loop
24 execute immediate 'alter table '||p_tab_name||' add '
25 ||' col_d'||trim(to_char(i))||' date';
26 end loop datcols;
27 end bld_table;
28 /
Procedure created.
SQL>
Here it is in action:
SQL> exec bld_table ('T23', 2, 3, 0)
PL/SQL procedure successfully completed.
SQL> desc t23
Name Null? Type
----------------------------------------- -------- ----------------------------
PK_COL NOT NULL NUMBER
COL_N1 NUMBER
COL_N2 NUMBER
COL_V1 VARCHAR2(30 CHAR)
COL_V2 VARCHAR2(30 CHAR)
COL_V3 VARCHAR2(30 CHAR)
SQL>
We can also use dynamic SQL to populate the table with rows of random data.
SQL> create or replace procedure pop_table
2 ( p_tab_name in varchar2
3 , p_no_of_rows in pls_integer
4 )
5 as
6 stmt varchar2(32767);
7 begin
8 stmt := 'insert into '||p_tab_name
9 || ' select rownum ';
10 for r in ( select column_name
11 , data_type
12 , data_length
13 from user_tab_columns
14 where table_name = p_tab_name
15 and column_name != 'PK_COL' )
16 loop
17 case r.data_type
18 when 'VARCHAR2' then
19 stmt := stmt ||', dbms_random.string(''a'', '||r.data_length||')';
20 when 'NUMBER' then
21 stmt := stmt ||', dbms_random.value(0, 1000)';
22 when 'DATE' then
23 stmt := stmt ||', sysdate + dbms_random.value(-1000, 0)';
24 end case;
25 end loop;
26 stmt := stmt || ' from dual connect by level <= '||p_no_of_rows;
27 execute immediate stmt;
28 end pop_table;
29 /
Procedure created.
SQL>
Note that the primary key is populated with the ROWNUM so it will most likely fail if the table already contains rows.
SQL> exec pop_table('T23', 4)
PL/SQL procedure successfully completed.
SQL> select * from t23
2 /
PK_COL COL_N1 COL_N2 COL_V1 COL_V2 COL_V3
---------- ---------- ---------- ------------------------------ ----------------------------- ------------------------------
1 913.797432 934.265814 NUtxjLoRQMCTLNMPKVGbTZwJeYaqnXTkCcWu WFRSHjXdLfpgVYOjzrGrtUoX jIBSoYOhSdhRFeEeFlpAxoanPabvwK
2 346.879815 104.800387 NTkvIlKeJWybCTNEdvsqJOKyidNkjgngwRNN PPIOInbzInrsVTmFYcDvwygr RyKFoMoSiWTmjTqRBCqDxApIIrctPu
3 93.1220275 649.335267 NTUxzPRrKKfFncWaeuzuyWzapmzEGtAwpnjj jHILMWJlcMjnlboOQEIDFTBG JRozyOpWkfmrQJfbiiNaOnSXxIzuHk
4 806.709357 857.489387 ZwLLkyINrVeCkUpznVdTHTdHZnuFzfPJbxCB HnoaErdzIHXlddOPETzzkFQk dXWTTgDsIeasNHSPbAsDRIUEyPILDT
4 rows selected.
SQL>
Again, there are all sorts of ways to improve the sophistication of the data.
As an aside, using these sorts of data pools for load testing is not always a good idea. Performance problems are often caused by skews in the distribution of data values which you just aren't going to get with DBMS_RANDOM. This is particularly true of some date columns - e.g. START_DATE - which would tend to be clustered together in real life but the above procedure will not generate that pattern. Similarly maxing out the varchar2 columns will lead to tables which take up more storage than they wlll under real-life usage.
In short, randomly generated data is better than nothing but we need to understand its weaknesses.
Two approaches
1) Write code to generate text files containing the CREATE TABLE commands that you need to run and populate your tables, then execute these using SQL*Plus.
2) Use Dynamic SQL embedded inside PL/SQL -
PROCEDURE create_random_table
(pTableName IN VARCHAR2,
pNumberOfColumns IN INTEGER)
IS
PRAGMA AUTONOMOUS_TRANSACTION;
lCommand VARCHAR2(32000);
BEGIN
lCommand :=
'CREATE TABLE '||pTableName||'(';
FOR i IN 1..pNumberOfColumns LOOP
append your column definition here
END LOOP;
lCommand := lCommand||';';
--
EXECUTE IMMEDIATE lCommand;
END;
You could also use 'CREATE TABLE AS SELECT' to populate your table at the same time (see other answer).
This should give you a good starting point - there isn't anything in the system that will do what you want without writing code.
You may generate such table by yourself.
Create table with required datatypes:
Create Table RandomTable
AS
Select dbms_random.string('A', 1) CharField,
dbms_random.string('a', 20) VarCharField,
TRUNC(dbms_random.value(0, 35000)) IntField,
dbms_random.value(0, 35000) NumberField,
Level SequenceField,
sysdate + dbms_random.value(-3500, 3500) DateField
from dual connect by level < 1000
(You can change 1000 to required rows numbers, and add required data types)
After that you can create or populate tables with random data from RandomTable
Create Table TestTable1
AS
SELECT CharField as a, NumberField as b
from RandomTable
INSERT INTO TestTable2(DateFrom, Quantity)
SELECT DateField, IntField
from RandomTable
Where SequenceField <= 500

Resources