PL/SQL Oracle - convert rows into fields dynamically - sql

I already checked same scenario like this but unfortunately still don't get it :( , I tried some code but still failed to work
convert rows into fields dynamically (fields are depends on number of rows and no. of rows are depends on TYPE)
Table
TYPE |ATTR_NAME | ATTR_VALUE
**sample flat rate |Activity | N
**sample flat rate |code | PLAN_999
**sample flat rate |codes object | object
**sample low rate |Activity | Y
**sample low rate |code | PLAN_1299
**sample low rate |codes object | charge
**sample low rate |indicator | 0
Codes:
declare
sqlqry VARCHAR2(4001);
cols VARCHAR2(4001);
begin
select listagg('''' || ATTR_NAME || ''' as "' || ATTR_NAME || '"', ',') within group (order by ATTR_NAME)
into cols
from (select distinct ATTR_NAME from temp_2);
sqlqry :=
'select * from
(
select *
from temp_2
)
pivot
(
MIN(ATTR_VALUE) for ATTR_NAME in (' || cols || ')
)'
;

With a table ilke the following:
create table temp_2 (TYPE, ATTR_NAME, ATTR_VALUE) as (
select 'sample flat rate', 'Activity' , 'N' from dual union all
select 'sample flat rate', 'code' , 'PLAN_999' from dual union all
select 'sample flat rate', 'codes object', 'object' from dual union all
select 'sample low rate', 'Activity' , 'Y' from dual union all
select 'sample low rate', 'code' , 'PLAN_1299' from dual union all
select 'sample low rate', 'codes object', 'charge' from dual union all
select 'sample low rate', 'indicator' , '0' from dual
)
this is a working example:
SQL> variable x refcursor
SQL>
SQL> declare
2 sqlqry VARCHAR2(4001);
3 cols VARCHAR2(4001);
4 begin
5 select listagg('''' || ATTR_NAME || ''' as "' || ATTR_NAME || '"', ',') within group (order by ATTR_NAME)
6 into cols
7 from (select distinct ATTR_NAME from temp_2);
8 sqlqry :=
9 'select * from
10 (
11 select *
12 from temp_2
13 )
14 pivot
15 (
16 MIN(ATTR_VALUE) for ATTR_NAME in (' || cols || ')
17 )';
18 open :x for sqlqry;
19 end;
20 /
PL/SQL procedure successfully completed.
SQL> print :x
TYPE Activity code codes obj indicator
---------------- --------- --------- --------- ---------
sample low rate Y PLAN_1299 charge 0
sample flat rate N PLAN_999 object
SQL>

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

How to convert vertical string into horizontal in Oracle

O
N
K
A
R
how to convert it into ONKAR. reverse of it I know. But this I am not able to solve.
You can't do what you want generally without also having a second column which provides the ordering for each letter. Assuming you do have a column for the position, we can try:
SELECT LISTAGG(letter, '') WITHIN GROUP (ORDER BY position) word
FROM yourTable;
Demo
Data:
letter | position
O | 1
N | 2
K | 3
A | 4
R | 5
Listagg is right solution for strings up to 4000 bytes cuz it returns varchar2 data type. But for longer strings you may get clob data type.
with s (letter, position) as (
select 'O', 1 from dual union all
select 'N', 2 from dual union all
select 'K', 3 from dual union all
select 'A', 4 from dual union all
select 'R', 5 from dual)
select xmlcast(xmlagg(xmlelement(x, letter) order by position) as clob) c
from s;
C
---------------
ONKAR
You can use this as long as data result has all rows that you want to stick together.
with data as (select 'O' as letter from dual
union all
select 'N' from dual
union all
select 'K' from dual
union all
select 'A' from dual
union all
select 'R' from dual)
SELECT LISTAGG(letter, '') WITHIN GROUP (ORDER BY rownum)
FROM data;
If your data is in a single row separated by newline (ASCII 13) characters then you can just use REPLACE( value, CHR(13) ):
Oracle Setup:
CREATE TABLE test_data ( value ) AS
SELECT 'O' || CHR(13) || 'N' || CHR(13) || 'K' || CHR(13) || 'A' || CHR(13) || 'R' FROM DUAL
Query:
SELECT value, REPLACE( value, CHR(13) ) FROM test_data
Output:
VALUE | REPLACE(VALUE,CHR(13))
:-------- | :---------------------
O | ONKAR
N |
K |
A |
R |
db<>fiddle here

In oracle can we create table having column name from another table column value name?

Is there any way in oracle pl/sql to have a new table created which has column name coming from other table column data value.
For Example:
TableA
column1
-----------
A
B
C
D
out of this a new table comes out as
TABLE2
A B C D
- - - -
Where A,B,C,D act as column name for Table2
Thanks in advance.
Yes, use PL/SQL to build a dynamic query and then use EXECUTE IMMEDIATE:
Oracle Setup:
CREATE TABLE TableA ( column1 ) AS
SELECT 'A' FROM DUAL UNION ALL
SELECT 'B' FROM DUAL UNION ALL
SELECT 'C' FROM DUAL UNION ALL
SELECT 'D' FROM DUAL;
PL/SQL:
DECLARE
p_sql VARCHAR2(4000);
BEGIN
SELECT 'CREATE TABLE TableB ('
|| LISTAGG(
column1 || ' NUMBER',
','
) WITHIN GROUP ( ORDER BY ROWNUM )
|| ')'
INTO p_sql
FROM TableA;
EXECUTE IMMEDIATE p_sql;
END;
/
Output:
SELECT * FROM TableB;
A | B | C | D
-: | -: | -: | -:
db<>fiddle here

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

Split comma separated values of a column in row, through Oracle SQL query

I have a table like below:
-------------
ID | NAME
-------------
1001 | A,B,C
1002 | D,E,F
1003 | C,E,G
-------------
I want these values to be displayed as:
-------------
ID | NAME
-------------
1001 | A
1001 | B
1001 | C
1002 | D
1002 | E
1002 | F
1003 | C
1003 | E
1003 | G
-------------
I tried doing:
select split('A,B,C,D,E,F', ',') from dual; -- WILL RETURN COLLECTION
select column_value
from table (select split('A,B,C,D,E,F', ',') from dual); -- RETURN COLUMN_VALUE
Try using below query:
WITH T AS (SELECT 'A,B,C,D,E,F' STR FROM DUAL) SELECT
REGEXP_SUBSTR (STR, '[^,]+', 1, LEVEL) SPLIT_VALUES FROM T
CONNECT BY LEVEL <= (SELECT LENGTH (REPLACE (STR, ',', NULL)) FROM T)
Below Query with ID:
WITH TAB AS
(SELECT '1001' ID, 'A,B,C,D,E,F' STR FROM DUAL
)
SELECT ID,
REGEXP_SUBSTR (STR, '[^,]+', 1, LEVEL) SPLIT_VALUES FROM TAB
CONNECT BY LEVEL <= (SELECT LENGTH (REPLACE (STR, ',', NULL)) FROM TAB);
EDIT:
Try using below query for multiple IDs and multiple separation:
WITH TAB AS
(SELECT '1001' ID, 'A,B,C,D,E,F' STR FROM DUAL
UNION
SELECT '1002' ID, 'D,E,F' STR FROM DUAL
UNION
SELECT '1003' ID, 'C,E,G' STR FROM DUAL
)
select id, substr(STR, instr(STR, ',', 1, lvl) + 1, instr(STR, ',', 1, lvl + 1) - instr(STR, ',', 1, lvl) - 1) name
from
( select ',' || STR || ',' as STR, id from TAB ),
( select level as lvl from dual connect by level <= 100 )
where lvl <= length(STR) - length(replace(STR, ',')) - 1
order by ID, NAME
There are multiple options. See Split comma delimited strings in a table in Oracle.
Using REGEXP_SUBSTR:
SQL> WITH sample_data AS(
2 SELECT 10001 ID, 'A,B,C' str FROM dual UNION ALL
3 SELECT 10002 ID, 'D,E,F' str FROM dual UNION ALL
4 SELECT 10003 ID, 'C,E,G' str FROM dual
5 )
6 -- end of sample_data mimicking real table
7 SELECT distinct id, trim(regexp_substr(str, '[^,]+', 1, LEVEL)) str
8 FROM sample_data
9 CONNECT BY LEVEL <= regexp_count(str, ',')+1
10 ORDER BY ID
11 /
ID STR
---------- -----
10001 A
10001 B
10001 C
10002 D
10002 E
10002 F
10003 C
10003 E
10003 G
9 rows selected.
SQL>
Using XMLTABLE:
SQL> WITH sample_data AS(
2 SELECT 10001 ID, 'A,B,C' str FROM dual UNION ALL
3 SELECT 10002 ID, 'D,E,F' str FROM dual UNION ALL
4 SELECT 10003 ID, 'C,E,G' str FROM dual
5 )
6 -- end of sample_data mimicking real table
7 SELECT id,
8 trim(COLUMN_VALUE) str
9 FROM sample_data,
10 xmltable(('"'
11 || REPLACE(str, ',', '","')
12 || '"'))
13 /
ID STR
---------- ---
10001 A
10001 B
10001 C
10002 D
10002 E
10002 F
10003 C
10003 E
10003 G
9 rows selected.
i solved similar problem this way...
select YT.ID,
REPLACE(REGEXP_SUBSTR(','||YT.STR||',',',.*?,',1,lvl.lvl),',','') AS STR
from YOURTABLE YT
join (select level as lvl
from dual
connect by level <= (select max(regexp_count(STR,',')+1) from YOURTABLE)
) lvl on lvl.lvl <= regexp_count(YT.STR,',')+1
You may try something like this:
CREATE OR REPLACE TYPE "STR_TABLE"
as table of varchar2
create or replace function GetCollection( iStr varchar2, iSplit char default ',' ) return STR_TABLE as
pStr varchar2(4000) := trim(iStr);
rpart varchar(255);
pColl STR_TABLE := STR_TABLE();
begin
while nvl(length(pStr),0) > 0 loop
pos := inStr(pStr, iSplit );
if pos > 0 then
rpart := substr(pStr,1, pos-1);
pStr := substr(pStr,pos+1,length(pStr));
else
rpart := pStr;
pStr := null;
end if;
if rpart is not null then
pColl.Extend;
pColl(pColl.Count) := rpart;
end if;
end loop;
return pColl;
end;
Do not use CONNECT BY or REGEXP which results in a Cartesian product on a complex query. Furthermore the above solutions expect you know the possible results (A,B,C,D,E,F) rather than a list of combinations
Use XMLTable:
SELECT c.fname, c.lname,
trim(COLUMN_VALUE) EMAIL_ADDRESS
FROM
CONTACTS c, CONTACT_STATUS s,
xmltable(('"'
|| REPLACE(EMAIL_ADDRESS, ';', '","')
|| '"'))
where c.status = s.id
The COLUMN_VALUE is a pseudocolumn that belongs to xmltable. This is quick and correct and allows you to reference a column w/o know its values.
This takes the column and makes a table of values "item","item2","item3" and automatically joins to its source table (CONTACTS). This was tested on thousands of rows
Note the ';' in the xmltable is the separator in the column field.
I tried the solution of Lalit Kumar B and it worked so far. But with more data I ran into an performance issue (> 60 Rows, >7 Level). Therefore I used a more static variation, I would like to share as alternative.
WITH T AS (
SELECT 1001 AS ID, 'A,B,C' AS NAME FROM DUAL
UNION SELECT 1002 AS ID, 'D,E,F' AS NAME FROM DUAL
UNION SELECT 1003 AS ID, 'C,E,G' AS NAME FROM DUAL
) --SELECT * FROM T
SELECT ID as ID,
distinct_column AS NAME
FROM ( SELECT t.ID,
trim(regexp_substr(t.NAME, '[^,]+', 1,1)) AS c1,
trim(regexp_substr(t.NAME, '[^,]+', 1,2)) AS c2,
trim(regexp_substr(t.NAME, '[^,]+', 1,3)) AS c3,
trim(regexp_substr(t.NAME, '[^,]+', 1,4)) AS c4 -- etc.
FROM T )
UNPIVOT ( distinct_column FOR cn IN ( c1, c2, c3, c4 ) )
ID NAME
------ ------
1001 A
1001 B
1001 C
1002 D
1002 E
1002 F
1003 C
1003 E
1003 G
9 Zeilen gewählt
this version works also with strings longer than one char:
select regexp_substr('A,B,C,Karl-Heinz,D','[^,]+', 1, level) from dual
connect by regexp_substr('A,B,C,Karl-Heinz,D', '[^,]+', 1, level) is not null;
see How to split comma separated string and pass to IN clause of select statement