Getting index of element in PL/SQL collection - oracle
Is there a built-in function to determine the (first) index of an element in a PL/SQL collection?
Something like
DECLARE
TYPE t_test IS TABLE OF VARCHAR2(1);
v_test t_test;
BEGIN
v_test := NEW t_test('A', 'B', 'A');
dbms_output.put_line( 'A: ' || get_index( v_test, 'A' ) );
dbms_output.put_line( 'B: ' || get_index( v_test, 'B' ) );
dbms_output.put_line( 'C: ' || get_index( v_test, 'C' ) );
END;
A: 1
B: 2
C:
I can use Associative Arrays, Nested Tables or Varrays, whatever necessary. If the same element exists more than once, then the index of the first occurrence is sufficient.
Otherwise I'd have to do something like
CREATE FUNCTION get_index ( in_test IN t_test, in_value IN VARCHAR2 )
RETURN PLS_INTEGER
AS
i PLS_INTEGER;
BEGIN
i := in_test.FIRST;
WHILE( i IS NOT NULL ) LOOP
IF( in_test(i) = in_value ) THEN
RETURN i;
END IF;
i := in_test.NEXT(i);
END LOOP;
RETURN NULL;
END get_index;
Not sure, if this really helps, or if you think it is more elegant:
create type t_test as table of varchar2(1);
/
DECLARE
--TYPE t_test IS TABLE OF VARCHAR2(1);
v_test t_test;
function get_index(q in t_test, c in varchar2) return number is
ind number;
begin
select min(rn) into ind from (
select column_value cv, rownum rn
from table(q)
)
where cv = c;
return ind;
end get_index;
BEGIN
v_test := NEW t_test('A', 'B', 'A');
dbms_output.put_line( 'A: ' || get_index( v_test, 'A' ) );
dbms_output.put_line( 'B: ' || get_index( v_test, 'B' ) );
dbms_output.put_line( 'C: ' || get_index( v_test, 'C' ) );
END;
/
show errors
drop type t_test;
I don't think there is a built-in function that searches a collection. However, if you know you will need to search a collection a lot, you could build an index. Adding element to the collection will be a bit more expensive, but looking for an element will be an O(1) operation (instead of O(n) for a brute force search). For example, you could use something like this:
SQL> DECLARE
2 TYPE t_test IS TABLE OF VARCHAR2(1);
3 TYPE t_test_r IS TABLE OF NUMBER INDEX BY VARCHAR2(1);
4
5 v_test t_test;
6 v_test_r t_test_r;
7
8 FUNCTION get_index(p_test_r t_test_r,
9 p_element VARCHAR2) RETURN NUMBER IS
10 BEGIN
11 RETURN p_test_r(p_element);
12 EXCEPTION
13 WHEN no_data_found THEN
14 RETURN NULL;
15 END get_index;
16
17 PROCEDURE add_element(p_test IN OUT t_test,
18 p_test_r IN OUT t_test_r,
19 p_element VARCHAR2) IS
20 BEGIN
21 p_test.extend;
22 p_test(p_test.count) := p_element;
23 p_test_r(p_element) := least(p_test.count,
24 nvl(get_index(p_test_r, p_element),
25 p_test.count));
26 END add_element;
27 BEGIN
28 v_test := NEW t_test();
29 add_element(v_test, v_test_r, 'A');
30 add_element(v_test, v_test_r, 'B');
31 add_element(v_test, v_test_r, 'A');
32 dbms_output.put_line('A: ' || get_index(v_test_r, 'A'));
33 dbms_output.put_line('B: ' || get_index(v_test_r, 'B'));
34 dbms_output.put_line('C: ' || get_index(v_test_r, 'C'));
35 END;
36 /
A: 1
B: 2
C:
PL/SQL procedure successfully completed
You could also define a record that contains both arrays and all functions/procedures to interact with arrays would use this record type.
When in doubt, consult the documentation ;) (here)
DECLARE
TYPE aa_type_int IS TABLE OF INTEGER INDEX BY PLS_INTEGER;
aa_int aa_type_int;
PROCEDURE print_first_and_last IS
BEGIN
DBMS_OUTPUT.PUT_LINE('FIRST = ' || aa_int.FIRST);
DBMS_OUTPUT.PUT_LINE('LAST = ' || aa_int.LAST);
END print_first_and_last;
BEGIN
aa_int(1) := 3;
aa_int(2) := 6;
aa_int(3) := 9;
aa_int(4) := 12;
DBMS_OUTPUT.PUT_LINE('Before deletions:');
print_first_and_last;
aa_int.DELETE(1);
aa_int.DELETE(4);
DBMS_OUTPUT.PUT_LINE('After deletions:');
print_first_and_last;
END;
/
Result:
Before deletions:
FIRST = 1
LAST = 4
After deletions:
FIRST = 2
LAST = 3
Related
NULL assigning to associative array
I wanted to assign NULL to an associative array. How can I do it? TYPE t_test IS TABLE OF PLS_INTEGER INDEX BY PLS_INTEGER; l_arr t_test; l_arr:=NULL-- Which is giving error.
I want to empty it. Use the delete collection method: l_arr.delete; Or if I want to assign a particular position as null, in that can also how can I do it then? Just assign null to that position: l_arr(2) := null; You can also delete a specific position: l_arr.delete(1); Demo of both: declare type t_test is table of pls_integer index by pls_integer; l_arr t_test; procedure show(p_label varchar2) is l_idx pls_integer; begin dbms_output.new_line; dbms_output.put_line(p_label || ': count = ' || l_arr.count); l_idx := l_arr.first; while l_idx is not null loop dbms_output.put_line(' ' || l_idx || ' -> ' || l_arr(l_idx)); l_idx := l_arr.next(l_idx); end loop; end show; begin l_arr(1) := 1; l_arr(42) := 42; show('a'); l_arr(2) := null; show('b'); l_arr.delete(1); show('c'); l_arr.delete; show('d'); end; / a: count = 2 1 -> 1 42 -> 42 b: count = 3 1 -> 1 2 -> 42 -> 42 c: count = 2 2 -> 42 -> 42 d: count = 0 PL/SQL procedure successfully completed.
Run query dynamically in stored procedure in Oracle
Select all the tables of database where column match than pass table name to next query using loop. If column name and column values matches than return true and exist for loop using a stored procedure: CREATE OR REPLACE PROCEDURE TEST ( NAME IN VARCHAR2 , ID IN NUMBER, RE OUT SYS_REFCURSOR ) AS BEGIN OPEN RE FOR SELECT A.TABLE_NAME FROM user_tables A JOIN user_tab_columns C ON C.TABLE_NAME = A.TABLE_NAME WHERE C.COLUMN_NAME = NAME; FOR RE IN LOOP v_Sql := 'SELECT COUNT(*) FROM '|| LOOP.TABLE_NAME || 'WHERE COLUMN_NAME = ID'; EXECUTE IMMEDIATE v_Sql IF v_Sql%ROWCOUNT > 0 THEN return true; EXIT END LOOP; END TEST; For more understanding the problem //Get all the tables of database where campus_id is exist in any table of database Campus, Class, Section (3 tables found) Apply forloop on the records Select count(campus_id) as total from (table name using loop) where campus_id = 1(value pass) if(total > 0){ Exist for loop and return true } else{ Again iterate the loop to next value }
What you described doesn't make much sense. If there are several tables that contain a column you're checking and you exit the loop as soon as you find the first one, what about the rest of them? Here's what I'd do, see if it helps. I'll create a function (not a procedure) that returns a table. In order to do that, I'll create type(s) first: SQL> create or replace type t_record as object (tn varchar2(30), cnt number); 2 / Type created. SQL> create or replace type t_table as table of t_record; 2 / Type created. SQL> The function: in a cursor FOR loop I'm selecting tables that contain that column L_STR is used to compose the SELECT statement DBMS_OUTPUT.PUT_LINE is used to display it first, so that I could visually check whether it is correctly set or not. if it is, I'm running it with the EXECUTE IMMEDIATE the result is stored into a table type and returned to the caller SQL> create or replace function f_colname 2 (par_column_name in varchar2, 3 par_column_value in varchar2 4 ) 5 return t_table 6 is 7 retval t_table := t_table(); 8 l_str varchar2(200); 9 l_cnt number; 10 begin 11 for cur_r in (select table_name 12 from user_tab_columns 13 where column_name = par_column_name 14 ) 15 loop 16 l_str := 'select count(*) from ' || cur_r.table_name || 17 ' where ' || par_column_name || ' = ' || 18 chr(39) || par_column_value || chr(39); 19 -- Display l_str first, to make sure that it is OK: 20 -- dbms_output.put_line(l_str); 21 execute immediate l_str into l_cnt; 22 retval.extend; 23 retval(retval.count) := t_record(cur_r.table_name, l_cnt); 24 end loop; 25 return retval; 26 end; 27 / Function created. Testing: SQL> select * from table (f_colname('DEPTNO', '10')); TN CNT ------------------------------ ---------- TEST_201812 1 DEPT 1 EMP 3 SQL> select * from table (f_colname('ENAME', 'KING')); TN CNT ------------------------------ ---------- EMP 1 BONUS 1 SQL> That won't work properly for some datatypes (such as DATE) and will have to be adjusted, if necessary. [EDIT: after you edited the question] OK then, that's even simpler. It should still be a function (that returns a Boolean, as you said that - in case that something's being found - you want to return TRUE). Code is pretty much similar to the previous function. SQL> create or replace function f_colname 2 (par_column_name in varchar2, 3 par_column_value in varchar2 4 ) 5 return boolean 6 is 7 l_str varchar2(200); 8 l_cnt number; 9 retval boolean := false; 10 begin 11 for cur_r in (select table_name 12 from user_tab_columns 13 where column_name = par_column_name 14 ) 15 loop 16 l_str := 'select count(*) from ' || cur_r.table_name || 17 ' where ' || par_column_name || ' = ' || 18 chr(39) || par_column_value || chr(39); 19 -- Display l_str first, to make sure that it is OK: 20 -- dbms_output.put_line(l_str); 21 execute immediate l_str into l_cnt; 22 if l_cnt > 0 then 23 retval := true; 24 exit; 25 end if; 26 end loop; 27 return retval; 28 end; 29 / Function created. Testing: as you can't return Boolean at SQL layer, you have to use an anonymous PL/SQL block, as follows: SQL> declare 2 l_ret boolean; 3 begin 4 if f_colname('DEPTNO', '15') then 5 dbms_output.put_line('It exists'); 6 else 7 dbms_output.put_line('It does not exist'); 8 end if; 9 end; 10 / It does not exist PL/SQL procedure successfully completed. SQL>
how to get the field name and value from a record dynamically
I have a procedure which receive as input parameter a record with 170 columns (it is based on the structure of a table). In the procedure I want to call a debugging procedure one of whose parameters is a text string containing all the field names and values of this record. For example: CREATE OR REPLACE PROCEDURE xxx (pi_record IN table_name%ROWTYPE) as text VARCHAR2(10000) := NULL; BEGIN ... text := 'pi_record.column1 = ' || pi_record.column1 || CHR(13) || 'pi_record.column2 = ' || pi_record.column2 || CHR(13) || ... 'pi_record.column170 = ' || pi_record.column170; logging_procedure (text); ... END; Is there any simple way to achieve this in a dynamic way (looping through record fields names and values) without enumerating all of them? Maybe something like this: CREATE OR REPLACE PROCEDURE xxx (pi_record IN table_name%ROWTYPE) as text VARCHAR2(10000) := NULL; BEGIN ... LOOP in pi_record.columns text := text || CHR(13) || pi_record.column.name || ' : ' || pi_record.column.value END LOOP logging_procedure (text); ... END; Many thanks,
Here's one way to do that. A package spec contains a variable whose type matches the one we'll use in a procedure. SQL> set serveroutput on SQL> create or replace package pkg_xxx 2 as 3 dept_rec dept%rowtype; 4 end; 5 / Package created. SQL> create or replace procedure xxx (pi_record in dept%rowtype) 2 as 3 text varchar2 (10000) := null; 4 l_str varchar2 (200); 5 l_var varchar2 (200); 6 begin 7 pkg_xxx.dept_rec := pi_record; 8 9 for cur_r in ( select column_name 10 from user_tab_columns 11 where table_name = 'DEPT' 12 order by column_id) 13 loop 14 l_str := 15 'begin ' 16 || ':x := to_char(pkg_xxx.dept_rec.' 17 || cur_r.column_name 18 || '); ' 19 || 'end; '; 20 21 execute immediate l_str using out l_var; 22 23 text := text || chr (10) || cur_r.column_name || ' = ' || l_var; 24 end loop; 25 26 dbms_output.put_line (text); 27 end; 28 / Procedure created. Now, let's pass something to the procedure and see what happens: SQL> declare 2 cursor c1 3 is 4 select * 5 from dept 6 where deptno = 10; 7 8 c1r c1%rowtype; 9 begin 10 open c1; 11 fetch c1 into c1r; 12 close c1; 13 14 xxx (c1r); 15 end; 16 / DEPTNO = 10 DNAME = ACCOUNTING LOC = NEW YORK PL/SQL procedure successfully completed. SQL> Huh, kind of works (if that's what you asked). Of course, it is just an example, you'll have to modify it if you want to get something really smart (hint: DATE columns).
The only idea I have is to insert the record into a TEMP table: CREATE OR REPLACE PROCEDURE xxx (pi_record IN TABLE_NAME%ROWTYPE) AS TEXT VARCHAR2(10000) := NULL; item VARCHAR2(1000); TABLE_DOES_NOT_EXIST EXCEPTION; PRAGMA EXCEPTION_INIT(TABLE_DOES_NOT_EXIST, -942); BEGIN BEGIN EXECUTE IMMEDIATE 'DROP TABLE TABLE_NAME_TMP'; EXCEPTION WHEN TABLE_DOES_NOT_EXIST then null; END; EXECUTE IMMEDIATE 'CREATE GLOBAL TEMPORARY TABLE TABLE_NAME_TMP AS SELECT * FROM TABLE_NAME WHERE ROWNUM = 0'; DELETE FROM TABLE_NAME_TMP; INSERT INTO TABLE_NAME_TMP VALUES pi_record; FOR aCol IN (SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS WHERE table_name = 'TABLE_NAME' ORDER BY COLUMN_ID) LOOP EXECUTE IMMEDIATE 'SELECT '||aCol.COLUMN_NAME||' FROM TABLE_NAME_TMP' INTO item; TEXT := TEXT || CHR(13) || aCol.COLUMN_NAME || ' : ' || item; END LOOP; DBMS_OUTPUT.PUT_LINE ( TEXT ); END; In case table TABLE_NAME has static attributes then you should skip dynamic DROP TABLE ... and CREATE GLOBAL TEMPORARY TABLE ... and create the TEMP table only once.
everyone! I got a different approach to get the difference between records dynamically: You just have to create the global variables on the package header as bellow: v_NAME_OF_TABLE_new NAME_OF_TABLE%rowtype; v_NAME_OF_TABLE_old NAME_OF_TABLE%rowtype; then create the function on your pkg body that return a boolean even if a field is different: function is_different(p_old NAME_OF_TABLE%rowtype, p_new NAME_OF_TABLE%rowtype) return boolean is cursor cols is select tb.COLUMN_NAME from all_tab_columns tb where tb.OWNER = 'DW' and tb.TABLE_NAME = 'NAME_OF_TABLE' order by tb.COLUMN_ID; l_sql varchar2(4000); l_new varchar2(4000); l_old varchar2(4000); begin pkg_NAME.v_NAME_OF_TABLE_new := p_new; pkg_NAME.v_NAME_OF_TABLE_old := p_old; for reg in cols loop l_sql := ' begin :x := pkg_NAME.v_NAME_OF_TABLE_new.'||reg.COLUMN_NAME||';'||' end;'; execute immediate l_sql using out l_new; l_sql := ' begin :x := pkg_NAME.v_NAME_OF_TABLE_old.'||reg.COLUMN_NAME||';'||' end;'; execute immediate l_sql using out l_old; --- dbms_output.put_line(l_new||' - '||l_old); if nvl(l_new,'NULO') <> nvl(l_old,'NULO') then return true; end if; end loop; return false; end; Atention: This can turn your process heavier and slower. That's all! Hope this can be helpful!
Print Record fields in PL/SQL
How can I print all the fields of a record variable in PL/SQL. The record variable has got lots of fields so is there a better way than printing each field? Also tried dynamic sql but didn't help.
Building on Ollies use of dbms_output, but for to dynamically go through the cursor set up for test /*create table temp (aa varchar2(50) , bb number , cc date ) ; insert into temp (aa,bb,cc) select chr(level+100) , level, sysdate+level from dual connect by level < 15 ; / */ Block to show the test (this assumes 11g) set serveroutput on declare l_cur SYS_REFCURSOR ; PROCEDURE CursorOutput( p_refcursor IN OUT SYS_REFCURSOR ) AS l_desc DBMS_SQL.DESC_TAB ; l_cols BINARY_INTEGER ; l_cursor BINARY_INTEGER ; v_varchar2 VARCHAR2( 4000 ) ; v_number NUMBER ; v_date DATE ; l_data varchar2( 32767 ) ; l_columnValue VARCHAR2( 32767 ) ; l_processedRows Number := 0; BEGIN /* Convert refcursor "parameter" to DBMS_SQL cursor... */ l_cursor := DBMS_SQL.TO_CURSOR_NUMBER( p_refcursor ); /* Describe the cursor... */ DBMS_SQL.DESCRIBE_COLUMNS( l_cursor, l_cols, l_desc ); /* Define columns to be fetched. We're only using V2, NUM, DATE for example... for a complete list of the col_types this link is accessible. http://download.oracle.com/docs/cd/B10501_01/server.920/a96540/sql_elements2a.htm#45504 http://forums.oracle.com/forums/thread.jspa?threadID=912475 if not a usable type, will throw new exception */ FOR i IN 1 .. l_cols LOOP IF l_desc(i).col_type = 2 THEN DBMS_SQL.DEFINE_COLUMN(l_cursor, i, v_number); ELSIF l_desc(i).col_type = 12 THEN DBMS_SQL.DEFINE_COLUMN(l_cursor, i, v_date); ELSif l_desc(i).col_type = 01 or l_desc(i).col_type = 96 then DBMS_SQL.DEFINE_COLUMN(l_cursor, i, v_varchar2, 4000); else --raise an exception if the user's query contains a datatype not (yet) supported by this procedure RAISE_APPLICATION_ERROR(-20000, 'Invalid Data Type for conversion to delimited file. {' || l_desc(i).col_name || '}'); END IF; END LOOP; /* -- print out the column names if desired FOR i IN 1 .. l_cols LOOP dbms_output.put_line('** ' || l_desc(i).col_name) ; END LOOP; */ /* Fetch all data... */ WHILE DBMS_SQL.FETCH_ROWS(l_cursor) > 0 LOOP dbms_output.put_line('LINE: ' || l_processedRows || ''); FOR i IN 1 .. l_cols LOOP if l_desc(i).col_type = 12 THEN --we are in a date DBMS_SQL.COLUMN_VALUE(l_cursor, i, v_date); v_varchar2 := to_char(v_date , 'dd-MON-yyyy' ) ; elsif l_desc(i).col_type = 2 THEN --we are in a number DBMS_SQL.COLUMN_VALUE(l_cursor, i, v_number); v_varchar2 := to_char(v_number) ; else --treat it as a string (should be varchar2,char,etc) DBMS_SQL.COLUMN_VALUE(l_cursor, i, v_varchar2); IF v_varchar2 IS NOT NULL THEN v_varchar2 := '"' || v_varchar2 || '"' ; ELSE v_varchar2 := ''; END IF ; end if ; dbms_output.put_line(l_desc(i).col_name || '=>' || v_varchar2) ; END LOOP; l_processedRows := l_processedRows + 1 ; END LOOP; dbms_sql.close_cursor(l_cursor); dbms_output.put_line('I found and processed ' || l_processedRows || ' rows .'); END; begin open l_cur for select * from temp; CursorOutput(p_refcursor => l_cur) ; end ; / will give you this result LINE: 0 AA=>"e" BB=>1 CC=>04-JAN-2012 LINE: 1 AA=>"f" BB=>2 CC=>05-JAN-2012 LINE: 2 AA=>"g" BB=>3 CC=>06-JAN-2012 LINE: 3 AA=>"h" BB=>4 CC=>07-JAN-2012 LINE: 4 AA=>"i" BB=>5 CC=>08-JAN-2012 LINE: 5 AA=>"j" BB=>6 CC=>09-JAN-2012 LINE: 6 AA=>"k" BB=>7 CC=>10-JAN-2012 LINE: 7 AA=>"l" BB=>8 CC=>11-JAN-2012 LINE: 8 AA=>"m" BB=>9 CC=>12-JAN-2012 LINE: 9 AA=>"n" BB=>10 CC=>13-JAN-2012 LINE: 10 AA=>"o" BB=>11 CC=>14-JAN-2012 LINE: 11 AA=>"p" BB=>12 CC=>15-JAN-2012 LINE: 12 AA=>"q" BB=>13 CC=>16-JAN-2012 LINE: 13 AA=>"r" BB=>14 CC=>17-JAN-2012 I found and processed 14 rows . I had done something similar to this to dynamically build a csv file utilizing these two links as sources http://www.oracle-developer.net/display.php?id=505 http://asktom.oracle.com/pls/apex/f?p=100:11:0::::P11_QUESTION_ID:88212348059 Depending on what you are going for, however, you may just want to run it in SQL Developer (or Toad) and export the results!
If it's a PL/SQL block that you are running in an IDE then you could at a pinch use DBMS_OUTPUT to output the values. http://docs.oracle.com/cd/B19306_01/appdev.102/b14258/d_output.htm For example: SET SERVEROUTPUT ON DECLARE -- Define the record TYPE test_rectype IS RECORD ( field1 NUMBER, field2 VARCHAR2 ); -- Define a variable for the record test_rec TEST_RECTYPE; BEGIN -- Populate the record test_rec.field1 := 1; test_rec.field2 := 'my value'; -- Enable the DBMS_OUTPUT DBMS_OUTPUT.enable(1000000); -- Send the output to the buffer DBMS_OUTPUT.put_line('Field1: '||test_rec.field1||', Field2: '||test_rec.field2); END; There is more to DBMS_OUTPUT so take a look at the docs from the link above. Alternatively, you could write the values to a file using UTL_FILE. http://docs.oracle.com/cd/B19306_01/appdev.102/b14258/u_file.htm Hope it helps...
Convert comma separated string to array in PL/SQL
How do I convert a comma separated string to a array? I have the input '1,2,3' , and I need to convert it into an array.
here is another easier option select to_number(column_value) as IDs from xmltable('1,2,3,4,5');
Oracle provides the builtin function DBMS_UTILITY.COMMA_TO_TABLE. Unfortunately, this one doesn't work with numbers: SQL> declare 2 l_input varchar2(4000) := '1,2,3'; 3 l_count binary_integer; 4 l_array dbms_utility.lname_array; 5 begin 6 dbms_utility.comma_to_table 7 ( list => l_input 8 , tablen => l_count 9 , tab => l_array 10 ); 11 dbms_output.put_line(l_count); 12 for i in 1 .. l_count 13 loop 14 dbms_output.put_line 15 ( 'Element ' || to_char(i) || 16 ' of array contains: ' || 17 l_array(i) 18 ); 19 end loop; 20 end; 21 / declare * ERROR at line 1: ORA-00931: missing identifier ORA-06512: at "SYS.DBMS_UTILITY", line 132 ORA-06512: at "SYS.DBMS_UTILITY", line 164 ORA-06512: at "SYS.DBMS_UTILITY", line 218 ORA-06512: at line 6 But with a little trick to prefix the elements with an 'x', it works: SQL> declare 2 l_input varchar2(4000) := '1,2,3'; 3 l_count binary_integer; 4 l_array dbms_utility.lname_array; 5 begin 6 dbms_utility.comma_to_table 7 ( list => regexp_replace(l_input,'(^|,)','\1x') 8 , tablen => l_count 9 , tab => l_array 10 ); 11 dbms_output.put_line(l_count); 12 for i in 1 .. l_count 13 loop 14 dbms_output.put_line 15 ( 'Element ' || to_char(i) || 16 ' of array contains: ' || 17 substr(l_array(i),2) 18 ); 19 end loop; 20 end; 21 / 3 Element 1 of array contains: 1 Element 2 of array contains: 2 Element 3 of array contains: 3 PL/SQL procedure successfully completed. Regards, Rob.
We can never run out of alternatives of doing the same thing differently, right? I recently found this is pretty handy: DECLARE BAR VARCHAR2 (200) := '1,2,3'; BEGIN FOR FOO IN ( SELECT REGEXP_SUBSTR (BAR, '[^,]+', 1, LEVEL) TXT FROM DUAL CONNECT BY REGEXP_SUBSTR (BAR, '[^,]+', 1, LEVEL) IS NOT NULL) LOOP DBMS_OUTPUT.PUT_LINE (FOO.TXT); END LOOP; END; Outputs: 1 2 3
I know Stack Overflow frowns on pasting URLs without explanations, but this particular page has a few really good options: http://www.oratechinfo.co.uk/delimited_lists_to_collections.html I particularly like this one, which converts the delimited list into a temporary table you can run queries against: /* Create the output TYPE, here using a VARCHAR2(100) nested table type */ SQL> CREATE TYPE test_type AS TABLE OF VARCHAR2(100); 2 / Type created. /* Now, create the function.*/ SQL> CREATE OR REPLACE FUNCTION f_convert(p_list IN VARCHAR2) 2 RETURN test_type 3 AS 4 l_string VARCHAR2(32767) := p_list || ','; 5 l_comma_index PLS_INTEGER; 6 l_index PLS_INTEGER := 1; 7 l_tab test_type := test_type(); 8 BEGIN 9 LOOP 10 l_comma_index := INSTR(l_string, ',', l_index); 11 EXIT WHEN l_comma_index = 0; 12 l_tab.EXTEND; 13 l_tab(l_tab.COUNT) := SUBSTR(l_string, l_index, l_comma_index - l_index); 14 l_index := l_comma_index + 1; 15 END LOOP; 16 RETURN l_tab; 17 END f_convert; 18 / Function created. /* Prove it works */ SQL> SELECT * FROM TABLE(f_convert('AAA,BBB,CCC,D')); COLUMN_VALUE -------------------------------------------------------------------------------- AAA BBB CCC D 4 rows selected.
Simple Code create or replace function get_token(text_is varchar2, token_in number, delim_is varchar2 := ';') return varchar2 is text_ls varchar2(2000); spos_ln number; epos _ln number; begin text_ls := delim_is || text_is || rpad(delim_is, token_in, delim_is); spos_ln := instr(text_ls, delim_is, 1, token_in); epos_ln := instr(text_ls, delim_is, 1, token_in+1); return substr(text_ls, spos_ln+1, epos_ln-spos_ln-1); end get_token;
Yes, it is very frustrating that dbms_utility.comma_to_table only supports comma delimieted lists and then only when elements in the list are valid PL/SQL identifies (so numbers cause an error). I have created a generic parsing package that will do what you need (pasted below). It is part of my "demo.zip" file, a repository of over 2000 files that support my training materials, all available at PL/SQL Obsession: www.toadworld.com/SF. Regards, Steven Feuerstein www.plsqlchallenge.com (daily PL/SQL quiz) CREATE OR REPLACE PACKAGE parse /* Generalized delimited string parsing package Author: Steven Feuerstein, steven#stevenfeuerstein.com Latest version always available on PL/SQL Obsession: www.ToadWorld.com/SF Click on "Trainings, Seminars and Presentations" and then download the demo.zip file. Modification History Date Change 10-APR-2009 Add support for nested list variations Notes: * This package does not validate correct use of delimiters. It assumes valid construction of lists. * Import the Q##PARSE.qut file into an installation of Quest Code Tester 1.8.3 or higher in order to run the regression test for this package. */ IS SUBTYPE maxvarchar2_t IS VARCHAR2 (32767); /* Each of the collection types below correspond to (are returned by) one of the parse functions. items_tt - a simple list of strings nested_items_tt - a list of lists of strings named_nested_items_tt - a list of named lists of strings This last type also demonstrates the power and elegance of string-indexed collections. The name of the list of elements is the index value for the "outer" collection. */ TYPE items_tt IS TABLE OF maxvarchar2_t INDEX BY PLS_INTEGER; TYPE nested_items_tt IS TABLE OF items_tt INDEX BY PLS_INTEGER; TYPE named_nested_items_tt IS TABLE OF items_tt INDEX BY maxvarchar2_t; /* Parse lists with a single delimiter. Example: a,b,c,d Here is an example of using this function: DECLARE l_list parse.items_tt; BEGIN l_list := parse.string_to_list ('a,b,c,d', ','); END; */ FUNCTION string_to_list (string_in IN VARCHAR2, delim_in IN VARCHAR2) RETURN items_tt; /* Parse lists with nested delimiters. Example: a,b,c,d|1,2,3|x,y,z Here is an example of using this function: DECLARE l_list parse.nested_items_tt; BEGIN l_list := parse.string_to_list ('a,b,c,d|1,2,3,4', '|', ','); END; */ FUNCTION string_to_list (string_in IN VARCHAR2 , outer_delim_in IN VARCHAR2 , inner_delim_in IN VARCHAR2 ) RETURN nested_items_tt; /* Parse named lists with nested delimiters. Example: letters:a,b,c,d|numbers:1,2,3|names:steven,george Here is an example of using this function: DECLARE l_list parse.named_nested_items_tt; BEGIN l_list := parse.string_to_list ('letters:a,b,c,d|numbers:1,2,3,4', '|', ':', ','); END; */ FUNCTION string_to_list (string_in IN VARCHAR2 , outer_delim_in IN VARCHAR2 , name_delim_in IN VARCHAR2 , inner_delim_in IN VARCHAR2 ) RETURN named_nested_items_tt; PROCEDURE display_list (string_in IN VARCHAR2 , delim_in IN VARCHAR2:= ',' ); PROCEDURE display_list (string_in IN VARCHAR2 , outer_delim_in IN VARCHAR2 , inner_delim_in IN VARCHAR2 ); PROCEDURE display_list (string_in IN VARCHAR2 , outer_delim_in IN VARCHAR2 , name_delim_in IN VARCHAR2 , inner_delim_in IN VARCHAR2 ); PROCEDURE show_variations; /* Helper function for automated testing */ FUNCTION nested_eq (list1_in IN items_tt , list2_in IN items_tt , nulls_eq_in IN BOOLEAN ) RETURN BOOLEAN; END parse; / CREATE OR REPLACE PACKAGE BODY parse IS FUNCTION string_to_list (string_in IN VARCHAR2, delim_in IN VARCHAR2) RETURN items_tt IS c_end_of_list CONSTANT PLS_INTEGER := -99; l_item maxvarchar2_t; l_startloc PLS_INTEGER := 1; items_out items_tt; PROCEDURE add_item (item_in IN VARCHAR2) IS BEGIN IF item_in = delim_in THEN /* We don't put delimiters into the collection. */ NULL; ELSE items_out (items_out.COUNT + 1) := item_in; END IF; END; PROCEDURE get_next_item (string_in IN VARCHAR2 , start_location_io IN OUT PLS_INTEGER , item_out OUT VARCHAR2 ) IS l_loc PLS_INTEGER; BEGIN l_loc := INSTR (string_in, delim_in, start_location_io); IF l_loc = start_location_io THEN /* A null item (two consecutive delimiters) */ item_out := NULL; ELSIF l_loc = 0 THEN /* We are at the last item in the list. */ item_out := SUBSTR (string_in, start_location_io); ELSE /* Extract the element between the two positions. */ item_out := SUBSTR (string_in , start_location_io , l_loc - start_location_io ); END IF; IF l_loc = 0 THEN /* If the delimiter was not found, send back indication that we are at the end of the list. */ start_location_io := c_end_of_list; ELSE /* Move the starting point for the INSTR search forward. */ start_location_io := l_loc + 1; END IF; END get_next_item; BEGIN IF string_in IS NULL OR delim_in IS NULL THEN /* Nothing to do except pass back the empty collection. */ NULL; ELSE LOOP get_next_item (string_in, l_startloc, l_item); add_item (l_item); EXIT WHEN l_startloc = c_end_of_list; END LOOP; END IF; RETURN items_out; END string_to_list; FUNCTION string_to_list (string_in IN VARCHAR2 , outer_delim_in IN VARCHAR2 , inner_delim_in IN VARCHAR2 ) RETURN nested_items_tt IS l_elements items_tt; l_return nested_items_tt; BEGIN /* Separate out the different lists. */ l_elements := string_to_list (string_in, outer_delim_in); /* For each list, parse out the separate items and add them to the end of the list of items for that list. */ FOR indx IN 1 .. l_elements.COUNT LOOP l_return (l_return.COUNT + 1) := string_to_list (l_elements (indx), inner_delim_in); END LOOP; RETURN l_return; END string_to_list; FUNCTION string_to_list (string_in IN VARCHAR2 , outer_delim_in IN VARCHAR2 , name_delim_in IN VARCHAR2 , inner_delim_in IN VARCHAR2 ) RETURN named_nested_items_tt IS c_name_position constant pls_integer := 1; c_items_position constant pls_integer := 2; l_elements items_tt; l_name_and_values items_tt; l_return named_nested_items_tt; BEGIN /* Separate out the different lists. */ l_elements := string_to_list (string_in, outer_delim_in); FOR indx IN 1 .. l_elements.COUNT LOOP /* Extract the name and the list of items that go with the name. This collection always has just two elements: index 1 - the name index 2 - the list of values */ l_name_and_values := string_to_list (l_elements (indx), name_delim_in); /* Use the name as the index value for this list. */ l_return (l_name_and_values (c_name_position)) := string_to_list (l_name_and_values (c_items_position), inner_delim_in); END LOOP; RETURN l_return; END string_to_list; PROCEDURE display_list (string_in IN VARCHAR2 , delim_in IN VARCHAR2:= ',' ) IS l_items items_tt; BEGIN DBMS_OUTPUT.put_line ( 'Parse "' || string_in || '" using "' || delim_in || '"' ); l_items := string_to_list (string_in, delim_in); FOR indx IN 1 .. l_items.COUNT LOOP DBMS_OUTPUT.put_line ('> ' || indx || ' = ' || l_items (indx)); END LOOP; END display_list; PROCEDURE display_list (string_in IN VARCHAR2 , outer_delim_in IN VARCHAR2 , inner_delim_in IN VARCHAR2 ) IS l_items nested_items_tt; BEGIN DBMS_OUTPUT.put_line( 'Parse "' || string_in || '" using "' || outer_delim_in || '-' || inner_delim_in || '"'); l_items := string_to_list (string_in, outer_delim_in, inner_delim_in); FOR outer_index IN 1 .. l_items.COUNT LOOP DBMS_OUTPUT.put_line( 'List ' || outer_index || ' contains ' || l_items (outer_index).COUNT || ' elements'); FOR inner_index IN 1 .. l_items (outer_index).COUNT LOOP DBMS_OUTPUT.put_line( '> Value ' || inner_index || ' = ' || l_items (outer_index) (inner_index)); END LOOP; END LOOP; END display_list; PROCEDURE display_list (string_in IN VARCHAR2 , outer_delim_in IN VARCHAR2 , name_delim_in IN VARCHAR2 , inner_delim_in IN VARCHAR2 ) IS l_items named_nested_items_tt; l_index maxvarchar2_t; BEGIN DBMS_OUTPUT.put_line( 'Parse "' || string_in || '" using "' || outer_delim_in || '-' || name_delim_in || '-' || inner_delim_in || '"'); l_items := string_to_list (string_in , outer_delim_in , name_delim_in , inner_delim_in ); l_index := l_items.FIRST; WHILE (l_index IS NOT NULL) LOOP DBMS_OUTPUT.put_line( 'List "' || l_index || '" contains ' || l_items (l_index).COUNT || ' elements'); FOR inner_index IN 1 .. l_items (l_index).COUNT LOOP DBMS_OUTPUT.put_line( '> Value ' || inner_index || ' = ' || l_items (l_index) (inner_index)); END LOOP; l_index := l_items.NEXT (l_index); END LOOP; END display_list; PROCEDURE show_variations IS PROCEDURE show_header (title_in IN VARCHAR2) IS BEGIN DBMS_OUTPUT.put_line (RPAD ('=', 60, '=')); DBMS_OUTPUT.put_line (title_in); DBMS_OUTPUT.put_line (RPAD ('=', 60, '=')); END show_header; BEGIN show_header ('Single Delimiter Lists'); display_list ('a,b,c'); display_list ('a;b;c', ';'); display_list ('a,,b,c'); display_list (',,b,c,,'); show_header ('Nested Lists'); display_list ('a,b,c,d|1,2,3|x,y,z', '|', ','); show_header ('Named, Nested Lists'); display_list ('letters:a,b,c,d|numbers:1,2,3|names:steven,george' , '|' , ':' , ',' ); END; FUNCTION nested_eq (list1_in IN items_tt , list2_in IN items_tt , nulls_eq_in IN BOOLEAN ) RETURN BOOLEAN IS l_return BOOLEAN := list1_in.COUNT = list2_in.COUNT; l_index PLS_INTEGER := 1; BEGIN WHILE (l_return AND l_index IS NOT NULL) LOOP l_return := list1_in (l_index) = list2_in (l_index); l_index := list1_in.NEXT (l_index); END LOOP; RETURN l_return; EXCEPTION WHEN NO_DATA_FOUND THEN RETURN FALSE; END nested_eq; END; /
Using a pipelined table function: SQL> CREATE OR REPLACE TYPE test_type 2 AS 3 TABLE OF VARCHAR2(100) 4 / Type created. SQL> CREATE OR REPLACE FUNCTION comma_to_table( 2 p_list IN VARCHAR2) 3 RETURN test_type PIPELINED 4 AS 5 l_string LONG := p_list || ','; 6 l_comma_index PLS_INTEGER; 7 l_index PLS_INTEGER := 1; 8 BEGIN 9 LOOP 10 l_comma_index := INSTR(l_string, ',', l_index); 11 EXIT 12 WHEN l_comma_index = 0; 13 PIPE ROW ( TRIM(SUBSTR(l_string, l_index, l_comma_index - l_index))); 14 l_index := l_comma_index + 1; 15 END LOOP; 16 RETURN; 17 END comma_to_table; 18 / Function created. Let's see the output: SQL> SELECT * 2 FROM TABLE(comma_to_table('12 3,456,,,,,abc,def')) 3 / COLUMN_VALUE ------------------------------------------------------------------------------ 12 3 456 abc def 8 rows selected. SQL>
A quick search on my BBDD took me to a function called split: create or replace function split ( p_list varchar2, p_del varchar2 := ',' ) return split_tbl pipelined is l_idx pls_integer; l_list varchar2(32767) := p_list;AA l_value varchar2(32767); begin loop l_idx := instr(l_list,p_del); if l_idx > 0 then pipe row(substr(l_list,1,l_idx-1)); l_list := substr(l_list,l_idx+length(p_del)); else pipe row(l_list); exit; end if; end loop; return; end split; I don't know if it'll be of use, but we found it here...
I was looking for a similar solution where I had multi-byte characters (hyphen, whitespace, underscore) in comma separated lists. So dbms_utility.comma_to_table didn't work for me. declare curr_val varchar2 (255 byte); input_str varchar2 (255 byte); remaining_str varchar2 (255 byte); begin remaining_str := input_str || ',dummy'; -- this value won't output while (regexp_like (remaining_str, '.+,.+')) loop curr_val := substr (remaining_str, 1, instr (remaining_str, ',') - 1); remaining_str = substr (remaining_str, instr (remaining_str, ',') + 1); dbms_output.put_line (curr_val); end loop; end; This is not an expert answer so I hope someone would improve this answer.
TYPE string_aa IS TABLE OF VARCHAR2(32767) INDEX BY PLS_INTEGER; FUNCTION string_to_list(p_string_in IN VARCHAR2) RETURN string_aa IS TYPE ref_cursor IS ref cursor; l_cur ref_cursor; l_strlist string_aa; l_x PLS_INTEGER; BEGIN IF p_string_in IS NOT NULL THEN OPEN l_cur FOR SELECT regexp_substr(p_string_in,'[^,]+', 1, level) FROM dual CONNECT BY regexp_substr(p_string_in, '[^,]+', 1, level) IS NOT NULL; l_x := 1; LOOP FETCH l_cur INTO l_strlist(l_x); EXIT WHEN l_cur%notfound; -- excludes NULL items e.g. 1,2,,,,5,6,7 l_x := l_x + 1; END LOOP; END IF; RETURN l_strlist; END string_to_list;
I just want to write more clearly Richard and Mike's answers. Method 1: VARRAY USAGE DECLARE TYPE v_array_type IS VARRAY (10) OF NUMBER; var v_array_type; begin select to_number(column_value) BULK COLLECT into var from xmltable('1,2,3,4,5,6,7,8,9,10'); for i in 1..var.count LOOP dbms_output.put_line(var(i)); END LOOP; end; But in this method you must declare array size. If you want more dynamic method you could use Mike's method. METHOD 2: begin FOR obj IN (SELECT TO_NUMBER(column_value) as ID FROM xmltable('1,2,3,4,5,6,7,8,9,10,11')) LOOP dbms_output.put_line(obj.ID); END LOOP; end;
you can use oracle method declare myarray apex_application_global.vc_arr2; begin myarray = apex_util.string_to_table('1,2,3', ',');//you can use any symbols instead of ',' for split for i in 1..myarray.COUNT loop dbms_output.put_line(myarray(i)); end loop;
Another possibility is: create or replace FUNCTION getNth ( input varchar2, nth number ) RETURN varchar2 AS nthVal varchar2(80); BEGIN with candidates (s,e,n) as ( select 1, instr(input,',',1), 1 from dual union all select e+1, instr(input,',',e+1), n+1 from candidates where e > 0) select substr(input,s,case when e > 0 then e-s else length(input) end) into nthVal from candidates where n=nth; return nthVal; END getNth; It's a little too expensive to run, as it computes the complete split every time the caller asks for one of the items in there...
The solution presented below by Stewart Ashton in this link is pretty handy. He eliminates the need for the value list to be integer, so you can use string list. In the WITH clause, he surrounds the values with single quotes then converts it to a table having a single column of type VARCHAR2. https://stewashton.wordpress.com with data as ( select '"'||replace(:txt, ',', '","')||'"' str from dual ) select xmlcast(column_value as varchar2(4000)) subs from data, xmltable(str);
declare seprator varchar2(1):=','; dosweeklist varchar2(4000):='a,b,c'; begin for i in (SELECT SUBSTR(dosweeklist, case when level=1 then 1 else INSTR(dosweeklist,seprator,1,LEVEL-1)+1 end, NVL(NULLIF(INSTR(dosweeklist,seprator,1,LEVEL),0),length(dosweeklist)+1) - case when level=1 then 1 else INSTR(dosweeklist,seprator,1,LEVEL-1)+1 end) dat FROM dual CONNECT BY LEVEL <= LENGTH(dosweeklist) - LENGTH(REPLACE(dosweeklist,seprator,'')) +1) loop dbms_output.put_line(i.dat); end loop; end; / so select query only in for loop can do the trick, by replacing dosweeklist as your delimited string and seprator as your delimited character. Lets see output a b c
declare v_str varchar2(100) := '1,2,3,4,6,7,8,9,0,'; v_str1 varchar2(100); v_comma_pos number := 0; v_start_pos number := 1; begin loop v_comma_pos := instr(v_str,',',v_start_pos); v_str1 := substr(v_str,v_start_pos,(v_comma_pos - v_start_pos)); dbms_output.put_line(v_str1); if v_comma_pos = 0 then v_str1 := substr(v_str,v_start_pos); dbms_output.put_line(v_str1); exit; end if; v_start_pos := v_comma_pos + 1; if v_comma_pos = 0 then exit; end if; end loop; end;
You can use Replace Function to replace comma easily. To Do this- The syntax for the REPLACE function in SQL Server (Transact-SQL) is: REPLACE( string, string_to_replace, replacement_string ) Parameters or Arguments string : The source string from which a sequence of characters will be replaced by another set of characters. string_to_replace : The string that will be searched for in string1. replacement_string : The replacement string. All occurrences of string_to_replace will be replaced with replacement_string in string1. Note : The REPLACE function performs a replacement that is not case-sensitive. So all occurrences of string_to_replace will be replaced with replacement_string regardless of the case of string_to_replace or replacement_string For Example : SELECT REPLACE('Kapil,raj,chouhan', ',' , ' ') from DUAL; Result : Kapil raj chouhan SELECT REPLACE('I Live In India', ' ' , '-') from DUAL; Result : I-Live-In-India SELECT REPLACE('facebook.com', 'face' , 'friends') from DUAL; Result : friendsbook.com I Hope it will be usefull for you.