Oracle - Derived table, Inserting values into a new table - sql

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.

Related

Oracle SQL/PLSQL: change type of specific columns in one time

Assume following table named t1:
create table t1(
clid number,
A number,
B number,
C number
)
insert into t1 values(1, 1, 1, 1);
insert into t1 values(2, 0, 1, 0);
insert into t1 values(3, 1, 0, 1);
clid A B C
1 1 1 1
2 0 1 0
3 1 0 1
Type of columns A, B, and C is number. What I need to do is to change types of those columns to VARCHAR but in a quite tricky way.
In my real table I need to change datatype for hundred of columns so it is not so convenient to write a statement like following hundred of time:
ALTER TABLE table_name
MODIFY column_name datatype;
What i need to do is rather to convert all columns to VARCHAR except CLID column like we can do that in Python or R
Is there any way to do so in Oracle SQL or PLSQL?
Appreciate your help.
Here is a example of procedure that can help...
It accepts two parameters that should be a name of your table and list of columns you do not want to change...
At the begining there is a cursor that gets all the column names for your table except the one that you do not want to change...
Then it loop's though the columns and changes them...
CREATE OR REPLACE procedure test_proc(p_tab_name in varchar2
, p_col_names in varchar2)
IS
v_string varchar2(4000);
cursor c_tab_cols
is
SELECT column_name
FROM ALL_TAB_COLS
WHERE table_name = upper(p_tab_name)
and column_name not in (select regexp_substr(p_col_names,'[^,]+', 1, level) from dual
connect by regexp_substr(p_col_names, '[^,]+', 1, level) is not null);
begin
FOR i_record IN c_tab_cols
loop
v_string := 'alter table ' || p_tab_name || ' modify '
|| i_record.column_name || ' varchar(30)';
EXECUTE IMMEDIATE v_string;
end loop;
end;
/
Here is a demo:
DEMO
You can also extend this procedure with a type of data you want to change into... and with some more options I am sure....
Unfortunately, that isn't as simple as you'd want it to be. It is not a problem to write query which will write query for you (by querying USER_TAB_COLUMNS), but - column must be empty in order to change its datatype:
SQL> create table t1 (a number);
Table created.
SQL> insert into t1 values (1);
1 row created.
SQL> alter table t1 modify a varchar2(1);
alter table t1 modify a varchar2(1)
*
ERROR at line 1:
ORA-01439: column to be modified must be empty to change datatype
SQL>
If there are hundreds of columns involved, maybe you can't even
create additional columns in the same table (of VARCHAR2 datatype)
move values in there
drop "original" columns
rename "new" columns to "old names"
because there'a limit of 1000 columns per table.
Therefore,
creating a new table (with appropriate columns' datatypes),
moving data over there,
dropping the "original" table
renaming the "new" table to "old name"
is probably what you'll finally do. Note that it won't be necessarily easy either, especially if there are foreign keys involved.
A "query that writes query for you" might look like this (Scott's sample tables):
SQL> SELECT 'insert into dept (deptno, dname, loc) '
2 || 'select '
3 || LISTAGG ('to_char(' || column_name || ')', ', ')
4 WITHIN GROUP (ORDER BY column_name)
5 || ' from emp'
6 FROM user_tab_columns
7 WHERE table_name = 'EMP'
8 AND COLUMN_ID <= 3
9 /
insert into dept (deptno, dname, loc) select to_char(EMPNO), to_char(ENAME), to_char(JOB) from emp
SQL>
It'll save you from typing names of hundreds of columns.
I think its not possible to change data type of a column if values are there...
Empty the column by copying values to a dummy column and change data types.

Convert a piece of SQL into an Oracle function

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;
/

PL/SQL Add Multiple Columns to Query with Single Function

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;

Return multiple rows from a stored procedure

How do I create a stored procedure which can return multiple rows using SQL Developer BTW.?
Right now stored procedure returns the value for 1 row in 4 diff variables (there are 4 cols)
How I would go about making it so that it could return more than 1 row, for example if i were to query in my date it could return all the relevant data for that date instead of only 1.
create or replace PROCEDURE P2
(
ts IN TIMESTAMP,
u_id OUT VARCHAR2,
u_email OUT VARCHAR2,
cmnt OUT VARCHAR2
)
AS
BEGIN
SELECT U_ID , U_EML, C_TX INTO u_id, u_email, cmnt
FROM U_CM
WHERE U_CM_TS = ts;
END;
ts is the input timestamp
if i put in more a timestamp that has multiple rows associated with it i get an error?
How do i change the design so I can be successful in doing what i want? I am new to this so I dont know where to start
Use ref cursor:
create or replace PROCEDURE P2
(
ts IN TIMESTAMP,
p_result OUT sys_refcursor
)
AS
BEGIN
open p_result for
SELECT U_ID , U_EML, C_TX
FROM U_CM
WHERE U_CM_TS = ts;
END;
Also, try to give a more detailed names to columns and tables for more maintainable code. For example, user_id, user_email instead of u_id, u_eml. What is c_tx? I have no idea. Read about table and column naming conventions.

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 :(.