Convert a piece of SQL into an Oracle function - sql

My table "COMMA_SEPERATED" looks something like this
ID NAME CITY
--- ---- -----------------------------
1 RAJ CHENNAI, HYDERABAD, JABALPUR
2 SAM BHOPAL,PUNE
I want to separate each City as a new record so my SQL is: (working fine)
SELECT id, TRIM(CITY_NEW)
FROM COMMA_SEPERATED, xmltable
(
'if (contains($X,",")) then ora:tokenize($X,"\,") else $X'
passing city AS X
columns CITY_NEW varchar2(4000) path '.'
);
I want to convert this piece of SQL into a function so that i can simply call the function like this
SELECT id, split_function(city) FROM COMMA_SEPERATED
Output:
1 CHENNAI
1 HYDERABAD
1 JABALPUR
2 BHOPAL
2 PUNE
Can anyone help on how to do that? I am very new to PL/SQL.

The query you're trying to get to:
SELECT id, split_function(city) FROM COMMA_SEPERATED
won't work, because you're trying to return multiple rows for each source row. You have to make it a bit more complicated than that unfortunately.
If the goal is to hide the splitting mechanism then the closest I can think of is to create a function which returns a collection of strings, which could be pipelined:
create or replace function split_function (p_string varchar2)
return sys.odcivarchar2list pipelined as
begin
for r in (
select result
from xmltable (
'if (contains($X,",")) then ora:tokenize($X,"\,") else $X'
passing p_string as x
columns result varchar2(4000) path '.'
)
)
loop
pipe row (trim(r.result));
end loop;
end split_function;
/
Your proposed call would then give you one row per ID with a collection:
select id, split_function(city) from comma_seperated;
ID SPLIT_FUNCTION(CITY)
---------- -----------------------------------------------------------------
1 ODCIVARCHAR2LIST('CHENNAI', 'HYDERABAD', 'JABALPUR')
2 ODCIVARCHAR2LIST('BHOPAL', 'PUNE')
which isn't quite what you want; but you can use a table collection expression and cross-join to convert into multiple rows instead:
select cs.id, t.column_value as city
from comma_seperated cs
cross join table(split_function(cs.city)) t;
ID CITY
---------- ------------------------------
1 CHENNAI
1 HYDERABAD
1 JABALPUR
2 BHOPAL
2 PUNE
db<>fiddle demo.
That isn't as simple as you hoped for, but is arguably still better than cross-joining to the xmltable(), particularly if you want to reuse that splitting logic/function in multiple places, as well as hide the details of how the split is done - which would let you change the mechanism easily if you wanted to, e.g. to use a more common regular expression to do the splitting.

Apart what #Alex showed, you can also create an object and get the object returned via function. See below:
--Created an object to hold your result columns
create or replace type Obj IS OBJECT (id number, city varchar2(20));
/
--Table of object
create or replace type var_obj is table of Obj;
/
--Function with return as with Object type.
create or replace function splt_fnct
return var_obj
as
var var_obj:=var_obj();
begin
Select obj(col,col1)
bulk collect into var
from (
Select distinct col , regexp_substr(col1,'[^,]+',1,level) col1
from tbl
connect by regexp_substr(col1,'[^,]+',1,level) is not null
order by 1);
return var;
end;
/
--Selecting result
select * from table(splt_fnct);
Edit: I was trying with #Alex solution and got some error as shown below:
create or replace function splt_fnct(input_strng varchar2)
return var_obj
as
var var_obj:=var_obj();
begin
Select obj(col,col1)
bulk collect into var
from (
select tbl.col, t.rslt --<--This column name should the same as used in colmns clause in the below query. Its giving error "invalid column". How to handle this case.
FROM tbl, xmltable
(
'if (contains($X,",")) then ora:tokenize($X,"\,") else $X'
passing col1 AS X
columns input_strng varchar2(4000) path '.'
) t
);
return var;
end;
/
Correction as per #Alex suggestion:
create or replace function splt_fnct(input_strng varchar2)
return var_obj
as
var var_obj:=var_obj();
begin
select obj(tbl.col, t.rslt)
bulk collect into var
FROM tbl, xmltable
(
'if (contains($X,",")) then ora:tokenize($X,"\,") else $X'
passing input_strng AS X
columns rslt varchar2(4000) path '.'
) t;
return var;
end;
/

