PL/SQL - Use "List" Variable in Where In Clause - oracle

In PL/SQL, how do I declare variable MyListOfValues that contains multiple values (MyValue1, MyValue2, etc.)
SELECT *
FROM DatabaseTable
WHERE DatabaseTable.Field in MyListOfValues
I am using Oracle SQL Developer

Create the SQL type like this:
CREATE TYPE MyListOfValuesType AS TABLE OF VARCHAR2(4000);
And then use it in a SQL statement
DECLARE
MyListOfValues MyListOfValuesType;
BEGIN
MyListOfValues := MyListOfValuesType('MyValue1', 'MyValue2');
FOR rec IN (
SELECT *
FROM DatabaseTable
WHERE DatabaseTable.Field in (
SELECT * FROM TABLE(MyListOfValues)
)
)
LOOP
...
END LOOP;
END;
Up until Oracle 11g, this only works with a SQL TABLE type, not with a PL/SQL TABLE type. With Oracle 12c, you could also use PL/SQL types.

Use a collection:
CREATE TYPE Varchar2TableType AS TABLE OF VARCHAR2(200);
Or use a built-in type like SYS.ODCIVARCHAR2LIST or SYS.ODCINUMBERLIST:
VARIABLE cursor REFCURSOR;
DECLARE
your_collection SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST();
BEGIN
your_collection.EXTEND( 100 );
your_collection( 1) := 'Some value';
your_collection( 2) := 'Some other value';
-- ...
your_collection(100) := DBMS_RANDOM.STRING( 'x', 20 );
OPEN :cursor FOR
SELECT t.*
FROM your_table t
INNER JOIN
TABLE( your_collection ) c
ON t.id = c.COLUMN_VALUE;
END;
/
PRINT cursor;

How about using a WITH clause which basically builds a temp table? Not real reusable. You could use an array or I would argue joining to a lookup table would be better.
WITH MyListOfValues(col1) AS (
select 'MyValue1' from dual union
select 'MyValue2' from dual union
select 'MyValue3' from dual
)
SELECT *
FROM DatabaseTable
WHERE Column in (
select col1
from MyListOfValues);

Related

How to assign multiple values to a variable using into clause in oracle query?

