Create a view/temporary table from a column with CSV [duplicate] - sql

This question already has answers here:
Closed 11 years ago.
Possible Duplicate:
Comma Separated values in Oracle
Folks, I know its an exteremly bad idea, and this table needs to be normalized. But unfortunately I cannot change the schema.
We have a table in Oracle DB , as
id|value | other_columns
----------------------------
1|a,b,c |some values
Can we create a view with something like
id|value
-----------
1|a
1|b
1|c
Thanks in advance for help.

I don't think this is an exact duplicate of the question referenced in the close votes. Similar yes, but not the same.
Not exactly beautiful, but:
CREATE OR REPLACE VIEW your_view AS
SELECT tt.ID, SUBSTR(value, sp, ep-sp) split, other_col1, other_col2...
FROM (SELECT id, value
, INSTR(','||value, ',', 1, L) sp -- 1st posn of substr at this level
, INSTR(value||',', ',', 1, L) ep -- posn of delimiter at this level
FROM tt JOIN (SELECT LEVEL L FROM dual CONNECT BY LEVEL < 20) q -- 20 is max #substrings
ON LENGTH(value)-LENGTH(REPLACE(value,','))+1 >= L
) qq JOIN tt on qq.id = tt.id;
where tt is your table.
Works for csv values longer than 1 or null. The CONNECT BY LEVEL < 20 is arbitrary, adjust for your situation.
To illustrate:
SQL> CREATE TABLE tt (ID INTEGER, c VARCHAR2(20), othercol VARCHAR2(20));
Table created
SQL> INSERT INTO tt VALUES (1, 'a,b,c', 'val1');
1 row inserted
SQL> INSERT INTO tt VALUES (2, 'd,e,f,g', 'val2');
1 row inserted
SQL> INSERT INTO tt VALUES (3, 'a,f', 'val3');
1 row inserted
SQL> INSERT INTO tt VALUES (4,'aa,bbb,cccc', 'val4');
1 row inserted
SQL> CREATE OR REPLACE VIEW myview AS
2 SELECT tt.ID, SUBSTR(c, sp, ep-sp+1) splitval, othercol
3 FROM (SELECT ID
4 , INSTR(','||c,',',1,L) sp, INSTR(c||',',',',1,L)-1 ep
5 FROM tt JOIN (SELECT LEVEL L FROM dual CONNECT BY LEVEL < 20) q
6 ON LENGTH(c)-LENGTH(REPLACE(c,','))+1 >= L
7 ) q JOIN tt ON q.id =tt.id;
View created
SQL> select * from myview order by 1,2;
ID SPLITVAL OTHERCOL
--------------------------------------- -------------------- --------------------
1 a val1
1 b val1
1 c val1
2 d val2
2 e val2
2 f val2
2 g val2
3 a val3
3 f val3
4 aa val4
4 bbb val4
4 cccc val4
12 rows selected
SQL>

I did something similiar to this in the past. You need to create a function that accepts an input string and a separator and returns a dataset. If separator is ommited, then comma is assumed.
First create a new type that represents a table of strings:
create or replace type varcharTableType as table of varchar2(255);
/
Then create this function:
create or replace function splitString(
allValues in varchar2,
delim in varchar2 default ','
)
return varcharTableType
as
str varchar2(255) := allValues || delim;
pos number;
dataset varcharTableType := varcharTableType();
begin
loop
pos := instr(str, delim);
exit when (nvl(pos, 0) = 0);
dataset.extend;
dataset(dataset.count) := ltrim(rtrim(substr(str, 1, pos - 1)));
str := substr(str, pos + length(delim));
end loop;
return dataset;
end;
/
Finally, call as:
select *
from table(cast(splitString('a,b,c') as varcharTableType));
COLUMN_VALUE
---------------
a
b
c
3 rows selected
To answer your specific case, you simply need to create a view that joins your table with this function table, as:
create or replace view splitView as
select yourTable.id, s.column_value as value
from yourTable,
table(cast(splitString(yourTable.value) as varcharTableType)) s;
select * from splitView;
id value
---- ---------------
1 a
1 b
1 c
3 rows selected
I am not sure if this last query will work, as I don't have an Oracle machine right now, but hopefully should help you.