Related

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.

How return dynamic number of columns in function?

In PostgreSQL 11 database I have table with 6 column. Next function return static number of defined columns.
CREATE FUNCTION CALCULATION(INTEGER)
RETURNS TABLE(
ORGANIZATION_ID INT4,
ORGANIZATION_NAME VARCHAR,
ORGANIZATION_RANG INT4,
PARENT_ORGANIZATION_ID INT4,
PARENT_ORGANIZATION_NAME VARCHAR,
PARENT_ORGANIZATION_RANG INT4
) AS $$
SELECT * FROM ANALYTICS;
$$ LANGUAGE SQL;
How can I make an SQL function in Postgres 11 which return a result set with dynamic number of columns according to a parameter passed in?
For example if I call SELECT * FROM CALCULATION(2);, function return first 2 columns.
If this is not possible with an SQL function, is it possible with a PL/pgSQL function?
This is possible for RECORD returning functions.
CREATE FUNCTION calculation(how_many integer) RETURNS SETOF RECORD
LANGUAGE plpgsql
AS $fff$
BEGIN
IF how_many = 1
THEN RETURN QUERY SELECT 'foo'::text;
ELSIF how_many = 2
THEN RETURN QUERY SELECT 'foo'::text, 'bar'::text;
END IF;
END;
$fff$
;
And now you can do:
jbet=> SELECT * FROM calculation(1) AS f(first_col text);
first_col
-----------
foo
(1 row)
jbet=> SELECT * FROM calculation(2) AS f(first_col text, second_col text);
first_col | second_col
-----------+------------
foo | bar
(1 row)
The very serious downside is that each time you call the function you have to define set of returned columns, so I don't think you'll find this answer useful : )
Anyway, Postgresql needs to know returned type of each SELECT before it runs the query, so one or other way you have to define the columns.
JSON return value could be a reasonable answer if you just want the data and don't care if there are separate columns or not.
Backing up a step, why not use a standard select to get the columns you want from your set-returning function?
select organization_name,
organization_rang,
parent_organization_name,
parent_organization_rang
from calculation();
That's easy to follow and flexible. I'm guessing that you've written a simplified example and have a good reason for what you're asking...but I figured I'd double-check.

Can we use LIKE operator along with MEMBER OF operator in a stored procedure?

I have an array of data using which I select rows from a table. For that I use member of operator in where clause. I want to know if we can do that same but by using Like operator along with member of operator.
When my Array consists of{Delhi, Mumbai, Kolkata}
I select the rows which have these three values in their row.
This is how I do that:
select ...
Into...
From xyz where city member of array;
///Receiving the array from an in parameter of the stored procedure.
And it works perfectly fine.
But If my array has {Del, Mum, Kolk} //parts of the actual names
How do I use this array for the same purpose, maybe using Like operator.
Create or replace zz2(ar in array_collection, c out sys_refcursor)
Is
anotherabc tablename.city%type
Begin
Open c
For
Select ABC
Into anotherabc
From tablename where city member of ar;
End zz2;
I expect the output to have all the rows which have cities starting with the alphabet/characters present in the array. Using member of operator
Something like this?
Select ABC
Into anotherabc a
From tablename WHERE EXISTS
( select 1 FROM ( select column_value as city
FROM TABLE(ar) ) s where a.city like s.city||'%' )
There is no direct way to use LIKE with MEMBER OF.
If It is the protocol that your collection contains the first three characters of the city name then you can use substr() to match only the first three characters in MEMBER OF.
try the following thing:
DECLARE
TYPE t_tab IS TABLE OF varchar(3);
l_tab1 t_tab := t_tab('Del','Mom','Kol');
BEGIN
DBMS_OUTPUT.put('Is ''Delhi'' MEMBER OF l_tab1? ');
IF SUBSTR('Delhi',1,3) MEMBER OF l_tab1 THEN -- note the use of SUBSTR here
DBMS_OUTPUT.put_line('TRUE');
ELSE
DBMS_OUTPUT.put_line('FALSE');
END IF;
END;
/
db<>fiddle demo
Cheers!!

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.)