I have an oracle stored procedure like this
CREATE OR REPLACE PROCEDURE DEMO (V_IN CHAR, V_OUT VARCHAR2)
IS
BEGIN
FOR ITEM IN LOOP (SELECT DISTINCT (NAME)
FROM TABLE1 INTO V_OUT
WHERE ID = V_IN
LOOP
--CODE TO PRINT V_OUT
END LOOP;
END;
Now how should I create that V_OUT variable so that it can hold all the values coming from query? I'm doing this in oracle12C.
You don't put the INTO clause in the cursor query. And even if you did, you have it in the wrong place in the SQL statement.
You deal with that when you fetch a row from the query:
CREATE OR REPLACE PROCEDURE
DEMO (V_IN CHAR, V_OUT
VARCHAR2)IS
BEGIN
FOR ITEM IN LOOP
(SELECT DISTINCT (NAME)
FROM TABLE1
WHERE ID= V_IN
)
LOOP
dbms_output.put_line(item.name);
v_out := item.name;
END LOOP;
END;
But then the problem is that we just keep overlaying the previous value, so that when your procedure actually exits, the only value of v_out is that last assigned. If you truely need a collection of values, you need to declare your output variable to be a ref cursor, and adjust code accordingly. I've never actually worked with them, so perhaps someone else will chime in.
You can work with collections, like this:
--declare the pakage type
CREATE OR REPLACE PACKAGE PKG_TYPES AS
TYPE LIST_VARCHAR IS TABLE OF VARCHAR2(2000);
END;
--create the proc that will assemble the value list
CREATE OR REPLACE PROCEDURE DEMO ( V_IN IN varchar2, V_OUT IN OUT PKG_TYPES.LIST_VARCHAR) IS
BEGIN
FOR ITEM IN (
SELECT DISTINCT (NAME) name
FROM (SELECT 'X' ID, 'A' name FROM dual
UNION
SELECT 'X' ID, 'b' name FROM dual
UNION
SELECT 'y' ID, 'c' name FROM dual
) TABLE1
WHERE ID= V_IN
)
LOOP
V_OUT.EXTEND;
V_OUT(V_OUT.LAST) := item.name;
--CODE TO PRINT V_OUT
END LOOP;
END;
--use the list. I separated this step but it can be in the demo proc as well
DECLARE
names PKG_TYPES.LIST_VARCHAR := PKG_TYPES.LIST_VARCHAR();
BEGIN
demo('X',names) ;
FOR i IN names.first..names.last LOOP
Dbms_Output.put_line(i);
END LOOP;
END;
You will have to handle exceptions for when no value is returned from the cursor (when no ID is found).
If you need a collection variable - you can use a nested table and bulk collect like below.
To be able to return the value from the procedure you will need to declare the nested table type in some package or on DB schema level.
declare
type test_type is table of varchar2(2000);
test_collection test_type;
begin
select distinct(name) bulk collect into test_collection
from (
select 1 id, 'AAA' name from dual
union all
select 1 id, 'BBB' name from dual
union all
select 1 id, 'AAA' name from dual
union all
select 2 id, 'CCC' name from dual
)
where id = 1;
for i in test_collection.first..test_collection.last loop
dbms_output.put_line(test_collection(i));
end loop;
end;
/
If you just need a string with concatenated values - you can use listagg to create it like below
declare
test_str varchar2(4000);
begin
select listagg(name, ', ') within group(order by 1)
into test_str
from (
select distinct name
from (
select 1 id, 'AAA' name from dual
union all
select 1 id, 'BBB' name from dual
union all
select 1 id, 'AAA' name from dual
union all
select 2 id, 'CCC' name from dual
)
where id = 1
);
dbms_output.put_line(test_str);
end;
/

dbms_sql.number_table in IN statement

I have declared a number table like:
v_areas_hijas dbms_sql.number_table;
I fill the table and then I'm trying this comparation:
select
idarea
from areas
where
IDAREAPADRE in v_areas_hijas;
but I'm getting:
Error(45,22): PLS-00382: expression is of wrong type
How should I do that IN statement?
EDIT 1: using "member of"
select
idarea
from areas
where
IDAREAPADRE member of v_areas_hijas;
I've got this two errors:
Error(45,29): PLS-00382:expression is of wrong type
Error(45,29): PL/SQL: ORA-00932: inconsistent datatypes expected UDT got CHAR
in the table AREAS, IDAREA and IDAREAPADRE are NUMBER,
As in your other question, v_areas_hijas is not a table. If it's a collection type defined in SQL you can use the member of collection operator:
select idarea bulk collect into v_areas_hijas_tmp
from areas
where idareapadre member of v_areas_hijas;
For example, using the predefined type sys.dbms_debug_vc2coll (look inall_coll_types for more examples),
declare
t_demo sys.dbms_debug_vc2coll := sys.dbms_debug_vc2coll('X','Y','Z');
l_result integer;
begin
select count(*) into l_result
from dual
where dummy member of t_demo;
-- Or this:
select count(*) into l_result
from dual
where dummy in
( select column_value from table(t_demo) );
-- Or this:
select count(*) into l_result
from dual d
join table(t_demo) t on t.column_value = d.dummy;
dbms_output.put_line(l_result);
end;
You can use the table() operator on nested table and varray types declared in packages:
create or replace package demo
as
type demo_collection_type is table of varchar2(1);
end demo;
then
declare
t_demo demo.demo_collection_type := demo.demo_collection_type('X','Y','Z');
l_result integer;
begin
select count(*) into l_result
from dual
where dummy in
( select column_value from table(t_demo) t );
dbms_output.put_line(l_result);
-- Or:
select count(*) into l_result
from dual d
join table(t_demo) t on t.column_value = d.dummy;
dbms_output.put_line(l_result);
end;
As of 12.1 you can't use member of for collection types declared in PL/SQL packages.
Some of these restrictions are lifted in 12.2.

Oracle PL/SQL: Returning specific selected columns from procedure

What is the best method for returning specific column values from a Procedure. For example, the below code doesn't work
/*DECLARATION*/
TYPE t_data IS TABLE OF Table1%ROWTYPE;
PROCEDURE get_values(data OUT t_data) AS
BEGIN
SELECT a.object_id, a.num, b.descrip
BULK COLLECT INTO data
FROM Table1 a INNER JOIN
Table2 b ON (a.id = b.id)
WHERE a.date IS NULL;
END get_values;
In the same scenario if I use a SELECT *, it works...
I assume this is an exercise on how to use a procedure to get a list of structured values; I would never recommend such an approach to get data from a table, preferring, if possible, a pure SQL method.
You seem to have tables like these:
create table table1(id ,object_id, num, "date") as (
select 1, 1, 100, sysdate from dual union all
select 2, 2, 200, null from dual
);
create table table2(id, descrip) as (
select 1, 'desc1' from dual union all
select 2, 'desc2' from dual
);
You're trying to create a procedure that return a set of rows, where each row contains elements from both tables; to do so, you need to build a type that matches the result of your select query.
You may want to define your package like this:
CREATE OR REPLACE PACKAGE yourPackage AS
TYPE tRec IS RECORD /* made to match the columns you want to extract in your query */
(
object_id NUMBER,
num NUMBER,
descrip VARCHAR2(100)
);
TYPE tTab IS TABLE OF tRec;
PROCEDURE get_values(data OUT tTab);
END yourPackage;
create or replace package body yourPackage as
PROCEDURE get_values(data OUT tTab) AS
BEGIN
SELECT a.object_id, a.num, b.descrip
BULK COLLECT INTO data
FROM Table1 a INNER JOIN
Table2 b ON (a.id = b.id)
WHERE a."date" IS NULL;
END get_values;
end yourPackage ;
You can call the procedure in the package this way:
declare
someVar yourPackage.tTab;
begin
yourPackage.get_values(someVar);
--
if someVar.first is not null then
for i in someVar.first .. someVar.last loop
dbms_output.put_line(someVar(i).object_id || ' - ' || someVar(i).num || ' - ' || someVar(i).descrip);
end loop;
end if;
end;
and this is the result you get:
2 - 200 - desc2
Firstly you cannot create a Type of any table%rowtype outside a PLSQL block. You need to create table as object and then have to create a type of that object. Then you can use it.
See below:
CREATE OR REPLACE TYPE Table11 AS OBJECT
(
id NUMBER,
num NUMBER,
description VARCHAR2 (20)
)
CREATE OR REPLACE TYPE t_data IS TABLE OF Table11;
CREATE OR REPLACE PROCEDURE get_values (v_data OUT t_data)
AS
BEGIN
SELECT Table11 (a.row_id, 222, 'hello')
BULK COLLECT INTO v_data
FROM Table1 a INNER JOIN Table2 b
ON (a.row_id = b.appid)
WHERE a.date IS NULL;
END get_values;
execution:
DECLARE
v_var t_data;
BEGIN
get_values (v_var);
FOR i IN 1 .. v_var.COUNT
LOOP
DBMS_OUTPUT.put_line (v_var (i).id ||' ' ||v_var(i).num ||' ' || v_var(i).description );
END LOOP;
END;
Output:
SQL> /
1 222 hello
2 222 hello
PL/SQL procedure successfully completed.

PL/SQL: declare the list of numbers to be used in IN condition

Is it possible to define the list of ids that will be used in the cursor select?
I've tried to do it as following
DECLARE
insert_user_id number := 1;
type nt_type is table of number;
building_num nt_type := nt_type (1,2,3,4,5);
cursor curs1 is
(
select ID
from objects
where BUILDING_NUM in (building_num)
);
But what I'm getting is the following error:
PLS-00642: local collection types not allowed in SQL statements
What I found out, is that if I'll declare the list of numbers this way, it will be possible to Loop through them. But i don't want it. All I want is to inside of the IN condition of the cursor.
How can I do it?
I you want to ask me, why I just don't put the ids inside of the IN in cursor? My answer is: I have several cursor that use the same list ids.
EDIT:
According to answers below, the code looks as following:
create type nt_type is table of number;
DECLARE
insert_user_id number := 1;
building_num nt_type := nt_type (1,2,3,4,5);
cursor curs1(building_nums nt_type) is
(
select ID
from objects
where BUILDING_NUM in (select * from table(building_nums))
);
1) Sql allowed to use only sql level collections. You have to create it.
create type nt_type is table of number;
2) And query shoull look like
DECLARE
building_num nt_type := nt_type (1,2,3,4,5);
begin
for rec in (select 1 from dual where 1 member of building_num) loop
null;
end loop;
end ;
DECLARE
building_num nt_type := nt_type (1,2,3,4,5);
begin
for rec in (select 1 from dual where 1 in (select column_value from table(building_num)) loop
null;
end loop;
end ;
Also you can check your database for existing collection of number and use it.select * from ALL_COLL_TYPES where coll_type = 'TABLE' and elem_type_name = 'NUMBER'
The root problem is that SQL query runs in SQL context and have no access to the private PL/SQL type type nt_type is table of number; defined in anonymous PL/SQL block. Instead you have to use SQL type. Below you'll find an example how to pass a list of numbers to a cursor. I'm positive you can adapt the idea to your problem !
create table so56_t (
id number
,d varchar2(1)
);
insert into so56_t values(1, 'A');
insert into so56_t values(2, 'B');
insert into so56_t values(3, 'C');
insert into so56_t values(4, 'D');
-- SQL type required (PL/SQL type won't work)
create type num_list_t is table of number;
/
declare
cursor cur_c(p_ids num_list_t) is
select * from so56_t where id in (select* from table(p_ids));
begin
declare
v_foos constant num_list_t := num_list_t(1, 3);
v_bars constant num_list_t := num_list_t(2, 4);
v_r cur_c%rowtype;
begin
open cur_c(v_foos);
loop
fetch cur_c into v_r;
exit when cur_c%notfound;
dbms_output.put_line(v_r.d);
end loop;
close cur_c;
open cur_c(v_bars);
loop
fetch cur_c into v_r;
exit when cur_c%notfound;
dbms_output.put_line(v_r.d);
end loop;
close cur_c;
end;
end;
/
Example run
SQL> /
A
C
B
D
PL/SQL procedure successfully completed.
SQL>
If you want to use string and not table of number
DECLARE
insert_user_id number := 1;
building_nums varchar2(100) := '1,2,3,4,5';
cursor curs1 is
(
select ID
from objects
where BUILDING_NUM in (
SELECT to_number(REGEXP_SUBSTR (building_nums , '[^,]+', 1, LEVEL))
FROM DUAL
CONNECT BY REGEXP_SUBSTR (building_nums , '[^,]+', 1, LEVEL) IS NOT NULL
)
);
And variation of #Arkadiusz Ɓukasiewicz answer
DECLARE
insert_user_id NUMBER := 1;
-- type nt_type is table of number;
svar VARCHAR2 (100) := '1,2,3,4,5';
building_nums nt_type;
n NUMBER;
CURSOR curs1
IS
(SELECT object_ID
FROM all_objects
WHERE object_id IN (SELECT COLUMN_VALUE
FROM TABLE (building_nums)));
BEGIN
SELECT TO_NUMBER (REGEXP_SUBSTR (svar, '[^,]+', 1, LEVEL))
BULK COLLECT INTO building_nums
FROM DUAL
CONNECT BY REGEXP_SUBSTR (svar, '[^,]+', 1, LEVEL) IS NOT NULL;
OPEN curs1;
LOOP
FETCH curs1 INTO n;
EXIT WHEN curs1%NOTFOUND;
dbms_output.put_line (n);
END LOOP;
CLOSE curs1;
END;
You can use the below query by replacing '1,2,3,4,5' with required list of values or fetch from table.
SELECT REGEXP_SUBSTR(('1,2,3,4,5'),'[^,]+', 1, LEVEL) FROM DUAL CONNECT BY REGEXP_SUBSTR(('1,2,3,4,5'), '[^,]+', 1, LEVEL) IS NOT NULL;
This is similar to what you have already, except that I'm not declaring a cursor. I just wanted to post it in case it's helpful to someone.
As already mentioned, creating the table type (nt_type) first allows it to be used as desired without throwing the error:
PLS-00642: local collection types not allowed in SQL statements
With that said, here is yet another working example...
GIVEN: a table called OBJECTS containing the following data:
ID BUILDING_NUM
---- ------------
1000 1
2000 2
3000 3
4000 4
5000 5
6000 6
7000 7
8000 8
9000 9
... and a type called nt_type
create type nt_type is table of number;
The PLSQL below loops through IDs in the OBJECTS table where BUILDING_NUM matches any in a given set (e.g., 1,2,3,4,5) and outputs some text to screen. Of course it could do something more useful like call a procedure, or execute some SQL.
set serveroutput ON
DECLARE
building_nums nt_type := nt_type (1,2,3,4,5);
BEGIN
for i in (
select * from objects
where BUILDING_NUM in (select column_value from table(building_nums))
) LOOP
dbms_output.put_line('--do something with ID: '||i.ID||', matching building_num: '||i.BUILDING_NUM||';');
END LOOP;
END;
/
Output:
--do something with ID: 1000, matching building_num: 1;
--do something with ID: 2000, matching building_num: 2;
--do something with ID: 3000, matching building_num: 3;
--do something with ID: 4000, matching building_num: 4;
--do something with ID: 5000, matching building_num: 5;
PL/SQL procedure successfully completed.

Dynamically selecting partitions

I have a table with a few hundred partitions and I am generally interested on the latest 35.
Accordingly I am trying to create views which would access these dynamically. i.e. always use the latest in case ones are created.
The query:
select PARTITION_NAME,
PARTITION_POSITION,
NUM_ROWS,
AVG_ROW_LEN
from all_tab_partitions
where
table_name = 'MY_TABLE'
AND PARTITION_NAME <> 'P_LAST'
AND PARTITION_POSITION < (SELECT MAX(PARTITION_POSITION)
FROM all_tab_partitions) - 35
order by 2 DESC
;
Seems to return me the partition names I'm interested, however, I don't manage to use it's results to select the partitions. e.g.:
CREATE OR REPLACE VIEW MY_VIIEW AS
WITH t AS ( [Above query] )
SELECT * FROM
MY_TABLE PARTITION (SELECT /*+ FIRST_ROWS(1) */ PARTITION_NAME
from t);
(not the actual view, just an example)
So how do I do that? How do I create a view which will acess always the latest partition (execpt of "MAX")?
I am using Oracle 10g
thanks
You can do it using PL/SQL only
create or replace package my_table_ is
type t_records is table of my_table%rowtype;
function getpart(c_parts sys_refcursor) return t_records pipelined;
end;
create or replace package body my_table_ is
function getpart(c_parts sys_refcursor) return t_records pipelined is
v_partition all_tab_partitions.partition_name%type;
v_row my_table%rowtype;
c_mytab sys_refcursor;
begin
loop
fetch c_parts into v_partition;
exit when c_parts%notfound;
open c_mytab for 'select * from my_table partition ('||v_partition||')';
loop
fetch c_mytab into v_row;
exit when c_mytab%notfound;
pipe row (v_row);
end loop;
end loop;
end;
end;
Now you can
select * from table(my_table_.getpart(cursor(<QUERY_RETURNING_PARTITION_NAMES>)));
May be you can construct view's query using batch of union all statements with partition name in each statement, e.g.
create view p as
select * from my_table partition (part1)
union all
select * from my_table partition (part1)
...
union all
select * from my_table partition (part35)
Ok... I don't think your can use the Partition-Names, but you can use the Starting-Values of the Partitions to select the Data matching these Partitions...
So you View would look like this:
SELECT * FROM my_table WHERE date_col > get_part_limit( 'my_table', 35 ):
Where date_col is the column you use for partitioning - and get_part_limit is a stored function you write like this:
...
BEGIN
SELECT high_value FROM all_tab_partitions
INTO local_var
WHERE table_name = parameter_name
AND PARTITION_POSITION = MAX... - 35
EXECUTE IMMEDIATE 'SELECT '||local_var||' FROM DUAL' INTO local_return_value;
RETURN local_return_value;
END;
partitions are designed to be transparent for the data, so when you write a query, you simply don't know how your data is stored.
I see only one possibility to hit a particular partition: your WHERE clause should match values to the partitioned columns of latest (or latest 5) partition.
Next question is to build this WHERE clause on the fly. You already know that there is plenty of information in oracle dictionary. So you will read that and create a constructor to convert metadata conditions back into SQL.
irl we do exactly the same thing and use falco's solution like.
Here is my code:
create or replace function longToDate( myOwner varchar2,
mytable_name in varchar2,
mypartition_name in varchar2
) return date
as
cDate date;
cvar varchar2(1024);
rq varchar2(1024);
infiniteValue EXCEPTION;
PRAGMA EXCEPTION_INIT(infiniteValue, -00904);
begin
select high_value into cvar FROM dba_tab_partitions t where t.table_owner=myOwner and table_name=mytable_name and partition_name=mypartition_name;
rq:='select '||cvar||' from dual';
execute immediate rq into cDate;
return cdate;
EXCEPTION
WHEN infiniteValue
then return'01 jan 3000';
when others
then return null;
end longToDate;
Ant the view is something like this
create or replace view last_35 as
with maxdate as
(select longToDate(p.table_owner,p.table_name,p.partition_name) mydate,
rank()over(order by p.partition_position desc) mypos,
p.* from all_tab_partitions p
where p.table_name='MY_TABLE'
)
select /*+full(a)*/* from MY_TABLE a, maxdate
where MY_TABLE.partition_name>maxdate.mydate
and maxdate.mypos=35

Resources