Related

How to insert long-format data into two separate tables using SQL?

I have selected the following data that I want to insert into the database.
Letter
Value
A
1
A
2
B
3
B
4
Since there is a repetition of "A" and "B" in this format, I want to split data into two separate tables: table1 and table2.
table1:
ID
Letter
1
A
2
B
ID here is automatically inserted by database (using a sequence).
table2:
table1_id
Value
1
1
1
2
2
3
2
4
In this particular example, I don't gain anything on storage but it illustrates in the best way what problem I have encountered.
How can I use SQL or PL/SQL to insert data into table1 and table2?
First populate table1 from the source
insert table1(Letter)
select distinct Letter
from srcTable;
Then load data from the source decoding letter to id
insert table2(table1_id, Value)
select t1.id, src.value
from srcTable src
join table1 t1 on t1.Letter = src.Letter;
You may use multitable insert with workaround to get stable nextval on sequence. Since nextval is evaluated on each row regardless of when condition, it is not sufficient to use it inside values.
insert all
when rn = 1 then into l(id, val) values(seq, letter)
when rn > 0 then into t(l_id, val) values(seq, val)
with a(letter, val) as (
select 'A', 1 from dual union all
select 'A', 2 from dual union all
select 'B', 3 from dual union all
select 'B', 4 from dual union all
select 'C', 5 from dual
)
, b as (
select
a.*,
l.id as l_id,
row_number() over(partition by a.letter order by a.val asc) as rn
from a
left join l
on a.letter = l.val
)
select
b.*,
max(decode(rn, 1, coalesce(
l_id,
extractvalue(
/*Hide the reference to the sequence due to restrictions
of multitalbe insert*/
dbms_xmlgen.getxmltype('select l_sq.nextval as seq from dual')
, '/ROWSET/ROW/SEQ/text()'
) + 0
))
) over(partition by b.letter) as seq
from b
select *
from l
ID | VAL
-: | :--
1 | A
2 | B
3 | C
select *
from t
L_ID | VAL
---: | --:
1 | 1
1 | 2
2 | 3
2 | 4
3 | 5
db<>fiddle here
Principally you need to produce and ID value for the table1 to be inserted into table2. For this, You can use INSERT ... RETURNING ID INTO v_id statement after creating the tables containing some constraints especially unique ones such as PRIMARY KEY and UNIQUE
CREATE TABLE table1( ID INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, letter VARCHAR2(1) NOT NULL );
ALTER TABLE table1 ADD CONSTRAINT uk_tab1_letter UNIQUE(letter);
CREATE TABLE table2( ID INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, table1_id INT, value INT );
ALTER TABLE table2 ADD CONSTRAINT fk_tab2_tab1_id FOREIGN KEY(table1_id) REFERENCES table1 (ID);
and adding exception handling in order not to insert repeating letters to the first table. Then use the following code block ;
DECLARE
v_id table1.id%TYPE;
v_letter table1.letter%TYPE := 'A';
v_value table2.value%TYPE := 1;
BEGIN
BEGIN
INSERT INTO table1(letter) VALUES(v_letter) RETURNING ID INTO v_id;
EXCEPTION WHEN OTHERS THEN NULL;
END;
INSERT INTO table2(table1_id,value) SELECT id,v_value FROM table1 WHERE letter = v_letter;
COMMIT;
END;
/
and run by changing the initialized values for v_letter&v_value as 'A'&2, 'B'&1,'B'&2 ..etc respectively.
Alternatively you can convert the code block to a stored procedure or function such as
CREATE OR REPLACE PROCEDURE Pr_Ins_Tabs(
v_letter table1.letter%TYPE,
v_value table2.value%TYPE
) AS
v_id table1.id%TYPE;
BEGIN
BEGIN
INSERT INTO table1(letter) VALUES(v_letter) RETURNING ID INTO v_id;
EXCEPTION WHEN OTHERS THEN NULL;
END;
INSERT INTO table2(table1_id,value) SELECT id,v_value FROM table1 WHERE letter = v_letter;
COMMIT;
END;
/
in order to revoke resiliently such as
BEGIN
Pr_Ins_Tabs('A',2);
END;
/
Demo
PS. If your DB verion is prior to 12c, then create sequences(seq1&seq2) and use seq1.nextval&seq2.nextval within the Insert statements as not possible to use GENERATED ALWAYS AS IDENTITY clause within the table creation DDL statements.