Oracle and SQLServer function evaluation in queries

Let's say I have a function call on a select or where clause in Oracle like this:
select a, b, c, dbms_crypto.hash(utl_raw.cast_to_raw('HELLO'),3)
from my_table
A similar example can be constructed for MS SQLServer.
What's the expected behavior in each case?
Is the HASH function going to be called once for each row in the table, or DBMS will be smart enough to call the function just once, since it's a function with constant parameters and no side-effects?
Thanks a lot.
The answer for Oracle is it depends. The function will be called for every row selected UNLESS the Function is marked 'Deterministic' in which case it will only be called once.
CREATE OR REPLACE PACKAGE TestCallCount AS
FUNCTION StringLen(SrcStr VARCHAR) RETURN INTEGER;
FUNCTION StringLen2(SrcStr VARCHAR) RETURN INTEGER DETERMINISTIC;
FUNCTION GetCallCount RETURN INTEGER;
FUNCTION GetCallCount2 RETURN INTEGER;
END TestCallCount;
CREATE OR REPLACE PACKAGE BODY TestCallCount AS
TotalFunctionCalls INTEGER := 0;
TotalFunctionCalls2 INTEGER := 0;
FUNCTION StringLen(SrcStr VARCHAR) RETURN INTEGER AS
BEGIN
TotalFunctionCalls := TotalFunctionCalls + 1;
RETURN Length(SrcStr);
END;
FUNCTION GetCallCount RETURN INTEGER AS
BEGIN
RETURN TotalFunctionCalls;
END;
FUNCTION StringLen2(SrcStr VARCHAR) RETURN INTEGER DETERMINISTIC AS
BEGIN
TotalFunctionCalls2 := TotalFunctionCalls2 + 1;
RETURN Length(SrcStr);
END;
FUNCTION GetCallCount2 RETURN INTEGER AS
BEGIN
RETURN TotalFunctionCalls2;
END;
END TestCallCount;
SELECT a,TestCallCount.StringLen('foo') FROM(
SELECT 0 as a FROM dual
UNION
SELECT 1 as a FROM dual
UNION
SELECT 2 as a FROM dual
);
SELECT TestCallCount.GetCallCount() AS TotalFunctionCalls FROM dual;
Output:
A TESTCALLCOUNT.STRINGLEN('FOO')
---------------------- ------------------------------
0 3
1 3
2 3
3 rows selected
TOTALFUNCTIONCALLS
----------------------
3
1 rows selected
So the StringLen() function was called three times in the first case. Now when executing with StringLen2() which is denoted deterministic:
SELECT a,TestCallCount.StringLen2('foo') from(
select 0 as a from dual
union
select 1 as a from dual
union
select 2 as a from dual
);
SELECT TestCallCount.GetCallCount2() AS TotalFunctionCalls FROM dual;
Results:
A TESTCALLCOUNT.STRINGLEN2('FOO')
---------------------- -------------------------------
0 3
1 3
2 3
3 rows selected
TOTALFUNCTIONCALLS
----------------------
1
1 rows selected
So the StringLen2() function was only called once since it was marked deterministic.
For a function not marked deterministic, you can get around this by modifying your query as such:
select a, b, c, hashed
from my_table
cross join (
select dbms_crypto.hash(utl_raw.cast_to_raw('HELLO'),3) as hashed from dual
);
For SQL server, it will be evaluated for every single row.
You will be MUCH better off by running the function once and assigning to a variable and using the variable in the query.
short answer....it depends.
If the function is accessing data ORACLE does not know if it is going to be the same for each row, therefore, it needs to query for each. If, for example, your function is just a formatter that always returns the same value then you can turn on caching (marking it as Deterministic) which may allow for you to only do the function call once.
Something you may want to look into is ORACLE WITH subquery:
The WITH query_name clause lets you
assign a name to a subquery block. You
can then reference the subquery block
multiple places in the query by
specifying the query name. Oracle
optimizes the query by treating the
query name as either an inline view or
as a temporary table
I got the quoted text from here, which has plenty of examples.