How to include column names along with rows dynamically in Oracle - oracle

I am trying to insert the column header and corresponding field into another table .
Table1 :
col1 col2 col3 col4
1 2 3 4
output should look like this:
COL_A COL_B COL_C COL_D COL_E COL_F COL_G COL_H
col1 1 col2 2 col3 3 col4 4
Table1 will be created dynamically , so structure of Table 1 might vary ,for eg .
sometime ,It might be
Case1:
col1 col2 col3 col4
1 2 3 4
For Case1 ,output should look like this:
COL_A COL_B COL_C COL_D COL_E COL_F COL_G COL_H
col1 1 col2 2 col3 3 col4 4
Or it might be
Case2:
col1 col2
1 2
For Case2 ,output should look like this:
COL_A COL_B COL_C COL_D
col1 1 col2 2
I got help with the static query for similar requirement in another question in Stack Overflow,but I was trying to convert the static query to dynamic query.
Static Query :
Static Table structure :
CREATE TABLE Table1
(col1 int, col2 int, col3 int, col4 int)
;
INSERT ALL
INTO Table1 (col1, col2, col3, col4)
VALUES (1, 2, 3, 4)
SELECT * FROM dual
;
Static Query :
SELECT * FROM
(
SELECT *
FROM Table1
UNPIVOT(val FOR col IN (
COL1
,COL2
,COL3
,COL4
))
)
PIVOT( MAX(COl) as C, MAX(VAL) as V FOR COL IN (
'COL1' as VAL1
,'COL2' as VAL2
,'COL3' as VAL3
,'COL4' as VAL4
));
Output :
VAL1_C VAL1_V VAL2_C VAL2_V VAL3_C VAL3_V VAL4_C VAL4_V
COL1 1 COL2 2 COL3 3 COL4 4
As I tried to make the static query dynamic ,I tried this :
SELECT * FROM
(
SELECT *
FROM Table1
UNPIVOT(val FOR col IN (
select regexp_substr((select * from (SELECT LISTAGG(COLUMN_NAME, ',') WITHIN GROUP ( order by COLUMN_ID) aa
FROM (select * from all_tab_columns where table_name = 'TABLE1'))),'[^,]+', 1, level) "yyy" from dual
connect by regexp_substr((select * from (SELECT LISTAGG(COLUMN_NAME, ',') WITHIN GROUP ( order by COLUMN_ID) aa
FROM (select * from all_tab_columns where table_name = 'TABLE1'))), '[^,]+', 1, level) is not null
))
)
PIVOT( MAX(COl) as C, MAX(VAL) as V FOR COL IN (
select regexp_substr((select '''' || AA ||'''' from (SELECT LISTAGG(COLUMN_NAME, ''',''') WITHIN GROUP ( order by COLUMN_ID) aa
FROM (select * from all_tab_columns where table_name = 'TABLE1'))),'[^,]+', 1, level) "yyy" from dual
connect by regexp_substr((select '''' || AA ||'''' from (SELECT LISTAGG(COLUMN_NAME, ''',''') WITHIN GROUP ( order by COLUMN_ID) aa
FROM (select * from all_tab_columns where table_name = 'TABLE1'))), '[^,]+', 1, level) is not null
));
But this throws error :
ORA-00904: : invalid identifier
00904. 00000 - "%s: invalid identifier"
*Cause:
*Action:
Error at Line: 6 Column: 5
I tried using dynamic sql .Below is the dynamic sql I used :
declare
sql_stmt varchar2(4000);
pivot_clause1 varchar2(4000);
pivot_clause2 varchar2(4000);
begin
SELECT LISTAGG(COLUMN_NAME, ',') WITHIN GROUP ( order by COLUMN_ID) aa
into pivot_clause1
FROM (select * from all_tab_columns where table_name = 'TEST11') ;
select * into pivot_clause2 from
(
select AA ||'''' aa
from (SELECT ''''||LISTAGG(COLUMN_NAME, ''',''') WITHIN GROUP ( order by COLUMN_ID) aa
FROM (select * from all_tab_columns where table_name = 'TEST11'))
);
dbms_output.put_line(pivot_clause1);
dbms_output.put_line(pivot_clause2);
sql_stmt := 'create table test13 as ('|| 'SELECT * FROM
(
SELECT *
FROM TEST11
UNPIVOT(val FOR col IN ( '
|| pivot_clause1 || '
))
)
PIVOT( MAX(COl) as C, MAX(VAL) as V FOR COL IN (' ||
pivot_clause2 || '
))
)';
execute immediate sql_stmt ;
end;
/
It is working fine for a table having description as below :
Name Null Type
---- ---- ----------
COL1 NUMBER(38)
COL2 NUMBER(38)
COL3 NUMBER(38)
COL4 NUMBER(38)
But it is not working for a table having description :
COL1 DATE
COL1 VARCHAR2(100)
COL1 VARCHAR2(100)
COL1 NUMBER
COL1 NUMBER
I am getting below error :
Error report -
ORA-01790: expression must have same datatype as corresponding expression
ORA-06512: at line 38
01790. 00000 - "expression must have same datatype as corresponding expression"
*Cause:
*Action:
I just learned unpivot cannot be used with columns having different datatype,Is there any way around ?
Can someone please help !!

Related

get min and max from subquery

I'm trying to get min and max values from query
SELECT TABLE_NAME , COLUMN_NAME
FROM ALL_TAB_COLUMNS
WHERE TABLE_NAME IN ('TABLE_A','TABLE_B')
and DATA_TYPE='NUMBER'
AND (DATA_PRECISION IS NULL OR DATA_SCALE IS NULL)
here what I get so far, but it shows nothing:
BEGIN DBMS_OUTPUT.ENABLE (buffer_size => NULL); END;
declare
l_max number;
begin
for "CUR_R" in
(SELECT TABLE_NAME , COLUMN_NAME
FROM ALL_TAB_COLUMNS
WHERE TABLE_NAME IN ('TABLE_A','TABLE_B')
and DATA_TYPE='NUMBER'
AND (DATA_PRECISION IS NULL OR DATA_SCALE IS NULL)
)
loop
execute immediate 'select max(' || "CUR_R"."COLUMN_NAME" ||') from ' || "CUR_R"."TABLE_NAME" into l_max;
dbms_output.put_line("CUR_R"."TABLE_NAME" ||'.'|| "CUR_R"."COLUMN_NAME" ||' -> max value = '|| l_max);
end loop;
end;
maybe i missing something?
also, I'm not an admin, just have grants to select to particular tables
can't create procedure or temp table
I expect result of this structure:
owner
column_name
max_value
min_value
maybe I am missing something?
also, I'm not an admin, just have grants to select to particular tables
can't create procedure or temp table
Dynamic SQL Approach
This query produces the query that gets the minand max for each column (line by line).
The simplest approach for ad Hoc purposed is to run the query, copy the result and execute it. You may of course fetch the result in a CLOBvariable and execute immediate it for repeatable tasks.
SELECT
'select '''||TABLE_NAME||''' TABLE_NAME ,''' || COLUMN_NAME||''' COLUMN_NAME,'|| ' max('|| COLUMN_NAME ||
') max_value,' || ' min('|| COLUMN_NAME ||') min_value from '|| TABLE_NAME ||
case when row_number() over (order by table_name desc, column_name desc) != 1 then ' UNION ALL' end as sql_text
FROM ALL_TAB_COLUMNS
WHERE TABLE_NAME IN ('TABLE_A','TABLE_B')
and DATA_TYPE='NUMBER'
AND (DATA_PRECISION IS NULL OR DATA_SCALE IS NULL)
order by table_name, column_name;
Result
select 'TABLE_A' TABLE_NAME ,'X' COLUMN_NAME, max(X) max_value, min(X) min_value from TABLE_A UNION ALL
select 'TABLE_A' TABLE_NAME ,'Y' COLUMN_NAME, max(Y) max_value, min(Y) min_value from TABLE_A UNION ALL
select 'TABLE_B' TABLE_NAME ,'X' COLUMN_NAME, max(X) max_value, min(X) min_value from TABLE_B UNION ALL
select 'TABLE_B' TABLE_NAME ,'Y' COLUMN_NAME, max(Y) max_value, min(Y) min_value from TABLE_B
I guess you should remove (comment) precision and scale conditions in cursor's query.
I used user_tab_columns and tables I have in my schema, but ... that's it, more or less:
SQL> SET SERVEROUTPUT ON
SQL>
SQL> DECLARE
2 l_max NUMBER;
3 BEGIN
4 FOR cur_r IN (SELECT table_name, column_name
5 FROM user_tab_columns
6 WHERE table_name IN ('EMP', 'DEPT')
7 AND data_type = 'NUMBER'
8 -- AND ( data_precision IS NULL
9 -- OR data_scale IS NULL)
10 AND 1 = 1)
11 LOOP
12 EXECUTE IMMEDIATE 'select max('
13 || cur_r.column_name
14 || ') from '
15 || cur_r.table_name
16 INTO l_max;
17
18 DBMS_OUTPUT.put_line (
19 cur_r.table_name
20 || '.'
21 || cur_r.column_name
22 || ' -> max value = '
23 || l_max);
24 END LOOP;
25 END;
26 /
DEPT.DEPTNO -> max value = 40
EMP.EMPNO -> max value = 7934
EMP.MGR -> max value = 7902
EMP.SAL -> max value = 5000
EMP.COMM -> max value = 1400
EMP.DEPTNO -> max value = 30
PL/SQL procedure successfully completed.
SQL>
Your query will only return column values where the data type is NUMBER or NUMBER(*,X). If the data type is NUMBER(X) or NUMBER(X,Y) then the filter on DATA_PRECISION IS NULL OR DATA_SCALE IS NULL will exclude those columns.
For example, given the sample data:
CREATE TABLE table_a (
value1 NUMBER,
value2 NUMBER(*,0),
value3 NUMBER(10),
value4 NUMBER(5,2)
);
INSERT INTO table_a
SELECT LEVEL, 2*LEVEL, 3*LEVEL, 4*LEVEL FROM DUAL CONNECT BY LEVEL <= 5;
CREATE TABLE table_b (
value1 NUMBER,
value2 NUMBER(*,0),
value3 NUMBER(10),
value4 NUMBER(5,2)
);
INSERT INTO table_b
SELECT LEVEL + 5, 2*LEVEL - 2, 3*LEVEL + 3, 1/LEVEL FROM DUAL CONNECT BY LEVEL <= 4;
Then:
DECLARE
l_min number;
l_max number;
BEGIN
DBMS_OUTPUT.ENABLE();
FOR c IN (
SELECT OWNER,
TABLE_NAME,
COLUMN_NAME
FROM ALL_TAB_COLUMNS
WHERE TABLE_NAME IN ('TABLE_A','TABLE_B')
AND DATA_TYPE='NUMBER'
AND (DATA_PRECISION IS NULL OR DATA_SCALE IS NULL)
)
LOOP
EXECUTE IMMEDIATE
'select min("' || c.column_name ||'"),
max("' || c.column_name ||'")
from "' || c.owner || '"."' || c.table_name || '"'
INTO l_min, l_max;
DBMS_OUTPUT.PUT_LINE(
c.owner || '.' || c.table_name ||'.'|| c.column_name
|| ' -> min_value = ' || l_min
|| ', max value = '|| l_max
);
END LOOP;
END;
/
Outputs:
FIDDLE_TNHCDFVJDASVYWAHROPU.TABLE_A.VALUE1 -> min_value = 1, max value = 5
FIDDLE_TNHCDFVJDASVYWAHROPU.TABLE_A.VALUE2 -> min_value = 2, max value = 10
FIDDLE_TNHCDFVJDASVYWAHROPU.TABLE_B.VALUE1 -> min_value = 6, max value = 9
FIDDLE_TNHCDFVJDASVYWAHROPU.TABLE_B.VALUE2 -> min_value = 0, max value = 6
The columns value3 and value4 are excluded as they have both scale and precision specified in their data type.
fiddle
why not query like:
select max(x), min(x)
from t

Dynamic SQL in Informatica

I have a mapping that is executing dynamic sql via a SQL transformation. The dynamic sql is stored in an Oracle table and I need to add a string parameter to this query.
In this example query, the values compared to COL4 and COL5 are output correctly in the log, but the value compared to COL6 is output as '$$STRING_PARA' in the query -
'SELECT COL1, COL2, COL3 FROM TABLE
WHERE COL4 = $$NUMERIC_PARA
AND COL5 = ''Y''
AND COL6 = ''||$$STRING_PARA||'' '
$$STRING_PARA is being output correctly at the beginning of the log. I have tried omitting the pipes and increasing the number of quotes but nothing seems to work.
Has anyone done something similar?
I guess you want to run the query by replacing the Informatica input variables with the values of the mapping. It might work like this
' SELECT COL1, COL2, COL3 FROM TABLE
WHERE COL4 = $$NUMERIC_PARA
AND COL5 = ''Y''
AND COL6 = ''$$STRING_PARA'' '
In PL/SQL
SQL> set serveroutput on size unlimited echo on lines 200
SQL> #test.sql
SQL> declare
2 v_query varchar2(4000) := ' SELECT COL1, COL2, COL3 FROM TABLE
3 WHERE COL4 = $$NUMERIC_PARA
4 AND COL5 = ''Y''
5 AND COL6 = ''$$STRING_PARA'' ' ;
6 begin
7 dbms_output.put_line(v_query);
8 end;
9 /
SELECT COL1, COL2, COL3 FROM TABLE
WHERE COL4 = $$NUMERIC_PARA
AND COL5 = 'Y'
AND COL6 = '$$STRING_PARA'
PL/SQL procedure successfully completed.
Runtime replacement and execution by replacing input variables
SQL> select object_id, object_name from dba_objects where object_name = 'MY_TEST' ;
OBJECT_ID OBJECT_NAME
---------- --------------------------------------------------------------------------------------------------------------------------------
8897475 MY_TEST
SQL> #test.sql 8897475 MY_TEST
SQL> declare
2 v_query varchar2(4000) := ' SELECT count(*) from dba_objects
3 WHERE object_id = &1
4 AND object_name = ''&2'' ' ;
5 v_counter pls_integer;
6 begin
7 dbms_output.put_line(v_query);
8 execute immediate v_query into v_counter;
9 dbms_output.put_line('Counter is '||v_counter||' ');
10 end;
11 /
old 3: WHERE object_id = &1
new 3: WHERE object_id = 8897475
old 4: AND object_name = ''&2'' ' ;
new 4: AND object_name = ''MY_TEST'' ' ;
SELECT count(*) from dba_objects
WHERE object_id = 8897475
AND object_name = 'MY_TEST'
Counter is 1
PL/SQL procedure successfully completed.
SQL>
It should work the same way, as the escaping for the single quote should be the same.
Thanks for the suggestions - ''$$STRING_PARA'' worked.
I couldn't see that at the time as there was another issue in the mapping that prevented it from working correctly.

How to write store procedure with object type as out parameter in Pl/SQL with multiple joins

I am trying to write a store procedure with below sample use case and tables,
Employee
Emp_id Emp_Name
1 Jhon
2 Mark
3 Marry
Department
Emp_Id Dept_Id
1 A
2 B
3 C
1 B
2 D
Assets
Emp_Id Asset_Name
1 AA
1 BB
2 CC
2 DD
4 EE
4 FF
Relationship
One employee can be added to more than one department.
e.g Emp 1 added to A and B Department.
One Employee Can have more than one Assets
e.g Emp 1 owing Assets AA and BB.
No Foreign key constraint between Employee And Assets.
So Assets can have EmpId which is not available in Employee Table.
e.g Emp 4
Desired output
EmployeeInfo
Emd_Id
Emp_Name
Array of Dept_Id[]
Array of Assets[]
Desired Result for employee Id 1
Emd_Id :1
Emp_Name :Jhon
Array of Dept_Id[] :[A,B]
Array of Assets[] :[AA,BB]
Desired Result for employee Id 4
Emd_Id :4
Emp_Name :null -- As no entry in Employee table.
Array of Dept_Id[] :null
Array of Assets[] :[EE,FF]
So want write a store procedure for this.Please suggest solution for this.
Either this can be achieved with multiple cursor or object type out variable?
Stored Procedure I tried as below,
CREATE OR REPLACE PROCEDURE PRC_TEST(
employeeInfo OUT SYS_REFCURSOR)
IS
BEGIN
OPEN employeeInfo FOR
SELECT e.EMP_ID ,e.EMP_NAME,
d.DEPT_ID,
a.ASSET_NAME
FROM EMPLOYEE e, DEPARTMENT d, ASSETS a
WHERE
e.EMP_ID = d.EMP_ID
AND e.EMP_ID = a.EMP_ID;
END;
Thanks in advance
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE Employee ( Emp_id, Emp_Name ) AS
SELECT 1, 'Jhon' FROM DUAL UNION ALL
SELECT 2, 'Mark' FROM DUAL UNION ALL
SELECT 3, 'Marry' FROM DUAL
/
CREATE TABLE Department ( Emp_Id, Dept_Id ) AS
SELECT 1, 'A' FROM DUAL UNION ALL
SELECT 2, 'B' FROM DUAL UNION ALL
SELECT 3, 'C' FROM DUAL UNION ALL
SELECT 1, 'B' FROM DUAL UNION ALL
SELECT 2, 'D' FROM DUAL
/
CREATE TABLE Assets ( Emp_Id, Asset_Name ) AS
SELECT 1, 'AA' FROM DUAL UNION ALL
SELECT 1, 'BB' FROM DUAL UNION ALL
SELECT 2, 'CC' FROM DUAL UNION ALL
SELECT 2, 'DD' FROM DUAL UNION ALL
SELECT 4, 'EE' FROM DUAL UNION ALL
SELECT 4, 'FF' FROM DUAL
/
CREATE TYPE StringLIst IS TABLE OF VARCHAR2(20)
/
CREATE TYPE Emp_Dept_Assets_Obj AS OBJECT(
Emp_id INTEGER,
Emp_Name VARCHAR2(50),
Depts StringList,
Assets StringList
)
/
CREATE FUNCTION get_Details(
i_emp_id IN Employee.EMP_ID%TYPE
) RETURN Emp_Dept_Assets_Obj
IS
o_details Emp_Dept_Assets_Obj;
BEGIN
o_details := Emp_Dept_Assets_Obj( i_emp_id, NULL, NULL, NULL );
BEGIN
SELECT Emp_Name
INTO o_details.Emp_Name
FROM Employee
WHERE emp_id = i_emp_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
NULL;
END;
SELECT dept_id
BULK COLLECT INTO o_details.Depts
FROM Department
WHERE emp_id = i_emp_id;
SELECT asset_name
BULK COLLECT INTO o_details.Assets
FROM Assets
WHERE emp_id = i_emp_id;
RETURN o_details;
END;
/
Query 1:
SELECT d.details.emp_id,
d.details.emp_name,
d.details.depts,
d.details.assets
FROM (
SELECT get_Details( LEVEL ) AS details
FROM DUAL
CONNECT BY LEVEL <= 4
) d
Results:
| DETAILS.EMP_ID | DETAILS.EMP_NAME | DETAILS.DEPTS | DETAILS.ASSETS |
|----------------|------------------|---------------|----------------|
| 1 | Jhon | A,B | AA,BB |
| 2 | Mark | B,D | CC,DD |
| 3 | Marry | C | |
| 4 | (null) | | EE,FF |
It is best practice to create a package definition and body. Your package definition will be something like below:
CREATE OR REPLACE PACKAGE EmployeeInfo AS
TYPE departament_type IS TABLE OF VARCHAR2(100);
TYPE assets_type IS TABLE OF VARCHAR2(100);
PROCEDURE get_employee_info ( Emp_Id_col IN OUT NUMBER
,Emp_Name_col OUT VARCHAR2
,departament_tbl OUT DEPARTAMENT_TYPE
,assets_tbl OUT ASSETS_TYPE);
END EmployeeInfo;
Then your package body will match your definition and it is where the procedure is going to be implemented:
CREATE OR REPLACE PACKAGE BODY EmployeeInfo AS
PROCEDURE get_employee_info ( Emp_Id_col IN OUT NUMBER
,Emp_Name_col OUT VARCHAR2
,departament_tbl OUT DEPARTAMENT_TYPE
,assets_tbl OUT ASSETS_TYPE)
IS
BEGIN
BEGIN
SELECT Emp_Name
INTO Emp_Name_col
FROM Employee
WHERE Emp_Id = Emp_Id_col;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN;
END;
departament_tbl := DEPARTAMENT_TYPE();
FOR dep_rec IN (SELECT *
FROM Department
WHERE Emp_id = Emp_Id_col) LOOP
departament_tbl.extend;
departament_tbl(departament_tbl.COUNT) := dep_rec.Dep_Id;
END LOOP;
assets_tbl := ASSETS_TYPE();
FOR asset_rec IN (SELECT *
FROM Assets
WHERE Emp_Id = Emp_Id_col) LOOP
assets_tbl.extend;
assets_tbl(assets_tbl.COUNT) := asset_rec.Asset_Name;
END LOOP;
END get_employee_info;
END EmployeeInfo;
Now here there is a very simple test script for your stored procedure:
DECLARE
Emp_Id_col NUMBER(10);
Emp_Name_col Employee.Emp_Name%TYPE;
departament_tbl EMPLOYEEINFO.DEPARTAMENT_TYPE;
assets_tbl EMPLOYEEINFO.ASSETS_TYPE;
BEGIN
Emp_Id_col := 1;
EMPLOYEEINFO.GET_EMPLOYEE_INFO(Emp_Id_col
,Emp_Name_col
,departament_tbl
,assets_tbl);
DBMS_OUTPUT.PUT_LINE(Emp_Name_col || ' - ' ||
departament_tbl.COUNT || ' - ' ||
assets_tbl.COUNT);
END;

How to Unpivot with dynamic columns Oracle

I need to unpiviot a table that I don't have control over the columns, so i need to dynamically get the column names: This is what I have
CREATE TABLE test
(
PK VARCHAR2(255 CHAR),
COL1 VARCHAR2(255 CHAR),
COL2 VARCHAR2(255 CHAR),
COL3 VARCHAR2(255 CHAR),
COL4 VARCHAR2(255 CHAR),
COL5 VARCHAR2(255 CHAR),
COL6 NUMBER,
)
declare
sql_stmt clob;
pivot_clause clob;
begin
select listagg('''' || column_name || ''' as "' || column_name || '"', ',') within group (order by column_name)
into pivot_clause
FROM USER_TAB_COLUMNS
WHERE table_name = 'test');
sql_stmt := 'SELECT PK,
VarName,
Valuer,
Max(timestamp) over (Partition by PK) as timestamp,
FROM test
UNPIVOT(Valuer FOR VarName IN (' || pivot_clause || '))';
execute immediate sql_stmt;
end;
Which returns:
Fehlerbericht -
ORA-00904: : invalid identifier
ORA-06512: at line 23
00904. 00000 - "%s: invalid identifier"
*Cause:
*Action:
Expected out put would be something like:
PK|VARNAME|Valuer
1 |Col1 | value1
1 |Col2 | value2
1 |Col3 | value3
1 |Col4 | value4
1 |Col5 | value5
1 |Col6 | 12345
2 |Col1 | value1
2 |Col2 | value2
2 |Col3 | value3
2 |Col4 | value4
2 |Col5 | value5
2 |Col6 | 12345
Which is the same error i get if i just toss the sub select right into the IN()
Thanks
You can use dbms_output.put_line(sql_stmt) to see the actual dynamic SQL being generated, which in this case is:
SELECT PK,
VarName,
Valuer,
Max(timestamp) over (Partition by PK) as timestamp,
FROM test
UNPIVOT(Valuer FOR VarName IN ('COL1' as "COL1",'COL2' as "COL2",'COL3' as "COL3",'COL4' as "COL4",'COL5' as "COL5",'COL6' as "COL6",'PK' as "PK",'TIMESTAMP' as "TIMESTAMP"))
Which has a number of issues. Generally for this sort of thing it's sensible to start from a static statement you know works and then figure out how to make it dynamic, but you don't seem to have done that here.
This version gets ORA-00936: missing expression because of the trailing comma after the timestamp in the dynamic statement. Which I imagine is another error from modifying your code for posting. Without that it gets the ORA-00904: : invalid identifier you have in your question. The immediate cause of that error you're getting is the parts in the parentheses, such as:
'COL1' as "COL1"
The quotes are wrong; that should be:
COL1 as 'COL1'
Fixing that then gives ORA-00904: "PK": invalid identifier because your column look-up isn't excluding the columns you don't want to pivot on, so really you want:
select listagg(column_name || ' as ''' || column_name || '''', ',')
within group (order by column_name)
into pivot_clause
from user_tab_columns
where table_name = 'TEST'
and column_name not in ('PK', 'TIMESTAMP');
Your next problem is that the columns you're unpivoting are different data types, so you get ORA-01790: expression must have same datatype as corresponding expression. That's a bit trickier - essentially you'll have to convert everything to strings, using suitable formats, particularly if there are dates involved. You need to do that conversion in a subquery, and you unpivot the result of that. An example that just explicitly handles the number column, generating the subquery columns in the same way as the unpivot clause:
declare
sql_stmt clob;
subquery clob;
pivot_clause clob;
begin
select listagg(column_name || ' as ''' || column_name || '''', ',')
within group (order by column_name),
listagg(
case when data_type = 'NUMBER' then 'to_char(' || column_name || ')'
else column_name
end || ' as ' || column_name, ',')
within group (order by column_name)
into pivot_clause, subquery
from user_tab_columns
where table_name = 'TEST'
and column_name not in ('PK', 'TIMESTAMP');
sql_stmt := 'select pk,
varname,
valuer,
max(timestamp) over (partition by pk) as timestamp
from (select pk, timestamp, ' || subquery || ' from test)
unpivot(valuer for varname in (' || pivot_clause || '))';
dbms_output.put_line(sql_stmt);
execute immediate sql_stmt;
end;
/
You could instead always cast every column to say varchar2(255) in the second listagg(), but then yuo have no direct control over formatting and are reliant on NLS settints.
When run the block above now generates:
select pk,
varname,
valuer,
max(timestamp) over (partition by pk) as timestamp
from (select pk, timestamp, COL1 as COL1,COL2 as COL2,COL3 as COL3,COL4 as COL4,COL5 as COL5,to_char(COL6) as COL6 from test)
unpivot(valuer for varname in (COL1 as 'COL1',COL2 as 'COL2',COL3 as 'COL3',COL4 as 'COL4',COL5 as 'COL5',COL6 as 'COL6'))
and when run manually that gets output like:
PK VARNAME VALUER TIMESTAMP
1 COL1 value1 11-AUG-17 15.49.42.239283000
1 COL2 value2 11-AUG-17 15.49.42.239283000
1 COL3 value3 11-AUG-17 15.49.42.239283000
1 COL4 value4 11-AUG-17 15.49.42.239283000
1 COL5 value5 11-AUG-17 15.49.42.239283000
1 COL6 12345 11-AUG-17 15.49.42.239283000
2 COL1 value6 11-AUG-17 15.49.42.340387000
2 COL2 value7 11-AUG-17 15.49.42.340387000
2 COL3 value8 11-AUG-17 15.49.42.340387000
2 COL4 value9 11-AUG-17 15.49.42.340387000
2 COL5 value10 11-AUG-17 15.49.42.340387000
2 COL6 23456 11-AUG-17 15.49.42.340387000
(I set up dummy data with different values in the unpivoted columns, and just used systimestamp to get the timestamp column value).
When you run it dynamically it doesn't really do anything - it's parsed but not fully executed because you aren't executing immediate into anything.
My plan was to use this in a view
You can make your anonymous block generate the view dynamically, as a one-off event:
sql_stmt := 'create or replace view your_view_name as
select pk,
varname,
valuer,
max(timestamp) over (partition by pk) as timestamp
from (select pk, timestamp, ' || subquery || ' from test)
unpivot(valuer for varname in (' || pivot_clause || '))';
execute immediate sql_stmt;
and you can then just select from your_view_name.
Whenever a new column is added to the table (which is hopefully rare and under change control - but then you wouldn't really need to do this dynamically) you can just re-run the block to recreate the view.

Output parameter value is shown as 'invalid identifier'

I am trying to retrieve values from temporarily created tables. But the return value throws the error 'Invalid Identifier'
create or replace procedure edu_stream (input in varchar2,vals out varchar2)
as
inp varchar2(30);
valu varchar2(30);
begin
inp:=input;
if inp='secondary education' then
Execute immediate'WITH secedu as (
(SELECT "ICSE" as name FROM dual ) UNION
(SELECT "CBSE" as name FROM dual ) UNION
(SELECT "STATE BOARD" as name FROM dual)
)
SELECT name into valu from(SELECT name
FROM secedu ORDER BY DBMS_RANDOM.RANDOM)where rownum<2';
vals:=valu;
else
if inp='intermediate education' then
Execute immediate'WITH intedu as (
(SELECT "MPC" as name FROM dual ) UNION
(SELECT "BIPC" as name FROM dual ) UNION
(SELECT "MBIPC" as name FROM dual) UNION
(SELECT "CEC" as name FROM dual)
)
SELECT name into valu from(SELECT name
FROM intedu ORDER BY DBMS_RANDOM.RANDOM)where rownum<2';
vals:=valu;
else
if inp='Graduation' then
Execute immediate'WITH gedu as (
(SELECT "ECE" as name FROM dual ) UNION
(SELECT "CSE" as name FROM dual ) UNION
(SELECT "CE" as name FROM dual) UNION
(SELECT "EEE" as name FROM dual)UNION
(SELECT "ME" as name FROM dual)UNION
(SELECT "AE" as name FROM dual)UNION
(SELECT "BIOTECH" as name FROM dual)UNION
(SELECT "EIE" as name FROM dual)
)
SELECT name into valu from(SELECT name
FROM gedu ORDER BY DBMS_RANDOM.RANDOM)where rownum<2';
vals:=valu;
else
if inp='post-graduation' then
Execute immediate'WITH pgedu as (
(SELECT "MCA" as name FROM dual ) UNION
(SELECT "MTECH" as name FROM dual ) UNION
(SELECT "MSC" as name FROM dual) UNION
(SELECT "MBA" as name FROM dual)
)
SELECT name into valu from(SELECT name
FROM pgedu ORDER BY DBMS_RANDOM.RANDOM)where rownum<2';
vals:=valu;
else
if inp='phd'then
Execute immediate' WITH phdedu as (
(SELECT "Doctorate of philosophy" as name FROM dual ) UNION
(SELECT "doctorate of medicine" as name FROM dual ) UNION
(SELECT "doctorate of science" as name FROM dual) UNION
(SELECT "Doctorate of computer sciences" as name FROM dual)
)
SELECT name into valu from(SELECT name
FROM phdgedu ORDER BY DBMS_RANDOM.RANDOM)where rownum<2';
vals:=valu;
end if;
end if;
end if;
end if;
end if;
end;
Execution:
declare
value1 varchar2(30);
cv varchar2(30);
begin
cv:='secondary education';
edu_stream(cv,value1);
dbms_output.put_line('val is'||value1);
end;
Error report:
Error starting at line 2 in command: declare value1 varchar2(30); cv
varchar2(30); begin cv:='secondary education'; edu_stream(cv,value1);
dbms_output.put_line('val is'||value1); end; Error report: ORA-00904:
"ICSE": invalid identifier ORA-06512: at "DATAFOCUS_GROUP.EDU_STREAM",
line 9 ORA-06512: at line 6
00904. 00000 - "%s: invalid identifier"
*Cause:
*Action:
if I use 'ICSE'instead of "ICSE"
ERROR SHOWS-
PLS-00103: Encountered the symbol "ICSE" when expecting one of the
following:
ERROR 103 * & = - + ; < / > at in is mod remainder not rem
return
returning <> or != or ~= >= <= <> and or
like like2 like4 likec between into using || multiset bulk
member submultiset
You can avoid dynamic SQL; also, probably due to confusion created by dynamic sql, you are using " instead of '.
You can re-write your code as:
CREATE OR REPLACE PROCEDURE edu_stream(input IN VARCHAR2, vals OUT VARCHAR2) AS
inp VARCHAR2(30);
valu VARCHAR2(30);
BEGIN
inp := input;
IF inp = 'secondary education'
THEN
WITH secedu AS
((SELECT 'ICSE' AS name FROM DUAL)
UNION
(SELECT 'CBSE' AS name FROM DUAL)
UNION
(SELECT 'STATE BOARD' AS name FROM DUAL))
SELECT name
INTO valu
FROM ( SELECT name
FROM secedu
ORDER BY DBMS_RANDOM.RANDOM)
WHERE ROWNUM < 2;
vals := valu;
ELSE
IF inp = 'intermediate education'
THEN
WITH intedu AS
((SELECT 'MPC' AS name FROM DUAL)
UNION
(SELECT 'BIPC' AS name FROM DUAL)
UNION
(SELECT 'MBIPC' AS name FROM DUAL)
UNION
(SELECT 'CEC' AS name FROM DUAL))
SELECT name
INTO valu
FROM ( SELECT name
FROM intedu
ORDER BY DBMS_RANDOM.RANDOM)
WHERE ROWNUM < 2;
vals := valu;
ELSE
IF inp = 'Graduation'
THEN
WITH gedu AS
((SELECT 'ECE' AS name FROM DUAL)
UNION
(SELECT 'CSE' AS name FROM DUAL)
UNION
(SELECT 'CE' AS name FROM DUAL)
UNION
(SELECT 'EEE' AS name FROM DUAL)
UNION
(SELECT 'ME' AS name FROM DUAL)
UNION
(SELECT 'AE' AS name FROM DUAL)
UNION
(SELECT 'BIOTECH' AS name FROM DUAL)
UNION
(SELECT 'EIE' AS name FROM DUAL))
SELECT name
INTO valu
FROM ( SELECT name
FROM gedu
ORDER BY DBMS_RANDOM.RANDOM)
WHERE ROWNUM < 2;
vals := valu;
ELSE
IF inp = 'post-graduation'
THEN
WITH pgedu AS
((SELECT 'MCA' AS name FROM DUAL)
UNION
(SELECT 'MTECH' AS name FROM DUAL)
UNION
(SELECT 'MSC' AS name FROM DUAL)
UNION
(SELECT 'MBA' AS name FROM DUAL))
SELECT name
INTO valu
FROM ( SELECT name
FROM pgedu
ORDER BY DBMS_RANDOM.RANDOM)
WHERE ROWNUM < 2;
vals := valu;
ELSE
IF inp = 'phd'
THEN
WITH phdedu AS
((SELECT 'Doctorate of philosophy' AS name FROM DUAL)
UNION
(SELECT 'doctorate of medicine' AS name FROM DUAL)
UNION
(SELECT 'doctorate of science' AS name FROM DUAL)
UNION
(SELECT 'Doctorate of computer sciences' AS name FROM DUAL))
SELECT name
INTO valu
FROM ( SELECT name
FROM phdgedu
ORDER BY DBMS_RANDOM.RANDOM)
WHERE ROWNUM < 2;
vals := valu;
END IF;
END IF;
END IF;
END IF;
END IF;
END;
Also, please notice that the variables inp and valu are not strictly necessary: you can simply use the parameters to check the input value and build the output parameter.
In case you need do use dynamic SQL (not here, but maybe in the future) the right way is something like:
declare
a number;
begin
execute immediate 'select 1 from dual' into a;
end;
As an alternative answer sticking with your dynamic SQL.
personal preference: i´d allways end a line with ' ||, to make it clear you are not missing a whitespace (even though it should just execute fine).
remove the into valu from the dynamic sql and make it a execute immediate 'query' into vals clause.
As an example i´m just using your first query:
Execute immediate 'WITH secedu as ( ' ||
'(SELECT ''ICSE'' as name FROM dual ) UNION ' ||
'(SELECT ''CBSE'' as name FROM dual ) UNION ' ||
'(SELECT ''STATE'' BOARD" as name FROM dual) ' ||
') ' ||
'SELECT name from(SELECT name ' ||
'FROM secedu ORDER BY DBMS_RANDOM.RANDOM)where rownum<2'
into vals;
You might have your own learning objectives but when those put aside the code is simply awful and unnecessary complex PL/SQL. Here is a partial but completely functional rewrite with references to relevant Oracle PL/SQL documentation. Hope you'll find the presented ideas useful !
-- blocks: http://docs.oracle.com/database/121/LNPLS/overview.htm#LNPLS141
declare
-- nested tables: http://docs.oracle.com/database/121/LNPLS/composites.htm#LNPLS99981
type str_list_t is table of varchar2(32767);
-- subprograms: http://docs.oracle.com/database/121/LNPLS/subprograms.htm#LNPLS008
-- common parts of f() refactored to r()
function r(p_list in str_list_t) return varchar2 is
begin
return p_list(floor(dbms_random.value(1, p_list.count + 1)));
end;
function f(p_edu_level in varchar2) return varchar2 is
begin
return
-- simple case: http://docs.oracle.com/database/121/LNPLS/controlstatements.htm#LNPLS394
case p_edu_level
when 'secondary' then r(str_list_t('ICSE', 'CBSE', 'STATE BOARD'))
when 'intermediate' then r(str_list_t('MPC', 'BIPC', 'MIPC', 'CEC'))
-- add here your other education levels, you should see the pattern ...
else null
end;
end;
begin
-- for loop: http://docs.oracle.com/database/121/LNPLS/controlstatements.htm#LNPLS411
for i in 1 .. 10
loop
dbms_output.put_line('secondary: ' || f('secondary'));
dbms_output.put_line('intermediate: ' || f('intermediate'));
end loop;
dbms_output.put_line('batman: ' || f('batman'));
end;
/

Resources