Oracle SQL: using LAG function with user-defined-type returns "inconsistent datatypes"

I have a type MyType defined as follows:
create or replace type MyType as varray(20000) of number(18);
And a table MyTable defined as follows:
create table MyTable (
id number(18) primary key
,widgets MyType
)
I am trying to select the widgets for each row and its logically previous row in MyTable using the following SQL:
select t.id
,lag(t.widgets,1) over (order by t.id) as widgets_previous
from MyTable t
order by t.id;
and I get the response:
ORA-00932: inconsistent datatypes: expected - got MYSCHEMA.MYTYPE
If I run the exact same query using a column of type varchar or number instead of MyType it works fine.
The type of the column in the current row and its previous row must be the same so I can only assume it is something related to the user defined type.
Do I need to do something special to use LAG with a user defined type, or does LAG not support user defined types? If the latter, are there any other utility functions that would provide the same functionality or do I need to do a traditional self join in order to achieve the same?
After reading all the above I've opted for the following as the most effective method for achieving what I need:
select curr.id
,curr.widgets as widgets
,prev.widgets as previous_widgets
from (select a.id
,a.widgets
,lag(a.id,1) over (order by a.id) as previous_id
from mytable a
) curr
left join mytable prev on (prev.id = curr.previous_id)
order by curr.id
ie. a lag / self join hybrid using lag on a number field that it doesn't complain about to identify the join condition. It's fairly tidy I think and I get my collections as desired. Thanks to everyone for the extremely useful input.
You can use lag with UDT. The problem is varray
Does this give you a result?
select t.id
,lag(
(select listagg(column_value, ',') within group (order by column_value)
from table(t.widgets))
,1) over (order by t.id) as widgets_previous
from MyTable t
order by t.id;
You could try something like:
SQL> create or replace type TestType as varray(20000) of number(18);
Type created.
SQL> create table TestTable (
id number(18) primary key
,widgets TestType
)
Table created.
SQL> delete from testtable
0 rows deleted.
SQL> insert into TestTable values (1, TestType(1,2,3,4))
1 row created.
SQL> insert into TestTable values (2, TestType(5,6,7))
1 row created.
SQL> insert into TestTable values (3, TestType())
1 row created.
SQL> insert into TestTable values (4,null)
1 row created.
SQL> commit
Commit complete.
SQL> -- show all data with widgets
SQL> select t.id, w.column_value as widget_ids
from testtable t, table(t.widgets) w
ID WIDGET_IDS
---------- ----------
1 1
1 2
1 3
1 4
2 5
2 6
2 7
7 rows selected.
SQL> -- show with lag function
SQL> select t.id, lag(w.column_value, 1) over (order by t.id) as widgets_previous
from testtable t, table(t.widgets) w
ID WIDGETS_PREVIOUS
---------- ----------------
1
1 1
1 2
1 3
2 4
2 5
2 6
7 rows selected.

How to update a varray type within a table with a simple update statement?

