PL/SQL Add Multiple Columns to Query with Single Function - sql

Is there any way to accomplish something like this in PL/SQL...
select a.col1, a.col2, a.col3, myFunc(a.id) from myTable a;
and the result be more than 4 columns? So basically, is there a way for a function to return or pipe more than one column back? The number needed is known and set in stone, it's 3. So this query would return 6 columns. I know I could call the myFunc() 3 separate times but the amount of processing would be tripled.
I've been playing around with pipeline functions but it doesn't appear they can be used to do this.
Oracle Database 11g Enterprise Edition Release 11.2.0.2.0 - 64bit
Production PL/SQL Release 11.2.0.2.0 - Production
Thanks!

Here are ways to do it in various Oracle versions. I use DBA_OBJECTS only as a substitute for your real table.
CREATE OR REPLACE TYPE my_func_rec IS OBJECT
(
mf_col1 NUMBER,
mf_col2 NUMBER,
mf_col3 NUMBER
);
CREATE OR REPLACE TYPE my_func_tab IS TABLE OF my_func_rec;
CREATE OR REPLACE FUNCTION my_func (id NUMBER)
RETURN my_func_tab IS
l_result my_func_tab;
BEGIN
SELECT my_func_rec (id + 100, id + 101, id + 102)
BULK COLLECT INTO l_result
FROM DUAL;
RETURN l_result;
END my_func;
12c
In 12c, it's pretty simple using CROSS APPLY.
SELECT object_id,
object_type,
status,
mf_col1,
mf_col2,
mf_col3
FROM dba_objects o
CROSS APPLY (SELECT mf_col1,
mf_col2,
mf_col3
FROM TABLE (my_func (o.object_id)) odet);
11g
In 11g, you do not have access to CROSS APPLY so you need to select the function results as an object and then TREAT it as an object to get access to the individual fields.
SELECT object_id,
object_type,
status,
TREAT (val AS my_func_rec).mf_col1,
TREAT (val AS my_func_rec).mf_col2,
TREAT (val AS my_func_rec).mf_col3
FROM (SELECT object_id,
object_type,
status,
(SELECT my_func_rec (mf_col1, mf_col2, mf_col3)
FROM TABLE (my_func (o.object_id)) mf)
val
FROM dba_objects o)
NOTE: I created the 11g answer after the 12c answer. The 11g answer can be further simplified by having my_func return a my_func_rec instead of a my_func_tab. In this case, it would simplify to:
SELECT object_id,
object_type,
status,
TREAT (val AS my_func_rec).mf_col1,
TREAT (val AS my_func_rec).mf_col2,
TREAT (val AS my_func_rec).mf_col3
FROM (SELECT object_id,
object_type,
status,
my_func (o.object_id) val
FROM dba_objects o)

A very simplied illustration. Hope it helps.
--The best way here is to make a table type function
--as mentioned below
--Create object type
CREATE OR REPLACE TYPE OBJ_DUM
IS
OBJECT
(
COL1 NUMBER,
COL2 NUMBER,
COL3 VARCHAR2(100) );
/
--Create table type
CREATE OR REPLACE TYPE TAB_DUM
IS
TABLE OF OBJ_DUM;
/
--Create dummy function. This canbe a pipelined function also
CREATE OR REPLACE
FUNCTION SO_DUM
RETURN TAB_DUM
AS
lv_tab tab_dum:=tab_dum(NULL,NULL,NULL);
BEGIN
SELECT obj_dum(level,level+1,'AVRAJIT+'
||LEVEL) BULK COLLECT
INTO lv_tab
FROM DUAL
CONNECT BY LEVEL < 2;
RETURN lv_tab;
END;
/
--To check the output
SELECT TAB.*,OBJ.* FROM USER_OBJECTS OBJ,TABLE(SO_DUM) TAB;

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.

Oracle - Derived table, Inserting values into a new table

