Where column in <function returns a collection > in oracle - sql

I want to achieve something like this:
Example:
Table Customers has columns customer_no, name , age.
some_package package has the following types defined in its spec:
type cust_type is record (custs Customers.customer_no);
type rec_type is table of cust_type index by binary_integer;
function some_function return rec_type;
I am trying to create a view the goes like this:
select ....
from customers c, tablex, tabley
where c.customer_no in some_function() and
... <<other clauses>>
I cannot avoid using some_function() as the logic uses dynamic SQL statements.
I get invalid data type error when i try to compile the view
Is it possible to achieve this in Oracle sql? I don't want to use another function and loops to do this.
Thanks.

No, type rec_type is declared in a package and can be used in PL/SQL blocks but cannot be used in SQL statements.
If you want to use a type in SQL statements then you will need to declare it using a CREATE TYPE statement like this:
CREATE TYPE customers_tab IS TABLE OF NUMBER;
or you can use an existing type like SYS.ODCINUMBERLIST.
Then change the package to:
CREATE OR REPLACE PACKAGE some_package
AS
FUNCTION some_function RETURN SYS.ODCINUMBERLIST;
END;
/
CREATE OR REPLACE PACKAGE BODY some_package
AS
FUNCTION some_function RETURN SYS.ODCINUMBERLIST
AS
t_customers SYS.ODCINUMBERLIST;
BEGIN
SELECT customer_no
BULK COLLECT INTO t_customers
FROM customers
WHERE MOD( customer_no, 3 ) = 0; -- or whatever your query is.
RETURN t_customers;
END;
END;
/
Then you can do:
SELECT *
FROM Customers c
INNER JOIN
TABLE( some_package.some_function() ) t
ON ( c.customer_no = t.COLUMN_VALUE );

Related

Can I create a "table-valued function" in an Oracle package?