I have a table that has a column defined as varray of a defined type. The production table is way more complicated then the following example.
I am able to select the single columns within the type of the varray. But I would like to update the table with a simple update statement (rather than going through a pl/sql routine).
If this is not possible (and I must go through a pl/sql routine) what is a smart and easy way to code this?
update (select l.id, t.* from my_object_table l, table(l.object_list) t)
set value2 = 'obj 4 upd'
where value1 = 10
ORA-01733: virtual column not allowed here
Here the full example of types etc.
create or replace type my_object
as object(
value1 number,
value2 varchar2(10),
value3 number);
create or replace type my_object_varray as varray(100000000) of my_object;
create table my_object_table (id number not null, object_list my_object_varray);
insert into my_object_table
values (1, my_object_varray (
my_object(1,'object 1',10),
my_object(2,'object 2',20),
my_object(3,'object 3',30)
)
);
insert into my_object_table
values (2, my_object_varray (
my_object(10,'object 4',10),
my_object(20,'object 5',20),
my_object(30,'object 6',30)
)
);
select l.id, t.* from my_object_table l, table(l.object_list) t;
Type created.
Type created.
Table created.
1 row created.
1 row created.
ID VALUE1 VALUE2 VALUE3
---------- ---------- ---------- ----------
1 1 object 1 10
1 2 object 2 20
1 3 object 3 30
2 10 object 4 10
2 20 object 5 20
2 30 object 6 30
6 rows selected.
I don't believe you can update a single object's value within a varray from plain SQL, as there is no way to reference the varray index. (The link Alessandro Rossi posted seems to support this, though not necessarily for that reason). I'd be interested to be proven wrong though, of course.
I know you aren't keen on a PL/SQL approach but if you do have to then you could do this to just update that value:
declare
l_object_list my_object_varray;
cursor c is
select l.id, l.object_list, t.*
from my_object_table l,
table(l.object_list) t
where t.value1 = 10
for update of l.object_list;
begin
for r in c loop
l_object_list := r.object_list;
for i in 1..l_object_list.count loop
if l_object_list(i).value1 = 10 then
l_object_list(i).value2 := 'obj 4 upd';
end if;
end loop;
update my_object_table
set object_list = l_object_list
where current of c;
end loop;
end;
/
anonymous block completed
select l.id, t.* from my_object_table l, table(l.object_list) t;
ID VALUE1 VALUE2 VALUE3
---------- ---------- ---------- ----------
1 1 object 1 10
1 2 object 2 20
1 3 object 3 30
2 10 obj 4 upd 10
2 20 object 5 20
2 30 object 6 30
SQL Fiddle.
If you're updating other things as well then you might prefer a function that returns the object list with the relevant value updated:
create or replace function get_updated_varray(p_object_list my_object_varray,
p_value1 number, p_new_value2 varchar2)
return my_object_varray as
l_object_list my_object_varray;
begin
l_object_list := p_object_list;
for i in 1..l_object_list.count loop
if l_object_list(i).value1 = p_value1 then
l_object_list(i).value2 := p_new_value2;
end if;
end loop;
return l_object_list;
end;
/
Then call that as part of an update; but you still can't update your in-line view directly:
update (
select l.id, l.object_list
from my_object_table l, table(l.object_list) t
where t.value1 = 10
)
set object_list = get_updated_varray(object_list, 10, 'obj 4 upd');
SQL Error: ORA-01779: cannot modify a column which maps to a non key-preserved table
You need to update based on relevant the ID(s):
update my_object_table
set object_list = get_updated_varray(object_list, 10, 'obj 4 upd')
where id in (
select l.id
from my_object_table l, table(l.object_list) t
where t.value1 = 10
);
1 rows updated.
select l.id, t.* from my_object_table l, table(l.object_list) t;
ID VALUE1 VALUE2 VALUE3
---------- ---------- ---------- ----------
1 1 object 1 10
1 2 object 2 20
1 3 object 3 30
2 10 obj 4 upd 10
2 20 object 5 20
2 30 object 6 30
SQL Fiddle.
If you wanted to hide the complexity even further you could create a view with an instead-of trigger that calls the function:
create view my_object_view as
select l.id, t.* from my_object_table l, table(l.object_list) t
/
create or replace trigger my_object_view_trigger
instead of update on my_object_view
begin
update my_object_table
set object_list = get_updated_varray(object_list, :old.value1, :new.value2)
where id = :old.id;
end;
/
Then the update is pretty much what you wanted, superficially at least:
update my_object_view
set value2 = 'obj 4 upd'
where value1 = 10;
1 rows updated.
select * from my_object_view;
ID VALUE1 VALUE2 VALUE3
---------- ---------- ---------- ----------
1 1 object 1 10
1 2 object 2 20
1 3 object 3 30
2 10 obj 4 upd 10
2 20 object 5 20
2 30 object 6 30
SQL Fiddle.
As the Oracle documentation states here
While nested tables can also be changed in a piecewise fashions,
varrays cannot.
There is no way to modify VARRAYS in piecewise fashion. The only things you could do are:
Convert the data type of your fied into a NESTED TABLE (CREATE TYPE xxx AS TABLE OF yyy)
Fetch the varray of the row you want to change, modify with in your client language, then update the row to set the modified value on it.
Have you tried this?
UPDATE (
SELECT value2
FROM
TABLE(SELECT object_list FROM my_object_table)
WHERE value1 = 10
) t
SET t.value2 = 'object 4 upd';
I couldn't test this query, use with care. I'm not sure Oracle can actually do that...

