Column Transpositions into Oracle (pl/sql, sql) - sql

I'm doing transpositions and looking if for some column exist exactly the same value into other table. Example:
tableA: id = 10, col = abc , value = 10
tableB: id = 10, abc = 10
I have below code:
declare
TYPE t_my_list is record(id VARCHAR2(4000),col VARCHAR2(4000),val VARCHAR2(4000));
Type list_3STR is table of t_my_list;
v_stmt VARCHAR2(32000) := 'SELECT id, col, val FROM userA.tableA';
v_lstmt VARCHAR2(32000);
v_ret list_3STR := list_3STR();
cDel number;
begin
EXECUTE IMMEDIATE v_stmt BULK COLLECT INTO v_ret;
for i in v_ret.first..v_ret.last loop
--DBMS_OUTPUT.PUT_LINE('ID: '||v_ret (i).id||', COL: '||v_ret (i).col||', VAL: '||v_ret (i).val);
v_lstmt := 'SELECT count(*) FROM userB.tableB WHERE NVL(cast('||v_ret (i).col||' as Varchar2(100)), ''<null>'') in ('''||v_ret (i).val||''', ''<null>'') and ID = '''||v_ret (i).id||''' ';
DBMS_OUTPUT.PUT_LINE(v_lstmt);
EXECUTE IMMEDIATE v_lstmt INTO cDel;
If cDel > 0 Then
DBMS_OUTPUT.PUT_LINE('delete row from userA.tableA')
End if;
end loop;
DBMS_OUTPUT.PUT_LINE('v_ret = '||v_ret.count);
end;
I have to consider 5 CASE:
userA.tableA to userB.tableB
NULL to NULL = 1 --DELETE value
NULL to DATA = 0 --NOT DELETE
the same DATA to DATA = 1 --DELETE
diffrent DATA to diffrent DATA = 0 --NOT DELETE
DATA to NULL = 0 = --NOT DELETE
My code is working for case 1 to 4. How to resolve 5th problem?
Update: CASE example:
1.
id = 10, col = test, val = null
id = 10, test = null
2.
id = 10, col = test, val = null
id = 10, test = 99
3.
id = 10, col = test, val = 99
id = 10, test = 99
4.
id = 10, col = test, val = 5
id = 10, test = 99
5.
id = 10, col = test, val = 4
id = 10, test = null

The way I would do this (if I absolutely had to do it this way) is to find out what columns need to be compared to in table b, then you can work out what the values in table a can be used to compare with those columns.
Once you have that (by querying user_tables (or all_tables/dba_tables as necessary) to retrieve the relevant columns), you can then generate a join clause.
The join clause needs to check that either both columns are null or both columns have the same non-null value.
Once you have that, you can then use that in a merge statement to delete the rows that match the join condition. We do that by first updating the matched rows (which we need to do in order for those rows to be seen by the delete in the next step) and then delete them.
Here's a working test case:
Setup:
create table a (id integer, col varchar2(30), val number, constraint a_pk primary key (id, col));
create table b (id integer, abc number, test number, xyz number, constraint b_pk primary key (id));
insert into a (id, col, val)
select 10, 'test', null from dual union all
select 11, 'test', null from dual union all
select 12, 'test', 99 from dual union all
select 13, 'test', 5 from dual union all
select 14, 'test', 4 from dual union all
select 10, 'abc', 1 from dual union all
select 10, 'xyz', 7 from dual union all
select 11, 'abc', 4 from dual union all
select 11, 'xyz', 6 from dual union all
select 12, 'abc', 12 from dual union all
select 12, 'efg', 30 from dual union all
select 13, 'abc', 3 from dual union all
select 13, 'xyz', 5 from dual union all
select 14, 'abc', 8 from dual union all
select 14, 'xyz', 9 from dual;
insert into b (id, abc, test, xyz)
select 10, 1, null, 7 from dual union all
select 11, 4, 99, 8 from dual union all
select 12, 11, 99, 30 from dual union all
select 13, 1, 5, 5 from dual union all
select 14, 1, null, 7 from dual;
commit;
Rows we're expecting to remain in table a
select a.*
from a tgt
full outer join b src on (tgt.id = src.id
and (1 = 0
or (upper(tgt.col) = 'ABC' and (tgt.val = src.ABC or (tgt.val is null and src.ABC is null)))
or (upper(tgt.col) = 'TEST' and (tgt.val = src.TEST or (tgt.val is null and src.TEST is null)))
or (upper(tgt.col) = 'XYZ' and (tgt.val = src.XYZ or (tgt.val is null and src.XYZ is null)))))
where tgt.id is not null and src.id is NULL
ORDER BY a.id, a.col;
ID COL VAL
-- ---- ---
11 test
11 xyz 6
12 abc 12
12 efg 30
13 abc 3
14 abc 8
14 test 4
14 xyz 9
Run the code
set serveroutput on
declare
v_sql clob;
begin
v_sql := 'merge into a tgt' || chr(10) ||
' using b src' || chr(10) ||
' on (tgt.id = src.id' || chr(10) ||
' and (1 = 0';
-- Generate the join conditions
for rec in (select ' or (upper(tgt.col) = '''||column_name||''' and (tgt.val = src.'||column_name||' or (tgt.val is null and src.'||column_name||' is null)))' join_condition
from user_tab_columns
where table_name = 'B'
and column_name != 'ID')
loop
v_sql := v_sql || chr(10) || rec.join_condition;
end loop;
v_sql := v_sql || '))' || chr(10) ||
'when matched then' || chr(10) || -- we only care about rows that match on the join clause
' update set tgt.col = tgt.col' || chr(10) || -- we need to physically update those rows, or the delete clause won't see them.
' delete where 1=1'; -- we need to have the where clause here, but we're deleting all rows that were updated in the previous step, hence 1=1 which is always true.
dbms_output.put_line (v_sql||';');
execute immediate v_sql;
end;
/
dbms_output of the statement
merge into a tgt
using b src
on (tgt.id = src.id
and (1 = 0
or (upper(tgt.col) = 'ABC' and (tgt.val = src.ABC or (tgt.val is null and src.ABC is null)))
or (upper(tgt.col) = 'TEST' and (tgt.val = src.TEST or (tgt.val is null and src.TEST is null)))
or (upper(tgt.col) = 'XYZ' and (tgt.val = src.XYZ or (tgt.val is null and src.XYZ is null)))))
when matched then
update set tgt.col = tgt.col
delete where 1=1;
Check we have the columns we expected would remain
select *
from a
order by id, col;
ID COL VAL
-- ---- ---
11 test
11 xyz 6
12 abc 12
12 efg 30
13 abc 3
14 abc 8
14 test 4
14 xyz 9
However, if you have any choice over the matter, I would seriously, seriously ask you to rethink this design.
Having key-values in table a is a very strange way to go about updating table b with, and as you have found out, it makes doing the simplest things very tricky.

Related

Oracle VIEW showing when last records were created in my tables

I have tables t1 and t2 and both of them do have columns created_on containing the timestamp when each record was created. Usual thing.
Now I'd like to create the view which would show my tables and the timestamp of last created record (MAX(created_on)) in corresponding table.
The result should look like:
table | last_record
======+============
t1 | 10.05.2019
t2 | 12.11.2020
For example I can retrieve the list of my tables with:
SELECT * FROM USER_TABLES WHERE table_name LIKE 'T%'
I'd like to get the timestamp of last record for each of these tables.
How to create this view?
It might depend on tables' description; I presume they are somehow related to each other.
Anyway: here's how I understood the question. Read comments within code.
SQL> with
2 -- sample data
3 t1 (id, name, created_on) as
4 (select 1, 'Little', date '2021-12-14' from dual union all --> max for Little
5 select 2, 'Foot' , date '2021-12-13' from dual union all --> max for Foot
6 select 2, 'Foot' , date '2021-12-10' from dual
7 ),
8 t2 (id, name, created_on) as
9 (select 2, 'Foot' , date '2021-12-09' from dual union all
10 select 3, 'SBrbot', date '2021-12-14' from dual --> max for SBrbot
11 )
12 -- query you'd use for a view
13 select id, name, max(created_on) max_created_on
14 from
15 -- union them, so that it is easier to find max date
16 (select id, name, created_on from t1
17 union all
18 select id, name, created_on from t2
19 )
20 group by id, name;
ID NAME MAX_CREATE
---------- ------ ----------
1 Little 14.12.2021
2 Foot 13.12.2021
3 SBrbot 14.12.2021
SQL>
After you fixed the question, that's even easier; view query begins at line #12:
SQL> with
2 -- sample data
3 t1 (id, name, created_on) as
4 (select 1, 'Little', date '2021-12-14' from dual union all
5 select 2, 'Foot' , date '2021-12-13' from dual union all
6 select 2, 'Foot' , date '2021-12-10' from dual
7 ),
8 t2 (id, name, created_on) as
9 (select 2, 'Foot' , date '2021-12-09' from dual union all
10 select 3, 'SBrbot', date '2021-12-14' from dual
11 )
12 select 't1' source_table, max(created_on) max_created_on from t1
13 union
14 select 't2' source_table, max(created_on) max_created_on from t2;
SO MAX_CREATE
-- ----------
t1 14.12.2021
t2 14.12.2021
SQL>
If it has to be dynamic, one option is to create a function that returns ref cursor:
SQL> create or replace function f_max
2 return sys_refcursor
3 is
4 l_str varchar2(4000);
5 rc sys_refcursor;
6 begin
7 for cur_r in (select distinct c.table_name
8 from user_tab_columns c
9 where c.column_name = 'CREATED_ON'
10 order by c.table_name
11 )
12 loop
13 l_str := l_str ||' union all select ' || chr(39) || cur_r.table_name || chr(39) ||
14 ' table_name, max(created_on) last_updated from ' || cur_r.table_name;
15 end loop;
16
17 l_str := ltrim(l_str, ' union all ');
18
19 open rc for l_str;
20 return rc;
21 end;
22 /
Function created.
Testing:
SQL> select f_max from dual;
F_MAX
--------------------
CURSOR STATEMENT : 1
CURSOR STATEMENT : 1
TA LAST_UPDAT
-- ----------
T1 14.12.2021
T2 14.12.2021
SQL>
I have 30+ tables and wanted avoid hard coding SELECT statements for each of these tables and UNION them all. I expected some solution where I would insert tablenames in kinda array and create JOIN to show result with all last records. I know problem is here that tablename is variable!
You cannot do this in SQL as a VIEW is set at compile time and the tables must be known; the best you can do is to dynamically create an SQL statement in PL/SQL and then use EXECUTE IMMEDIATE and then re-run it if you want to recreate the view:
DECLARE
v_tables SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST(
'TABLE1', 'TABLE2', 'table3', 'TABLE5'
);
v_sql CLOB := 'CREATE OR REPLACE VIEW last_dates (table_name, last_date) AS ';
BEGIN
FOR i in 1 .. v_tables.COUNT LOOP
IF i > 1 THEN
v_sql := v_sql || ' UNION ALL ';
END IF;
v_sql := v_sql || 'SELECT '
|| DBMS_ASSERT.ENQUOTE_LITERAL(v_tables(i))
|| ', MAX(created_on) FROM '
|| DBMS_ASSERT.ENQUOTE_NAME(v_tables(i), FALSE);
END LOOP;
EXECUTE IMMEDIATE v_sql;
END;
/
Then for the sample tables:
CREATE TABLE table1 (created_on) AS
SELECT SYSDATE - LEVEL FROM DUAL CONNECT BY LEVEL <= 3;
CREATE TABLE table2 (created_on) AS
SELECT SYSDATE - LEVEL FROM DUAL CONNECT BY LEVEL <= 3;
CREATE TABLE "table3" (created_on) AS
SELECT SYSDATE - LEVEL FROM DUAL CONNECT BY LEVEL <= 3;
CREATE TABLE table5 (created_on) AS
SELECT SYSDATE FROM DUAL;
After running the PL/SQL block, then:
SELECT * FROM last_dates;
Outputs:
TABLE_NAME
LAST_DATE
TABLE1
2021-12-13 13:21:58
TABLE2
2021-12-13 13:21:59
table3
2021-12-13 13:21:59
TABLE5
2021-12-14 13:21:59
db<>fiddle here

Oracle function return table

I don't know what is best solution for my problem. I need function with one parameter which resault is
VAL
-----
1
2
3
In function I need put union all to get all value.
Select column_1 as VAL from my_table where id = P_FUNCTION_PARAMETER --return 1
union all
Select column_2 as VAL from my_table where id = P_FUNCTION_PARAMETER --return 2
union all
Select column_3 as VAL from my_table where id = P_FUNCTION_PARAMETER; --return 3
What is the best solution for this?
"Best" just depends. Return a ref cursor or a collection, whichever you prefer.
For example:
SQL> create or replace function f_test_rc
2 return sys_refcursor
3 is
4 rc sys_refcursor;
5 begin
6 open rc for
7 select 1 from dual union all
8 select 2 from dual union all
9 select 3 from dual;
10
11 return rc;
12 end;
13 /
Function created.
SQL> select f_test_rc from dual;
F_TEST_RC
--------------------
CURSOR STATEMENT : 1
CURSOR STATEMENT : 1
1
----------
1
2
3
SQL> create or replace function f_test_coll
2 return sys.odcinumberlist
3 as
4 l_coll sys.odcinumberlist;
5 begin
6 select * bulk collect into l_coll
7 from (select 1 from dual union all
8 select 2 from dual union all
9 select 3 from dual
10 );
11
12 return l_coll;
13 end;
14 /
Function created.
SQL> select * from table(f_test_coll);
COLUMN_VALUE
------------
1
2
3
SQL>
First let's set up a small table for testing:
create table my_table
( id number primary key
, column_1 number
, column_2 number
, column_3 number
);
insert into my_table
select 1008, 3, -8, 0.2 from dual union all
select 1002, 6, null, -1.2 from dual
;
commit;
The function can look like this. Note that I don't use union all - that will require reading the table three times, when only one time is enough.
create or replace function my_function (p_function_parameter number)
return sys.odcinumberlist
as
arr sys.odcinumberlist;
begin
select case ord when 1 then column_1
when 2 then column_2
when 3 then column_3 end
bulk collect into arr
from my_table cross join
(select level as ord from dual connect by level <= 3)
where id = p_function_parameter
order by ord
;
return arr;
end;
/
The function could be used, for example, like this: (in older versions you may need to wrap the function call within the table operator)
select * from my_function(1002);
COLUMN_VALUE
------------
6
-1.2

Oracle - How use string data in (in operator)

In tbl_1 I have:
id
1
2
3
4
5
6
7
8
9
in tbl_2:
id value
1 1,2,3
2 5
Select * from tbl_1 where id in (Select value from tbl_2 where id = 2); --is OK
Select * from tbl_1 where id in (Select value from tbl_2 where id = 1);
--Need this resault: 3 rows: 1, 2 and 3
Fix your data model! You should not be storing numbers as strings. You should have properly declared foreign key relationships. Strings should not be used to store multiple values.
Sometimes, we are stuck with other people's really, really, really bad decisions. You can do what you want with `like:
Select t1.*
from tbl_1 t1
where exists (select 1
from tbl_2 t2
where t2.id = 1 and
',' || t2.value || ',' like '%,' || t1.id ',%'
);
However, your effort should be going into fixing the data, rather than trying to deal with it. The correct data would be a junction/association table with one row per id and value for table 2:
id value
1 1
1 2
1 3
2 5
Yet another option:
SQL> with
2 -- sample data
3 tbl_1 (id) as
4 (select 1 from dual union all
5 select 2 from dual union all
6 select 3 from dual union all
7 select 4 from dual union all
8 select 5 from dual union all
9 select 6 from dual union all
10 select 7 from dual union all
11 select 8 from dual union all
12 select 9 from dual
13 ),
14 tbl_2 (id, value) as
15 (select 1, '1,2,3' from dual union all
16 select 2, '5,6,7' from dual
17 )
18 -- query which returns what you want
19 select a.id
20 from tbl_1 a join
21 (select regexp_substr(b.value, '[^,]+', 1, column_value) id
22 from tbl_2 b cross join
23 table(cast(multiset(select level from dual
24 connect by level <= regexp_count(b.value, ',') + 1
25 ) as sys.odcinumberlist))
26 where b.id = 1
27 ) c on c.id = a.id;
ID
----------
1
2
3
SQL>
One option uses string functions:
select t1.*
from t1
inner join t2 on ',' || t2.value || ',' like '%,' || t1.id || ',%'
where t2.id = 1

Apply a select for every element of a column

I would like to update third with a select that uses column first
|first #|second #|third #|
|_______|________|_______|
|___1___|___1____|_null__|
|___5___|___2____|_null__|
|___3___|___6____|_null__|
|___2___|___4____|_null__|
In pseudo code:
for row in table:
row.third = result_of_a_select(row.first)
What is the equivalent on SQL?
My wrong attempt:
update example_table
set third=
(
SELECT MAX(CDARTI) FROM
(
SELECT A.CDARTI
FROM PGMR.UT_ART_CODALT T,
PGMR.MRP_ARCH_ARTICOLI A
WHERE
A.CDARTI = T.CDARTI AND
T.CDARTI = first
UNION
SELECT A.CDARTI
FROM PGMR.UT_ART_CODALT T,
PGMR.MRP_ARCH_ARTICOLI A
WHERE
A.CDARTI = T.CDARTI_A AND
T.CDARTI = first
UNION
SELECT A.CDARTI
FROM PGMR.UT_ART_CODALT T,
PGMR.MRP_ARCH_ARTICOLI A
WHERE
A.CDARTI = T.CDARTI AND
T.CDARTI_A = first
UNION
SELECT A.CDARTI
FROM PGMR.UT_ART_CODALT T,
PGMR.MRP_ARCH_ARTICOLI A
WHERE
A.CDARTI = T.CDARTI_A AND
T.CDARTI = (SELECT T2.CDARTI FROM PGMR.UT_ART_CODALT T2 WHERE T2.CDARTI_A = first)
)
);
commit;
Works for me.
update t1 set third = (select third from t2 where t2.first = t1.first);
Fiddle (switched to MySql DB since I can't make Oracle work in it, but tested against my local Oracle as well).
The problem is not in the approach, but in your query. To fix that, you'll need to provide a Minimal, Complete, and Verifiable example.
A simplified test case:
SQL> create table yourTable (first, second, third) as (
2 select 1, 1, cast (null as number) from dual union all
3 select 5, 2, cast (null as number) from dual union all
4 select 3, 6, cast (null as number) from dual union all
5 select 2, 4, cast (null as number) from dual
6 );
Table created.
SQL> update yourTable t
2 set third = (select t.first * 2 from dual);
4 rows updated.
SQL> select * from yourTable;
FIRST SECOND THIRD
---------- ---------- ----------
1 1 2
5 2 10
3 6 6
2 4 4
SQL>
To make it a slightly more interesting/illustrative example I'm updating the third column with the value from a mapping table and have included duplicate values.
You can use MERGE and match on the pseudocolumn ROWID:
Oracle Setup:
CREATE TABLE table_name ( first, second, third ) AS
SELECT 1, 1, CAST( NULL AS NUMBER ) FROM DUAL UNION ALL
SELECT 5, 2, NULL FROM DUAL UNION ALL
SELECT 3, 6, NULL FROM DUAL UNION ALL
SELECT 2, 4, NULL FROM DUAL UNION ALL
SELECT 3, 4, NULL FROM DUAL;
CREATE TABLE table_name_map ( first, value ) AS
SELECT 1, 9 FROM DUAL UNION ALL
SELECT 2, 8 FROM DUAL UNION ALL
SELECT 3, 7 FROM DUAL UNION ALL
SELECT 4, 6 FROM DUAL UNION ALL
SELECT 5, 5 FROM DUAL;
Update:
MERGE INTO table_name dst
USING ( SELECT t.ROWID AS ri,
m.value
FROM table_name t
INNER JOIN table_name_map m
ON ( t.first = m.first )
) src
ON ( src.ri = dst.ROWID )
WHEN MATCHED THEN
UPDATE SET third = src.value;
Result:
FIRST SECOND THIRD
----- ------ -----
1 1 9
5 2 5
3 6 7
2 4 8
3 4 7

Transpose multiple Columns at same

I have this:
Year Apple Orange
1 100 150
2 200 250
3 300 350
2 200 250
1 100 150
I need this:
Fruit 1 2 3
Apple 200 400 300
Orange 300 500 350
I have option A and option B, but it only transposes 1 fruit unless i do an "Union all".
Option A:
select
'Apple' as Fruit
,MAX(DECODE(year, '1', sum(Apple)) "1"
,MAX(DECODE(year, '2', sum(Apple)) "2"
from MyTable
Option B:
select
*
from (
select
Apple
,Year
from MyTable
)
PIVOT(sum(Apple) for year in ('1', '2', '3'))
Question:
Can U transpose all columns without an "Union"?
Oracle Setup:
CREATE TABLE table_name ( year, apple, orange ) AS
SELECT 1, 100, 150 FROM DUAL UNION ALL
SELECT 2, 200, 250 FROM DUAL UNION ALL
SELECT 3, 300, 350 FROM DUAL UNION ALL
SELECT 2, 200, 250 FROM DUAL UNION ALL
SELECT 1, 100, 150 FROM DUAL;
Query - Unpivot then pivot:
SELECT *
FROM (
SELECT *
FROM table_name
UNPIVOT( value FOR fruit IN ( Apple, Orange ) )
)
PIVOT ( SUM( value ) FOR year IN ( 1, 2, 3 ) );
Output:
FRUIT 1 2 3
------ --- --- ---
ORANGE 300 500 350
APPLE 200 400 300
This is how you can do it dynamically.
Create statements
CREATE TABLE MyTable
(Year int, Apple int, Orange int) ;
INSERT ALL
INTO MyTable (Year, Apple, Orange) VALUES (1, 100, 150)
INTO MyTable (Year, Apple, Orange) VALUES (2, 200, 250)
INTO MyTable (Year, Apple, Orange) VALUES (3, 300, 350)
INTO MyTable (Year, Apple, Orange) VALUES (2, 200, 250)
INTO MyTable (Year, Apple, Orange) VALUES (1, 100, 150)
SELECT * FROM dual;
Run this in SQL Developer or SQLPlus (I tried in SQL Developer).
Or you can encapsulate it in a procedure and can return the result.
SET ServerOutput ON size 100000;
variable rc refcursor;
DECLARE
v_column_list varchar2 (2000);
v_years varchar2(2000);
BEGIN
SELECT listagg('"' || column_name || '"', ',') within
GROUP (ORDER BY column_id)
INTO v_column_list
FROM all_tab_columns
WHERE table_name = 'MYTABLE'
AND column_name <> 'YEAR';
SELECT listagg(year, ',') within
GROUP (ORDER BY year)
INTO v_years
FROM (
SELECT DISTINCT year
FROM MyTable);
-- dbms_output.put_line(' v_column_list =' || v_column_list);
-- dbms_output.put_line(' v_years =' || v_years);
OPEN :rc FOR
'SELECT * FROM
( SELECT *
FROM MyTable
UNPIVOT ( val for fruit in ( ' || v_column_list || ' )
)
)
PIVOT ( sum ( val ) for year in ( ' || v_years || ' ) )';
END;
/
PRINT :rc
Output:
------------------------------------------------------------------------
FRUIT 1 2 3
------ ---------------------- ---------------------- ----------------------
ORANGE 300 500 350
APPLE 200 400 300