I'm relatively new to the Oracle world. Most of my experience is with SQL Server.
I am writing code that would benefit from a "parameterized view", aka a "table-valued function" (tvf) in SQL Server.
I found a good example here that I'm trying to follow: Oracle: Return a «table» from a function
But I need mine to be inside a package, and I'm having a devil of a time with it.
Here's an example of what I'm trying:
CREATE OR REPLACE PACKAGE pkg_test_oracle_tvfs IS
TYPE t_tvf_row IS RECORD(
i NUMBER,
n VARCHAR2(30));
TYPE t_tvf_tbl IS TABLE OF t_tvf_row INDEX BY BINARY_INTEGER;
FUNCTION fn_get_tvf(p_max_num_rows INTEGER) RETURN t_tvf_tbl;
END pkg_test_oracle_tvfs;
CREATE OR REPLACE PACKAGE BODY pkg_test_oracle_tvfs IS
FUNCTION fn_get_tvf(p_max_num_rows INTEGER) RETURN t_tvf_tbl IS
v_tvf_tbl t_tvf_tbl;
BEGIN
SELECT pkg_test_oracle_tvfs.t_tvf_row(rownum,
uo.object_name)
BULK COLLECT
INTO v_tvf_tbl
FROM user_objects uo
WHERE rownum <= p_max_num_rows;
RETURN v_tvf_tbl;
END;
END pkg_test_oracle_tvfs;
With the intent that I can do something like:
SELECT * FROM pkg_test_oracle_tvfs.fn_get_tvf(5);
Or
SELECT * FROM TABLE(pkg_test_oracle_tvfs.fn_get_tvf(5));
(I'm unclear if the TABLE() is required.)
But when I compile the package I get:
Compilation errors for PACKAGE BODY XXX.PKG_TEST_ORACLE_TVFS
Error: PL/SQL: ORA-00913: too many values
Line: 11
Text: FROM user_objects uo
Error: PL/SQL: SQL Statement ignored
Line: 7
Text: SELECT pkg_test_oracle_tvfs.t_tvf_row(rownum,
What am I doing wrong here? Why does this syntax seem to work fine outside of a package but not inside one?
Do I need to use the "pipeline" style of constructing the table as described in Oracle: Pipelined PL/SQL functions If so, why is this example different than the one I've been trying to follow?
Thanks!
Your initial error is because you're selecting into a record type, not an object type, so you don't need the constructor:
SELECT rownum, uo.object_name
BULK COLLECT
INTO v_tvf_tbl
fiddle, which shows it now compiles, but you can't call it from SQL for the reason's MTO already explained.
As an alternative to creating an object type, you can as you suggested use a pipelined function, if you modify the collection type:
CREATE OR REPLACE PACKAGE pkg_test_oracle_tvfs IS
TYPE t_tvf_row IS RECORD(
i NUMBER,
n VARCHAR2(30));
TYPE t_tvf_tbl IS TABLE OF t_tvf_row;
FUNCTION fn_get_tvf(p_max_num_rows INTEGER) RETURN t_tvf_tbl PIPELINED;
END pkg_test_oracle_tvfs;
/
CREATE OR REPLACE PACKAGE BODY pkg_test_oracle_tvfs IS
FUNCTION fn_get_tvf(p_max_num_rows INTEGER) RETURN t_tvf_tbl PIPELINED IS
v_tvf_tbl t_tvf_tbl;
BEGIN
SELECT rownum, uo.object_name
BULK COLLECT
INTO v_tvf_tbl
FROM user_objects uo
WHERE rownum <= p_max_num_rows;
FOR i IN 1..v_tvf_tbl.COUNT LOOP
PIPE ROW (v_tvf_tbl(i));
END LOOP;
RETURN;
END;
END pkg_test_oracle_tvfs;
/
SELECT * FROM pkg_test_oracle_tvfs.fn_get_tvf(5);
I
N
1
PKG_TEST_ORACLE_TVFS
2
PKG_TEST_ORACLE_TVFS
SELECT * FROM TABLE(pkg_test_oracle_tvfs.fn_get_tvf(5));
I
N
1
PKG_TEST_ORACLE_TVFS
2
PKG_TEST_ORACLE_TVFS
fiddle
There is a fundamental flaw; both RECORDs and associative arrays (TABLE OF ... INDEX BY ...) are PL/SQL only data types and cannot be used in SQL statements.
If you want to use a record-like and array-like data structure in an SQL statement then you will need to define it in the SQL scope which means that you cannot define it in a package and would need to use an OBJECT type and a nested-table collection type:
CREATE TYPE t_tvf_row IS OBJECT(
i NUMBER,
n VARCHAR2(30)
);
CREATE TYPE t_tvf_tbl IS TABLE OF t_tvf_row;
Then:
CREATE OR REPLACE PACKAGE pkg_test_oracle_tvfs IS
FUNCTION fn_get_tvf(
p_max_num_rows INTEGER
) RETURN t_tvf_tbl;
END pkg_test_oracle_tvfs;
/
CREATE OR REPLACE PACKAGE BODY pkg_test_oracle_tvfs IS
FUNCTION fn_get_tvf(
p_max_num_rows INTEGER
) RETURN t_tvf_tbl
IS
v_tvf_tbl t_tvf_tbl;
BEGIN
SELECT t_tvf_row(
rownum,
object_name
)
BULK COLLECT INTO v_tvf_tbl
FROM (
SELECT object_name
FROM user_objects
ORDER BY object_name
)
WHERE rownum <= p_max_num_rows;
RETURN v_tvf_tbl;
END;
END pkg_test_oracle_tvfs;
/
fiddle
why is this example different than the one I've been trying to follow?
Because you are defining data-types in a PL/SQL scope (a package) that can only be used in PL/SQL (because records and associative arrays are PL/SQL-only data types) and then trying to use them in an SQL scope (a SELECT statement). The example you are following defines the data types as an OBJECT and a non-associative array and defines them in the SQL scope (outside of a package) and then using them in an SQL statement is allowable.

ORA-01460 unimplemented or unreasonable conversion requested

I am passing ID's in oracle proc. ID's can be in 1000's. currently its able to process around 600 ID's, if I pass more than 600 ID's - I am getting ORA-01460 unimplemented or unreasonable conversion requested. ID is varchar2 datatype, how can I process 1000's of Id's in varchar2 or what will be the best strategy to handle this kind of issue. Any guidance/suggestion will be highly appreciated. Can this be solved using CLOB datatype?
//this is how I am processing Id's
create or replace procedure Emp(
emp_id in varchar2
)
//passing those id's in CTE before passing to subquery
WITH
EMP_LIST AS(
select regexp_substr(emp_id,'[^,]+', 1, level) from dual
connect by level <= LENGTH(regexp_substr(emp_id, '[^,]+'))+1
)
Pass a collection or VARRAY rather than passing a comma-delimited string:
CREATE TYPE number_list IS TABLE OF NUMBER(10,0);
Then you can use it something like:
CREATE PROCEDURE emp(
emp_ids IN number_list
)
IS
BEGIN
-- Do something with the ids like inserting them into a table
INSERT INTO employees ( id )
SELECT COLUMN_VALUE
FROM TABLE( emp_ids );
-- Or something like this:
SELECT something
INTO some_variable -- you need to define this variable first
FROM some_table
WHERE emp_id MEMBER OF emp_ids;
END;
/
Update
If you can't create anything then you can use a built-in collection like SYS.ODCINUMBERLIST:
CREATE PROCEDURE emp(
emp_ids IN SYS.ODCINUMBERLIST
)
IS
BEGIN
-- Do something with the ids like inserting them into a table
INSERT INTO employees ( id )
SELECT COLUMN_VALUE
FROM TABLE( emp_ids );
-- Or something like this:
SELECT something
INTO some_variable -- you need to define this variable first
FROM some_table
WHERE emp_id IN ( SELECT COLUMN_VALUE FROM TABLE( emp_ids ) );
END;
/
(Note: SYS.ODCI*LIST types are VARRAY data types and do not support the MEMBER OF operator like collections do; instead you can get the values from the VARRAY using a nested SELECT statement with a TABLE() collection expression.)
However, if you really can't CREATE anything then you won't be able to CREATE PROCEDURE .... not sure there is any solution to that apart from talking to your DBA.

How to use one sql parameter to represent input array

Is there a way to write sql for Oracle, MS SQL:
Select * from table where id in(:arr)
Select * from table where id in(#arr)
With one param in sql 'arr' to represent an array of items?
I found examples that explode arr to #arr0,.., #arrn and feed array as n+1 separate parameters, not array, like this
Select * from table where id in(:arr0, :arr1, :arr2)
Select * from table where id in(#arr0, #arr1, #arr2)
Not what i want.
These will cause change in sql query and this creates new execution plans based on number of parameter.
I ask for .net, c# and Oracle and MS SQL.
Thanks for constructive ideas!
/ip/
I believe Table Value Parameter is good option for this case. Have a look at a sample code below in SQL Server.
-- Your table
CREATE TABLE SampleTable
(
ID INT
)
INSERT INTO SampleTable VALUES
(1010),
(2010),
(3010),
(4010),
(5010),
(6010),
(7010),
(8030)
GO
-- Create a TABLE type in SQL database which you can fill from front-end code
CREATE TYPE ParameterTableType AS TABLE
(
ParameterID INT
--, some other columns
)
GO
-- Create a stored proc using table type defined above
CREATE PROCEDURE ParameterArrayProcedure
(
#ParameterTable AS ParameterTableType READONLY
)
AS
BEGIN
SELECT
S.*
FROM SampleTable S
INNER JOIN #ParameterTable P ON S.ID = P.ParameterID
END
GO
-- Populated table type variable
DECLARE #ParameterTable AS ParameterTableType
INSERT INTO #ParameterTable (ParameterID) VALUES (1010), (4010), (7010)
EXECUTE ParameterArrayProcedure #ParameterTable
DROP PROCEDURE ParameterArrayProcedure
DROP TYPE ParameterTableType
DROP TABLE SampleTable
GO
Apart from Table Value Parameter, you can also use Json or XML values as SQL parameter but yes, it will definitely change your execution plan accordingly.
In addition to a Table Valued Parameter as Steve mentioned, there are a couple of other techniques available. For example you can parse a delimited string
Example
Declare #arr varchar(50) = '10,20,35'
Select A.*
From YourTable A
Join string_split(#arr,',') B on A.ID=value
Or even
Select A.*
From YourTable A
Where ID in ( select value from string_split(#arr,',') )
Oracle
In other languages (i.e. Java) you can pass an SQL collection as a bind parameter and directly use it in an SQL statement.
However, C# does not support passing SQL collections and only supports passing OracleCollectionType.PLSQLAssociativeArray (documentation link) which is a PL/SQL only data-type and cannot be used (directly) in SQL statements.
To pass an array, you would need to pass a PLSQLAssociativeArray to a PLSQL stored procedure and use that to convert it to an SQL collection that you can use in an SQL statement. An example of a procedure to convert from a PL/SQL associative array to an SQL collection is:
CREATE TYPE IntList AS TABLE OF INTEGER
/
CREATE PACKAGE tools IS
TYPE IntMap IS TABLE OF INTEGER INDEX BY BINARY_INTEGER;
FUNCTION IntMapToList(
i_map IntMap
) RETURN IntList;
END;
/
CREATE PACKAGE BODY tools IS
FUNCTION IntMapToList(
i_map IntMap
) RETURN IntList
IS
o_list IntList := IntList();
i BINARY_INTEGER;
BEGIN
IF i_map IS NOT NULL THEN
i := o_list.FIRST;
WHILE i IS NOT NULL LOOP
o_list.EXTEND;
o_list( o_list.COUNT ) := i_map( i );
i := i_map.NEXT( i );
END LOOP;
END IF;
RETURN o_list;
END;
END;
/

Oracle SQL Create function or procedure returning a table

In SQL Server, I can just use 'RETURNS TABLE' and it will do the job. But I can't find how to do the same in Oracle SQL
I have the following SELECT statement that needs to be put in a function or procedure:
SELECT a.CodAcord, a.Descr
FROM FreqSoce f
LEFT JOIN Acord a ON a.CodAcord = f.CodAcord
WHERE f.codSoce = codSoce;
codSoce is an INTEGER IN parameter, and I need to return a.CodAcord and a.Descr as result from my function/procedure.
Is there a simple way to do this? Without having to deal with temp variables and/or advanced content...
EDIT: Aditional info:
- I need to return a.CodAcord and a.Descr, but when I did some research to know how to return more than one variable using SQL Functions or Procedures, all I could find was that this was only possible by returning a TABLE. If there's a way to return more than one item from a Function or Procedure, please let me know.
- The use of Functions or Procedures is strictly required.
- I'm using SQL Developer.
You can achieve a table as a return value from function by using a pipelined table function. Please see the example below:
-- Create synthetic case
CREATE TABLE Acord AS
SELECT rownum CodAcord, 'Description ' || rownum Descr
FROM dual CONNECT BY LEVEL <= 5;
CREATE TABLE FreqSoce AS
SELECT rownum CodSoce, rownum CodAcord
FROM dual CONNECT BY LEVEL <= 10;
-- Test dataset
SELECT a.CodAcord, a.Descr
FROM FreqSoce f
LEFT JOIN Acord a ON a.CodAcord = f.CodAcord
WHERE f.CodSoce = 10;
-- Here begins actual code
-- Create an object type to hold each table row
CREATE OR REPLACE TYPE typ_acord AS OBJECT(
CodAcord NUMBER,
Descr VARCHAR2(40)
);
/
-- Create a collection type to hold all result set rows
CREATE OR REPLACE TYPE tab_acord AS TABLE OF typ_acord;
/
-- Our function that returns a table
CREATE OR REPLACE FUNCTION getAcord(pCodSoce IN NUMBER)
RETURN tab_acord PIPELINED
AS
BEGIN
FOR x IN (SELECT a.CodAcord, a.Descr
FROM FreqSoce f
LEFT JOIN Acord a ON a.CodAcord = f.CodAcord
WHERE f.CodSoce = pCodSoce)
LOOP
PIPE ROW (typ_acord(x.CodAcord, x.Descr));
END LOOP;
END;
/
-- Testing the function (please note the TABLE operator)
SELECT * FROM TABLE(getAcord(5));
Take the following as a code template:
CREATE OR REPLACE PACKAGE tacord AS
TYPE ttabAcord IS TABLE OF ACord%ROWTYPE;
END tacord;
/
show err
CREATE OR REPLACE PACKAGE BODY tacord AS
BEGIN
NULL;
END tacord;
/
show err
CREATE OR REPLACE FUNCTION demo RETURN tacord.ttabAcord AS
to_return tacord.ttabAcord;
BEGIN
SELECT a.*
BULK COLLECT INTO to_return
FROM FreqSoce f
LEFT JOIN Acord a ON a.CodAcord = f.CodAcord
WHERE f.codSoce = codSoce
;
RETURN to_return;
END demo;
/
show err
Key points:
%ROWTYPE represents the datatype of a database table's record
BULK COLLECT INTO inserts a complete result set into a plsql data structure
Iteration over the table contents is availabel by the plsql collection methods, namely .FIRST, .LAST, .NEXT.

How to use in statement with nested table

Hey there I have a function, and part of the function is to make sure that the selected value is within the passed in table of varchar2s. To start I declare a varchar2 table type like so.
create or replace type Varchar2Table is table of varchar2(200)
Then I have the function which accepts the nested table parameter and has a select statement on them.
function SelectPeople(inputNames Varchar2Table) return People
begin
--stuff
select * from person_table where name in inputNames; --line of interest
--more stuff
end;
This doesn't seem to work though, I get the following error:
ORA-00932: inconsistent datatypes: expected NUMBER got
ENGSPL5.VARCHAR2TABLE
Any suggestions?
The TABLE operator allows nested tables to be used in SQL statements. The function was also missing an IS and an INTO.
create or replace type Varchar2Table is table of varchar2(200);
create table person_table(id number, name varchar2(100));
create or replace function SelectPeople(inputNames Varchar2Table) return number
is --Missing "IS".
type numberTable is table of number; --Need a collection to store results.
numbers numberTable;
begin
select id
bulk collect into numbers --Missing "INTO".
from person_table
where name in (select column_value from table(inputNames)); --Missing "TABLE".
--Alternatively a multiset condition can be used.
--where name member of inputNames;
--Dummy return value to make the function compile.
return 1;
end;
/