I have a function that will read through a list of comma-separated values within a field. The result of this function will create a derived table.
Example:
derived Table:
Now, I need to get this data inserted into a new/existing table. I created and object type/package that has all the columns from the derived table, however, I'm not sure how to add/reference this data to be able to insert into the new/existing table.
Functions that return a nested table can be converted into regular rows and columns by selecting from the table function using the TABLE function. (And in recent versions of Oracle, the TABLE operator is optional.)
For example:
SQL> create or replace function get_columns return sys.odcivarchar2list is
2 begin
3 return sys.odcivarchar2list('355352', 'Yes');
4 end;
5 /
Function created.
SQL> select column_value from table(get_columns);
COLUMN_VALUE
--------------------------------------------------------------------------------
355352
Yes
Now the function can be used anywhere, like an insert: insert into some_table select column_value from table(get_columns);
Your specific solution may be more complicated if the results contain multiple columns, or you have multiple result sets, etc. Edit your question with more details about the function and types if you still need help.
Edit: Below is a full example of storing CSV values in a column, creating a function to convert CSV to a VARRAY, calling that function to pivot and then pivot again back into multiple columns:
create table table_1
(
request_id number,
service varchar2(100),
method varchar2(100),
request_date date,
process_date date,
te_suid number,
status varchar2(100),
params varchar2(4000)
);
create or replace type params_varray is varray(48) of varchar2(4000);
--Example function that converts comma-separated lists into VARRAYS.
--(Not tested for nulls, consecutive commas, etc.)
create or replace function get_columns(p_params varchar2, p_delimiter varchar2) return params_varray is
v_array params_varray := params_varray();
begin
for i in 1 .. regexp_count(p_params, p_delimiter) + 1 loop
v_array.extend;
v_array(v_array.count) := regexp_substr(p_params, '[^'||p_delimiter||']+', 1, i);
end loop;
return v_array;
end;
/
insert into table_1 values(1,1,1,sysdate,sysdate,1,1,'1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48');
insert into table_1 values(2,2,2,sysdate,sysdate,2,2,'49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96');
--Convert CSV into multiple columns.
--
--Convert rows back into multiple columns.
select request_id, service, method, request_date, process_date, te_suid, status
,max(case when param_index = 1 then param_value else null end) value1
,max(case when param_index = 2 then param_value else null end) value2
,max(case when param_index = 3 then param_value else null end) value3
--Repeat for 4 through 48.
from
(
--Convert CSV to rows.
select request_id, service, method, request_date, process_date, te_suid, status,
row_number() over (partition by table_1.rowid order by rownum) param_index,
column_value param_value
from table_1
cross join get_columns(table_1.params, ',')
)
group by request_id, service, method, request_date, process_date, te_suid, status;
You can run the example on db<>fiddle.
But if you have any choice in the matter, I strongly suggest you denormalize your table and never store delimited values in a database.

If the first field doesn't exists in a table then look at a different field in the same table

