Oracle arrays "contains" function - sql

I want to store a list of primary keys to other records in the same table. I then want to be able to perform a select that resembles something like:
SELECT * FROM mytable WHERE myarraycol CONTAINS '123'
I've seen that Oracle has an array data type. However it looks like the EXISTS function only verifies if the element exists at the specified index. Is there a way to verify that a given variable is in the array data type column in a single SQL query?
As an alternative to using the array data type I tried storing the PKs as a comma-delimited string like "123,324,543,23432." My query then looked like:
SELECT * FROM mytable WHERE mystringcol LIKE '%123%'
if I wanted all records with the PK '123' in it. The problem with this (among many others) is that if another record has a value "432,9912399,432" this record will show up because of the "123" in the "9912399."
One way I could solve this problem using "LIKE" and string could be to have my where clause be:
WHERE mystringcol LIKE '%,123,% OR mystringcol LIKE '%123, OR mystringcol LIKE '%,123
to test for "123" being in the middle, start or end of the entire string, but that starts to get ugly and I'd rather not do it this way.
Has anyone done something like this before and can point me in the right direction?

In theory Oracle has MEMBER OF function for collection but regarding 3rd normal form you idea looks strange.
SQL> CREATE OR REPLACE PROCEDURE member_of_example AS
2 TYPE nestedTableType IS TABLE OF VARCHAR2(10);
3 myTable1 nestedTableType;
4 result BOOLEAN;
5 BEGIN
6 myTable1 := nestedTableType('F', 'G', 'S');
7 result := 'George' MEMBER OF myTable1;
8 IF result THEN
9 DBMS_OUTPUT.PUT_LINE('''George'' is a member');
10 END IF;
11 END member_of_example;
12 /
An example for function to get the plsql table from comma-separated stuff:
CREATE OR REPLACE function Str2NmbTbl(p_str varchar2) return number_table is
l_col number_table := number_table();
l_pos number;
l_cnt number := 1;
l_num number;
begin
l_pos := instr(p_str, '[', l_cnt);
while l_pos > 0 loop
l_num := to_number(substr(p_str, l_pos + 1, instr(p_str, ']', 1, l_cnt) - l_pos - 1));
l_col.extend;
l_col(l_cnt) := l_num;
l_cnt := l_cnt + 1;
l_pos := instr(p_str, '[', l_pos + 1);
end loop;
return l_col;
end;
/
with s as
(select 1 id, '[11412][21][3131][3333]' str from dual union all
select 2 id, '[64376][553]' str from dual union all
select 3 id, '[5943][74621][19][3333][0]' str from dual union all
select 4 id, '[21593][22321][43][094]' str from dual --union all
)
select id, Str2NmbTbl(str) collctn
from s;

Related

How to select all rows from the oracle PL/SQL collection into SYS_REFCURSOR

Note: I have seen many solution and all says I can not use SQL with a PL/SQL type. I must have to use CREATE or REPLACE, but my restriction is I can not use system object for this task.
What I have tried the below example returns only last row.
create or replace PROCEDURE SP_TEST (TEST_cursor OUT SYS_REFCURSOR)IS
TYPE TEMP_RECORD IS RECORD(
entries NUMBER,
name VARCHAR2(50),
update VARCHAR2(200)
);
TYPE TEMP_TABLE IS TABLE OF TEMP_RECORD INDEX BY PLS_INTEGER;
VAR_TEMP TEMP_TABLE;
IDX PLS_INTEGER := 0;
BEGIN
VAR_TEMP(IDX).cur_entries := 1;
VAR_TEMP(IDX).cur_entries := 2;
OPEN TEST_cursor FOR
SELECT VAR_TEMP(idx).cur_entries from dual;
END SP_TEST;
Another way tried.
OPEN TEST_cursor FOR
SELECT * FROM TABLE(VAR_TEMP)
--- It gives compilation error ora-
Given that you can't create an object in the database, the only solution I can think of is to use dynamic SQL:
CREATE TYPE temp_record AS OBJECT
(
entries NUMBER,
entry_name VARCHAR2 (50),
update_value VARCHAR2 (200)
);
CREATE TYPE temp_table IS TABLE OF temp_record;
CREATE OR REPLACE PROCEDURE sp_test (test_cursor OUT SYS_REFCURSOR) IS
var_temp temp_table := temp_table ();
strSql VARCHAR2(32767);
BEGIN
-- Populate the temp table, or pass it in from elsewhere
var_temp.EXTEND();
var_temp (var_temp.LAST).entries := 1;
var_temp (var_temp.LAST).entry_name := 'test';
FOR i IN 1..var_temp.COUNT LOOP
strSql := strSql ||
CASE
WHEN LENGTH(strSql) > 0 THEN ' UNION ALL '
ELSE NULL
END ||
'SELECT ' || var_temp.ENTRIES || ' ENTRIES,' ||
'''' || var_temp.ENTRY_NAME || ''' ENTRY_NAME FROM DUAL';
END LOOP;
OPEN test_cursor FOR strSql;
END sp_test;
Now, I may have messed up the string concatenation logic there a bit, but the objective is to end up with an SQL string which looks something like
SELECT 1 ENTRIES,'test' ENTRY_NAME FROM DUAL
UNION ALL
SELECT 2 ENTRIES,'test 2' ENTRY_NAME FROM DUAL
UNION ALL
SELECT 3 ENTRIES,'test_3' ENTRY_NAME FROM DUAL
but, of course, without the nice white space and etc.
The 32K limit on dynamic SQL may bite you eventually, but if push comes to shove you can the DBMS_SQL package to handle arbitrarily large SQL text, although that presents its own challenges.
Best of luck.
In order to reference types in SQL (as opposed to PL/SQL), they must be created as objects in the database. This is effectively a scope issue: when you run SQL you are shifting to a different context. Any structures that you have created locally are not available there.
CREATE TYPE temp_record AS OBJECT
(
entries NUMBER,
entry_name VARCHAR2 (50),
update_value VARCHAR2 (200)
);
CREATE TYPE temp_table IS TABLE OF temp_record;
CREATE OR REPLACE PROCEDURE sp_test (test_cursor OUT SYS_REFCURSOR) IS
var_temp temp_table := temp_table ();
BEGIN
var_temp.EXTEND ();
var_temp (var_temp.LAST).entries := 1;
var_temp (var_temp.LAST).entry_name := 'test';
OPEN test_cursor FOR SELECT * FROM TABLE (var_temp);
END sp_test;

How to make a select from a cursor that returns from a plsql function ORACLE

i have a function that return a cursor my package is like this
FUNCTION SEDIRUNTIME (sede varchar2) return SYS_REFCURSOR
this cursor return x number of row with only one value, for example :
ROW1 - 34
ROW2 - 55
ROW3 - 56 ecc. ecc.
now i have i select like this
.. AND field in (select DBK_ENIN_REPORT.*SEDIRUNTIME*(sede) from dual)
this will simulate a clause IN which we can know the values at run time.
for example, based on the location parameter, the cursor can give me 22 and 34 rather than just 56 or 78 98 09.
written so i do not work from error number 00932 incoherent data types.
solutions?
You cannot use a CURSOR like that.
But you can change the function to return a collection:
CREATE TYPE Numberlist IS TABLE OF NUMBER;
/
CREATE FUNCTION DBK_ENIN_REPORT.SEDIRUNTIME (
sede varchar2
) return NumberList
IS
out_numbers NumberList;
BEGIN
SELECT id
BULK COLLECT INTO out_numbers
FROM your_table;
RETURN out_numbers;
END;
/
Then you can do:
.. AND field MEMBER OF DBK_ENIN_REPORT.SEDIRUNTIME(sede)
or
.. AND field IN ( SELECT COLUMN_VALUE FROM TABLE( DBK_ENIN_REPORT.SEDIRUNTIME(sede) ) )
Well I would say you CAN do it but you need to twist little bit. You can see how i have done it as below. I take employee table as and example.
Create function:
CREATE OR REPLACE FUNCTION SEDIRUNTIME (sede VARCHAR2)
RETURN SYS_REFCURSOR
AS
cur SYS_REFCURSOR;
BEGIN
OPEN cur FOR SELECT employee_id FROM employee;
RETURN cur;
END;
/
Anonymous block which you can implement in your package as Procedure;
DECLARE
x SYS_REFCURSOR;
y NUMBER;
v VARCHAR2 (100);
v_sql VARCHAR2 (200);
TYPE var_emp IS TABLE OF employee%ROWTYPE
INDEX BY PLS_INTEGER;
v_emp var_emp;
BEGIN
x := SEDIRUNTIME ('sede');
LOOP
FETCH x INTO y;
v := v || ',' || y;
EXIT WHEN x%NOTFOUND;
END LOOP;
--Created the IN clause list
v := LTRIM (v, ',');
v_sql := 'Select * from employee where employee_id in (' || v || ')';
EXECUTE IMMEDIATE v_sql BULK COLLECT INTO v_emp;
FOR i IN 1 .. v_emp.COUNT
LOOP
DBMS_OUTPUT.put_line ( v_emp (i).employee_id || '--' || v_emp (i).first_name);
END LOOP;
END;
OUTPUT:
SQL> /
1--XXX
2--YYY
PL/SQL procedure successfully completed.
SQL>
PS: Ofcourse the solution provided by MTO is going to be much faster that this.

How do you specify IN clause in a dynamic query using a variable?

In PL/SQL, you can specify the values for the IN operator using concatenation:
v_sql := 'select field1
from table1
where field2 in (' || v_list || ')';
Is it possible to do the same using a variable?
v_sql := 'select field1
from table1
where field2 in (:v_list)';
If so, how?
EDIT: With reference to Marcin's answer, how do I select from the resultant table?
declare
cursor c_get_csv_as_tables is
select in_list(food_list) food_list
from emp_food
where emp_type = 'PERM';
cursor c_get_food_list (v_food_table varchar2Table)is
select *
from v_food_table;
begin
for i in c_get_csv_as_tables loop
for j in c_get_food_list(i.food_list) loop
dbms_output.put_line(j.element);
end loop;
end loop;
end;
I get the following error:
ORA-06550: line 10, column 6:
PL/SQL: ORA-00942: table or view does not exist
ORA-06550: line 9, column 1:
PL/SQL: SQL Statement ignored
ORA-06550: line 15, column 34:
PLS-00364: loop index variable 'J' use is invalid
ORA-06550: line 15, column 13:
PL/SQL: Statement ignored
Like in #Sathya link, you can bind the varray (I took #Codo example):
CREATE OR REPLACE TYPE str_tab_type IS VARRAY(10) OF VARCHAR2(200);
/
DECLARE
l_str_tab str_tab_type;
l_count NUMBER;
v_sql varchar2(3000);
BEGIN
l_str_tab := str_tab_type();
l_str_tab.extend(2);
l_str_tab(1) := 'TABLE';
l_str_tab(2) := 'INDEX';
v_sql := 'SELECT COUNT(*) FROM all_objects WHERE object_type IN (SELECT COLUMN_VALUE FROM TABLE(:v_list))';
execute immediate v_sql into l_count using l_str_tab;
dbms_output.put_line(l_count);
END;
/
UPDATE: the first command can be replaced with:
CREATE OR REPLACE TYPE str_tab_type IS TABLE OF VARCHAR2(200);
/
then call:
l_str_tab.extend(1);
when ever you add a value
Unfortunately you cannot bind a list like this, however you can use a table function. Read this
Here's an example of usage based on your code:
declare
cursor c_get_csv_as_tables is
select in_list(food_list) food_list
from emp_food
where emp_type = 'PERM';
cursor c_get_food_list (v_food_table varchar2Table)is
select column_value food
from TABLE(v_food_table);
begin
for i in c_get_csv_as_tables loop
for j in c_get_food_list(i.food_list) loop
dbms_output.put_line(j.food);
end loop;
end loop;
end;
I used here a column_value pseudocolumn
Bind variable can be used in Oracle SQL query with "in" clause.
Works in 10g; I don't know about other versions.
Bind variable is varchar up to 4000 characters.
Example: Bind variable containing comma-separated list of values, e.g.
:bindvar = 1,2,3,4,5
select * from mytable
where myfield in
(
SELECT regexp_substr(:bindvar,'[^,]+', 1, level) items
FROM dual
CONNECT BY regexp_substr(:bindvar, '[^,]+', 1, level) is not null
);
As per #Marcin's answer you can't do this, however, there's a fair bit to add to that, as your query should actually work, i.e. run.
Simply put, you cannot use a bind variable for a table or column. Not only that, bind variables they are assumed to be a character, so if you want a number you have to use to_number(:b1) etc.
This is where your query falls down. As you're passing in a string Oracle assumes that your entire list is a single string. Thus you are effectively running:
select field1
from table1
where field2 = v_list
There is no reason why you can't do this a different way though. I'm going to assume you're dynamically creating v_list, which means that all you need to do is create this list differently. A series of or conditions is, purportedly :-), no different to using an in.
By purportedly, I mean never rely on something that's untested. Although Tom does say in the link that there may be performance constraints there's no guarantee that it wasn't quicker than using in to begin with. The best thing to do is to run the trace on your query and his and see what difference there is, if any.
SQL> set serveroutput on
SQL>
SQL> declare
2
3 l_string varchar2(32767);
4 l_count number;
5
6 begin
7
8 for xx in ( select rownum as rnum, a.*
9 from user_tables a
10 where rownum < 20 ) loop
11
12 if xx.rnum = 1 then
13 l_string := 'table_name = ''' || xx.table_name || '''';
14 else
15 l_string := l_string || ' or table_name = ''' || xx.table_name || '
''';
16 end if;
17
18 end loop;
19
20 execute immediate 'select count(*)
21 from user_tables
22 where ' || l_string
23 into l_count
24 ;
25
26 dbms_output.put_line('count is ' || l_count);
27
28 end;
29 /
count is 19
PL/SQL procedure successfully completed.

PLSQL - How to retrieve values into a collection given an array of values?

I have a procedure that accepts an array of folder IDs and needs to return a list of document IDs. Folders are associated to documents in a one-to-many relationship--there are many documents for each folder. Specifically, there is a documents table which has a parent_folderid fk to the folders table.
This is what I have:
PROCEDURE get_folder_documents_ (
paa_folderids_i IN gtyp_folderids_table
) IS
lnt_temp_docids &&MATTER_SCHEMA..docid_tab := &&MATTER_SCHEMA..docid_tab();
lv_current_table_size NUMBER := gnt_documentids.COUNT;
BEGIN
FOR i IN paa_folderids_i.FIRST .. paa_folderids_i.LAST
LOOP
SELECT documentid
BULK COLLECT INTO lnt_temp_docids
FROM t$documents
WHERE parent_folderid = paa_folderids_i(i);
FOR j IN 1 .. lnt_temp_docids.COUNT
LOOP
lv_current_table_size := lv_current_table_size + 1;
gnt_documentids.EXTEND(1);
gnt_documentids(lv_current_table_size) := lnt_temp_docids(j);
END LOOP;
END LOOP;
END get_folder_documents_;
Is there a better way?
If gtyp_folderids_table is declared as a SQL type (as opposed to a PL/SQL type) you could use it in a SQL statement via a table() function like this:
SELECT documentid
BULK COLLECT INTO gnt_documentids
FROM t$documents
WHERE parent_folderid in ( select * from table( paa_folderids_i));
edit
If you want a PL/SQL answer, in 10g there is a more effective way - or at least an approach which requires less typing ;).
Oracle introduced some neat-o set operators which we can use with collections. The following example uses MULTISET UNION to munge several collections into one...
SQL> set serveroutput on size unlimited
SQL>
SQL> declare
2 v1 sys.dbms_debug_vc2coll
3 := sys.dbms_debug_vc2coll('SAM I AM', 'FOX IN SOCKS');
4 v2 sys.dbms_debug_vc2coll
5 := sys.dbms_debug_vc2coll('MR KNOX', 'GRINCH');
6 v3 sys.dbms_debug_vc2coll
7 := sys.dbms_debug_vc2coll('LORAX', 'MAISIE');
8 v_all sys.dbms_debug_vc2coll := sys.dbms_debug_vc2coll();
9 begin
10 dbms_output.put_line('V_ALL has '|| v_all.count() ||' elements');
11 v_all := v1 multiset union v2;
12 dbms_output.put_line('V_ALL has '|| v_all.count() ||' elements');
13 v_all := v_all multiset union v3;
14 dbms_output.put_line('V_ALL has '|| v_all.count() ||' elements');
15 end;
16 /
V_ALL has 0 elements
V_ALL has 4 elements
V_ALL has 6 elements
PL/SQL procedure successfully completed.
SQL>
Find out more about collections in 10g.

For each string, execute a function/procedure

I'd like to loop through a list of strings and execute a function/procedure with each string as the argument.
What's the best alternative to the following generic code (since it's not legal):
set serveroutput on;
begin
FOR r IN ('The', 'Quick', 'brown', 'fox')
LOOP
dbms_output.put_line( r );
END LOOP;
end;
I assume there might be pattern for this.
Just for completeness, a pure PL/SQL solution.
SQL> set serveroutput on
SQL>
SQL> declare
2 my_array sys.dbms_debug_vc2coll
3 := sys.dbms_debug_vc2coll('The', 'Quick', 'brown', 'fox');
4 begin
5 for r in my_array.first..my_array.last
6 loop
7 dbms_output.put_line( my_array(r) );
8 end loop;
9 end;
10 /
The
Quick
brown
fox
PL/SQL procedure successfully completed.
SQL>
This uses the preclared sys.dbms_debug_vc2coll datatype, which has quite a generous definition ...
SQL> desc sys.dbms_debug_vc2coll
sys.dbms_debug_vc2coll TABLE OF VARCHAR2(1000)
SQL>
... so, like Gary says, you may wish to declare your own. Especially if your strings are short and you have lots of them.
DECLARE
-- 1. declare a list type
TYPE STR_LIST_TYPE IS TABLE OF VARCHAR2(15);
-- 2. declare the variable of the list
V_STR_VALUES STR_LIST_TYPE;
-- 3. optional variable to store single values
V_STR_VALUE VARCHAR2(15);
BEGIN
-- 4. initialize the list of values to be iterated in a for-loop
V_STR_VALUES := STR_LIST_TYPE('String 1','String 2');
-- 5. iterating over the values
FOR INDX IN V_STR_VALUES.FIRST..V_STR_VALUES.LAST
LOOP
-- 6. accessing the value itself
V_STR_VALUE := V_STR_VALUES(INDX);
END LOOP;
END;
I generally use my own collection type, but you can use the built-in sys.dbms_debug_vc2coll
select column_value from table(sys.dbms_debug_vc2coll('The', 'Quick', 'brown', 'fox'));
[I incorrectly had column_name not column_value. Thanks for the correction]
The answer here depends on where the strings come from. In a non 'database language' you would probably get the strings into an array somehow, and then loop over the array, as you have illustrated above. The question is, is that list of strings hardcoded, or are you selecting them from a database table?
OMG Ponies solution will work, but it involves a possibly needless select. You may be better using PLSQL table or varrays - as I said, it depends on how you get the strings into your program that you need to process. Here is an example using plsql tables:
declare
type myarray is table of varchar2(255) index by binary_integer;
v_array myarray;
begin
v_array(v_array.count + 1) := 'The';
v_array(v_array.count + 1) := 'quick';
v_array(v_array.count + 1) := 'brown';
v_array(v_array.count + 1) := 'fox';
for i in 1..v_array.count loop
dbms_output.put_line(v_array(i));
end loop;
end;
/
Use:
SELECT package.your_function(x.col)
FROM (SELECT 'The' AS col
FROM DUAL
UNION ALL
SELECT 'Quick'
FROM DUAL
UNION ALL
SELECT 'brown'
FROM DUAL
UNION ALL
SELECT 'fox'
FROM DUAL) x
Oracle 9i+, Using Subquery Factoring (AKA CTE)
WITH list AS (
SELECT 'The' AS col
FROM DUAL
UNION ALL
SELECT 'Quick'
FROM DUAL
UNION ALL
SELECT 'brown'
FROM DUAL
UNION ALL
SELECT 'fox'
FROM DUAL)
SELECT package.your_function(x.col)
FROM list x
set serveroutput on;
begin
dbms_output.put_line('The');
dbms_output.put_line('Quick');
dbms_output.put_line('brown');
dbms_output.put_line('fox');
end;