How can I select from list of values in Oracle

I am referring to this stackoverflow answer:
How can I select from list of values in SQL Server
How could something similar be done in Oracle?
I've seen the other answers on this page that use UNION and although this method technically works, it's not what I would like to use in my case.
So I would like to stay with syntax that more or less looks like a comma-separated list of values.
UPDATE regarding the create type table answer:
I have a table:
CREATE TABLE BOOK
( "BOOK_ID" NUMBER(38,0)
)
I use this script but it does not insert any rows to the BOOK table:
create type number_tab is table of number;
INSERT INTO BOOK (
BOOK_ID
)
SELECT A.NOTEBOOK_ID FROM
(select column_value AS NOTEBOOK_ID from table (number_tab(1,2,3,4,5,6))) A
;
Script output:
TYPE number_tab compiled
Warning: execution completed with warning
But if I use this script it does insert new rows to the BOOK table:
INSERT INTO BOOK (
BOOK_ID
)
SELECT A.NOTEBOOK_ID FROM
(SELECT (LEVEL-1)+1 AS NOTEBOOK_ID FROM DUAL CONNECT BY LEVEL<=6) A
;
You don't need to create any stored types, you can evaluate Oracle's built-in collection types.
select distinct column_value from table(sys.odcinumberlist(1,1,2,3,3,4,4,5))
If you are seeking to convert a comma delimited list of values:
select column_value
from table(sys.dbms_debug_vc2coll('One', 'Two', 'Three', 'Four'));
-- Or
select column_value
from table(sys.dbms_debug_vc2coll(1,2,3,4));
If you wish to convert a string of comma delimited values then I would recommend Justin Cave's regular expression SQL solution.
Starting from Oracle 12.2, you don't need the TABLE function, you can directly select from the built-in collection.
SQL> select * FROM sys.odcinumberlist(5,2,6,3,78);
COLUMN_VALUE
------------
5
2
6
3
78
SQL> select * FROM sys.odcivarchar2list('A','B','C','D');
COLUMN_VALUE
------------
A
B
C
D
There are various ways to take a comma-separated list and parse it into multiple rows of data. In SQL
SQL> ed
Wrote file afiedt.buf
1 with x as (
2 select '1,2,3,a,b,c,d' str from dual
3 )
4 select regexp_substr(str,'[^,]+',1,level) element
5 from x
6* connect by level <= length(regexp_replace(str,'[^,]+')) + 1
SQL> /
ELEMENT
----------------------------------------------------
1
2
3
a
b
c
d
7 rows selected.
Or in PL/SQL
SQL> create type str_tbl is table of varchar2(100);
2 /
Type created.
SQL> create or replace function parse_list( p_list in varchar2 )
2 return str_tbl
3 pipelined
4 is
5 begin
6 for x in (select regexp_substr( p_list, '[^,]', 1, level ) element
7 from dual
8 connect by level <= length( regexp_replace( p_list, '[^,]+')) + 1)
9 loop
10 pipe row( x.element );
11 end loop
12 return;
13 end;
14
15 /
Function created.
SQL> select *
2 from table( parse_list( 'a,b,c,1,2,3,d,e,foo' ));
COLUMN_VALUE
--------------------------------------------------------------------------------
a
b
c
1
2
3
d
e
f
9 rows selected.
You can do this:
create type number_tab is table of number;
select * from table (number_tab(1,2,3,4,5,6));
The column is given the name COLUMN_VALUE by Oracle, so this works too:
select column_value from table (number_tab(1,2,3,4,5,6));
Hi it is also possible for Strings with XML-Table
SELECT trim(COLUMN_VALUE) str FROM xmltable(('"'||REPLACE('a1, b2, a2, c1', ',', '","')||'"'));
[Deprecated] - Just to add for the op,
The issue with your second code it seems to be that you haven't defined there your "number_tab" type.
AS :
CREATE type number_tab is table of number;
SELECT a.notebook_id FROM (
SELECT column_value AS notebook_id FROM table (number_tab(1,2,3,4,5,6) ) ) a;
INSERT INTO BOOK ( BOOK_ID )
SELECT a.notebook_id FROM (
SELECT column_value AS notebook_id FROM table (number_tab(1,2,3,4,5,6) ) ) a;
DROP type number_tab ;
Sorry, I couldn't reproduce your error, could you send us the version of oracle used and the same code used for the procedure in the first instance?, that could help. All the best.