Is there a way to select a field from a table and if that field doesn't exist then select a different field from the same table? example:
SELECT MY_FIELD from MY_TABLE
error: "MY_FIELD": invalid identifier
is there any way to check if it exists and if it does then use that field for the query, if it doesn't exist then use example:
SELECT my_field2 from client.
My problem is
I am writing a report that will be used on two databases, but the field names on occasion can be named slightly different depending on the database.
What you really need to do is talk to your management / development leads about why the different databases are not harmonized. But, since this is a programming site, here is a programming answer using dynamic SQL.
As has been pointed out, you could create views in the different databases to provide yourself with a harmonized layer to query from. If you are unable to create views, you can do something like this:
create table test ( present_column NUMBER );
insert into test select rownum * 10 from dual connect by rownum <= 5;
declare
l_rc SYS_REFCURSOR;
begin
BEGIN
OPEN l_rc FOR 'SELECT missing_column FROM test';
EXCEPTION
WHEN others THEN
OPEN l_rc FOR 'SELECT present_column FROM test';
END;
-- This next only works in 12c and later
-- In earlier versions, you've got to process l_rc on your own.
DBMS_SQL.RETURN_RESULT(l_rc);
end;
This is inferior to the other solutions (either harmonizing the databases or creating views). For one thing, you get no compile time checking of your queries this way.
That won't compile, so - I'd say not. You might try with dynamic SQL which reads contents of the USER_TAB_COLUMNS and create SELECT statement on-the-fly.
Depending on reporting tool you use, that might (or might not) be possible. For example, Apex offers (as reports's source) a function that returns query, so you might use it there.
I'd suggest a simpler option - create views on both databases which have unified column names, so that your report always selects from the view and works all the time. For example:
-- database 1:
create view v_client as
select client_id id,
client_name name
from your_table;
-- database 2:
create view v_client as
select clid id,
clnam name
from your_table;
-- reporting tool:
select id, name
from v_client;
This can be done in a single SQL statement using DBMS_XMLGEN.GETXML, but it gets messy. It would probably be cleaner to use dynamic SQL or a view, but there are times when it's difficult to create supporting objects.
Sample table:
--Create either table.
create table my_table(my_field1 number);
insert into my_table values(1);
insert into my_table values(2);
create table my_table(my_field2 number);
insert into my_table values(1);
insert into my_table values(2);
Query:
--Get the results by converting XML into rows.
select my_field
from
(
--Convert to an XMLType.
select xmltype(clob_results) xml_results
from
(
--Conditionally select either MY_FIELD1 or MY_FIELD2, depending on which exists.
select dbms_xmlgen.GetXML('select my_field1 my_field from my_table') clob_results
from user_tab_columns
where table_name = 'MY_TABLE'
and column_name = 'MY_FIELD1'
--Stop transformations from running the XMLType conversion on nulls.
and rownum >= 1
union all
select dbms_xmlgen.GetXML('select my_field2 my_field from my_table') clob_results
from user_tab_columns
where table_name = 'MY_TABLE'
and column_name = 'MY_FIELD2'
--Stop transformations from running the XMLType conversion on nulls.
and rownum >= 1
)
--Only convert non-null values.
where clob_results is not null
)
cross join
xmltable
(
'/ROWSET/ROW'
passing xml_results
columns
my_field number path 'MY_FIELD'
);
Results:
MY_FIELD
--------
1
2
Here's a SQL Fiddle if you want to see it running.

Oracle function with select all from tables

SELECT DISTINCT L.* FROM LABALES L , MATCHES M
WHERE M.LIST LIKE '%ENG'
ORDER BY L.ID
I need to create function with this select, I tried this but it doesn't work.
CREATE OR REPLACE FUNCTION getSoccerLists
RETURN varchar2 IS
list varchar2(2000);
BEGIN
SELECT DISTINCT L.* FROM LABALES L , MATCHES M
WHERE M.LIST LIKE '%ENG'
ORDER BY L.ID
return list;
END;
How will I create function that returns all from table L.
Thanks
You may use implicit result using DBMS_SQL.RETURN_RESULT(Oracle12c and above) in a procedure using a cursor to your query.
CREATE OR REPLACE PROCEDURE getSoccerLists
AS
x SYS_REFCURSOR;
BEGIN
OPEN x FOR SELECT DISTINCT L.* FROM LABALES L
JOIN MATCHES M ON ( 1=1 ) -- join condition
WHERE M.LIST LIKE '%ENG'
ORDER BY L.ID;
DBMS_SQL.RETURN_RESULT(x);
END;
/
then simply call the procedure
EXEC getSoccerLists;
For lower versions(Oracle 11g) , you may use a print command to display the cursor's o/p passing ref cursor as out parameter.
CREATE OR REPLACE PROCEDURE getSoccerLists (x OUT SYS_REFCURSOR)
AS
BEGIN
OPEN x FOR SELECT DISTINCT L.* FROM LABALES L
JOIN MATCHES M ON ( 1=1 ) -- join condition
WHERE M.LIST LIKE '%ENG'
ORDER BY L.ID;
END;
/
Then, in SQL* Plus or running as script in SQL developer and Toad, you may get the results using this.
VARIABLE r REFCURSOR;
EXEC getSoccerLists (:r);
PRINT r;
Another option is to use TABLE function by defining a collection of the record type of the result within a package.
Refer Create an Oracle function that returns a table
I guess this questions is a repetition of the your previously asked question, where you wanted to get all the columns of tables but into separate column. I already answered in stating this you cannot do if you call your function via a SELECT statement. If you call your function in a Anoymous block you can display it in separate columns.
Here Oracle function returning all columns from tables
Alternatively, you can get the results separated by a comma(,) or pipe (|) as below:
CREATE OR REPLACE
FUNCTION getSoccerLists
RETURN VARCHAR2
IS
list VARCHAR2(2000);
BEGIN
SELECT col1
||','
||col2
||','
||col2
INTO LIST
FROM SOCCER_PREMATCH_LISTS L ,
SOCCER_PREMATCH_MATCHES M
WHERE M.LIST LIKE '%' || (L.SUB_LIST) || '%'
AND (TO_TIMESTAMP((M.M_DATE || ' ' || M.M_TIME), 'DD.MM.YYYY HH24:MI') >
(SELECT SYSTIMESTAMP AT TIME ZONE 'CET' FROM DUAL
))
ORDER BY L.ID");
Return list;
End;
Note here if the column size increased 2000 chars then again you will lose the data.
Edit:
From your comments
I want it to return a table set of results.
You then need to create a table of varchar and then return it from the function. See below:
CREATE TYPE var IS TABLE OF VARCHAR2(2000);
/
CREATE OR REPLACE
FUNCTION getSoccerLists
RETURN var
IS
--Initialization
list VAR :=var();
BEGIN
SELECT NSO ||',' ||NAME BULK COLLECT INTO LIST FROM TEST;
RETURN list;
END;
Execution:
select * from table(getSoccerLists);
Note: Here in the function i have used a table called test and its column. You replace your table with its columnname.
Edit 2:
--Create a object with columns same as your select statement
CREATE TYPE v_var IS OBJECT
(
col1 NUMBER,
col2 VARCHAR2(10)
)
/
--Create a table of your object
CREATE OR REPLACE TYPE var IS TABLE OF v_var;
/
CREATE OR REPLACE FUNCTION getSoccerLists
RETURN var
IS
--Initialization
list VAR :=var();
BEGIN
--You above object should have same columns with same data type as you are selecting here
SELECT v_var( NSO ,NAME) BULK COLLECT INTO LIST FROM TEST;
RETURN list;
END;
Execution:
select * from table(getSoccerLists);
This is not an answer on how to build a function for this, as I'd recommend to make this a view instead:
CREATE OR REPLACE VIEW view_soccer_list AS
SELECT *
FROM soccer_prematch_lists l
WHERE EXISTS
(
SELECT *
FROM soccer_prematch_matches m
WHERE m.list LIKE '%' || (l.sub_list) || '%'
AND TO_TIMESTAMP((m.m_date || ' ' || m.m_time), 'DD.MM.YYYY HH24:MI') >
(SELECT SYSTIMESTAMP AT TIME ZONE 'CET' FROM DUAL)
);
Then call it in a query:
SELECT * FROM view_soccer_list ORDER BY id;
(It makes no sense to put an ORDER BY clause in a view, because you access the view like a table, and table data is considered unordered, so you could not rely on that order. The same is true for a pipelined function youd access with FROM TABLE (getSoccerLists). Always put the ORDER BY clause in your final queries instead.)

local collection types not allowed in SQL statements

I have a question regarding using of collections in Oracle SQL functions.
There are definitions of types in package:
/* Types of package*/
create or replace PACKAGE "test" AS
TYPE type_record_1 IS record ( id_num NUMBER , timestamp_num NUMBER,value NUMBER);
TYPE type_table_1 IS TABLE OF type_record_1;
TYPE type_record_2 IS record ( id_num NUMBER , timestamp_num NUMBER,pValue NUMBER);
TYPE type_table_2 IS TABLE OF type_record_2;
END test;
Problem is in functions_2.
function_2 uses output from function_1.
The error message occurs when I try select in function_2.
Error message "local collection types not allowed in SQL statements".
Could you please help? What is wrong with using of collections in functions?
/*function 1*/
FUNCTION function_1
RETURN type_table_1
IS
table_1 type_table_1;
BEGIN
-- select values from
SELECT id_num, timestamp_num, value --type_record_1 (id_num, timestamp_num, value)
BULK COLLECT INTO table_1
FROM (
SELECT
l.id_num,
EXTRACT(hour from end_time) * 60 + EXTRACT(minute from end_time) as timestamp_num,
l.value
FROM INTERVAL_F l
WHERE id_num IN (SELECT id_num FROM table_rev)
);
RETURN table_1;
END function_1;
/*function 2*/
FUNCTION function_2
(
table_1 IN type_table_1
)
RETURN type_table_2
IS
table_2 type_table_2;
BEGIN
SELECT type_record_2(id_num , timestamp_num , pValue)
BULK COLLECT INTO table_2 FROM (
SELECT id_num
, timestamp_num
, value as pValue
FROM table(table_1) -- ERROR IS HERE
);
RETURN table_2;
END function_2;
To achive that you should use something like:
CREATE OR REPLACE TYPE type_record_1...
/
CREATE OR REPLACE TYPE type_table_1 AS TABLE OF type_record_1;
/
Oracle does not allow types declared in package to be casted as table.
I talk about Oracle until 11, still not check 12c new features :(.