Pivot values of a column based on a search string

Note: I would like to do this in a single SQL statement. not pl/sql, cursor loop, etc.
I have data that looks like this:
ID String
-- ------
01 2~3~1~4
02 0~3~4~6
03 1~4~5~1
I want to provide a report that somehow pivots the values of the String column into distinct rows such as:
Value "Total number in table"
----- -----------------------
1 3
2 1
3 2
4 3
5 1
6 1
How do I go about doing this? It's like a pivot table but I am trying to pivot the data in a column, rather than pivoting the columns in the table.
Note that in real application, I do not actually know what the values of the String column are; I only know that the separation between values is '~'
Given this test data:
CREATE TABLE tt (ID INTEGER, VALUE VARCHAR2(100));
INSERT INTO tt VALUES (1,'2~3~1~4');
INSERT INTO tt VALUES (2,'0~3~4~6');
INSERT INTO tt VALUES (3,'1~4~5~1');
This query:
SELECT VALUE, COUNT(*) "Total number in table"
FROM (SELECT tt.ID, SUBSTR(qq.value, sp, ep-sp) VALUE
FROM (SELECT id, value
, INSTR('~'||value, '~', 1, L) sp -- 1st posn of substr at this level
, INSTR(value||'~', '~', 1, L) ep -- posn of delimiter at this level
FROM tt JOIN (SELECT LEVEL L FROM dual CONNECT BY LEVEL < 20) q -- 20 is max #substrings
ON LENGTH(value)-LENGTH(REPLACE(value,'~'))+1 >= L
) qq JOIN tt on qq.id = tt.id)
GROUP BY VALUE
ORDER BY VALUE;
Results in:
VALUE Total number in table
---------- ---------------------
0 1
1 3
2 1
3 2
4 3
5 1
6 1
7 rows selected
SQL>
You can adjust the maximum number of items in your search string by adjusting the "LEVEL < 20" to "LEVEL < your_max_items".