Re-write table update as set instead of iterative method - oracle

I have a list of user email addresses, in a test Oracle database, which are currently all set to the same value. I want to replace these with unique entries, with a number of invalid addresses and null values mixed in. My table currently looks like this and is around 250k rows in total.(I've excluded the null and invalid entries to save some space)
+-------------+--------------------+
| employee_id | email |
+-------------+--------------------+
| 1 | test#testemail.com |
| 2 | test#testemail.com |
| 3 | test#testemail.com |
|... |... |
+-------------+--------------------+
And I'd like it to look like this
+-------------+---------------------+
| employee_id | email |
+-------------+---------------------+
| 1 | test1#testemail.com |
| 2 | test2#testemail.com |
| 3 | test3#testemail.com |
|... |... |
+-------------+---------------------+
I've wrote the following PL/SQL to do the change, it works, but it's seems very inefficient. Can I use another method to take advantage of set processing?
Thanks for any help with this.
DECLARE
i number(20);
l_employee_id hr.employees.employee_id%TYPE;
output_query varchar2(1000);
CURSOR c_cursor IS
SELECT employee_id
FROM hr.employees;
PROCEDURE update_sql(id_num IN hr.employees.employee_id%TYPE,
email IN VARCHAR2) IS
BEGIN
output_query := 'UPDATE hr.employees
SET email = '''|| email ||'''
WHERE employee_id = '|| id_num;
dbms_output.put_line(output_query); --for debug
EXECUTE IMMEDIATE output_query;
END;
BEGIN
OPEN c_cursor;
i := 1;
<<outer_loop>>
LOOP
For j IN 1..5 LOOP
FETCH c_cursor INTO l_employee_id;
EXIT outer_loop WHEN c_cursor%NOTFOUND;
IF j <= 3 THEN
update_sql(l_employee_id, ('test' || i || '#testemail.com'));
ELSIF j = 4 THEN
update_sql(l_employee_id, ('test' || i || 'testemail.com'));
ELSIF j = 5 THEN
update_sql(l_employee_id, ' ');
END IF;
i := i + 1;
END LOOP;
END LOOP outer_loop;
CLOSE c_cursor;
END;
/
EDIT - 26/09/2016 - to clarify size of table.

create table emp (emp_id number, email varchar2(32));
insert into emp select level as emp_id, 'test#testemail.com' as email
from dual connect by level<=2500000;
update emp set email = regexp_replace(email, '(\w+)(#\w+\.\w+)', '\1' || emp_id || '\2');
--250,000 rows updated ~16 sec.
EMP_ID, EMAIL
1 test1#testemail.com
2 test2#testemail.com
3 test3#testemail.com
...
drop table emp;

First off, even if you were going to code this iteratively, please don't use dynamic SQL where it is not necessary. And it is only necessary if you don't know the table or columns you are going to be querying at compile time.
That said, it sounds like you just want
UPDATE employees
SET email = (case when employee_id <= 3
then 'test' || employee_id || '#testemail.com'
when employee_id = 4
then 'test' || employee_id || 'testemail.com'
when employee_id = 5
then ' '
else null
end)

Oracle Setup:
create table employees (
id NUMBER,
email VARCHAR2(100)
);
INSERT INTO employees
SELECT 1, 'test#testemail.com' FROM DUAL UNION ALL
SELECT 2, 'test#testemail.com' FROM DUAL UNION ALL
SELECT 3, 'test#testemail.com' FROM DUAL UNION ALL
SELECT 4, 'test#testemail.com' FROM DUAL UNION ALL
SELECT 5, 'test#testemail.com' FROM DUAL UNION ALL
SELECT 6, 'test#testemail.com' FROM DUAL;
Query:
update employees
set email = CASE MOD( ROWNUM, 5 )
WHEN 4 THEN 'test' || ROWNUM || 'testemail.com'
WHEN 0 THEN ''
ELSE 'test' || ROWNUM || '#testemail.com'
END;
Output:
SELECT * FROM employees;
ID EMAIL
---------- ------------------------
1 test1#testemail.com
2 test2#testemail.com
3 test3#testemail.com
4 test4testemail.com
5
6 test6#testemail.com

Related

How to get distinct items and number of occurrences on a nested table?

I have a nested table that includes a list of emails, which could be inside several times.
The problem is I need to get the distinct list of items, and the number of times it appears on the list.
| emails |
| ------------ |
| a#mail.com |
| b#mail.com |
| c#mail.com |
| d#mail.com |
| c#mail.com |
| c#mail.com |
| a#mail.com |
| a#mail.com |
| b#mail.com |
| b#mail.com |
| c#mail.com |
Ideally, my result would be a table or an output that tells me the following:
| Email | Number |
| ---------- | - |
| a#mail.com | 3 |
| b#mail.com | 3 |
| c#mail.com | 4 |
| d#mail.com | 1 |
To select from a table I would use a select statement, but if I try this in my code I get an error "ORA- 00942: table or view does not exist" same with even a simple select from emails table so I'm just guessing you can't use select on nested tables that way.
The nested table was created like this:
type t_email_type is table of varchar2(100);
t_emails t_email_type := t_email_type();
and then populated under a loop that adds an email for each iteration of the loop:
t_emails.extend;
t_emails(t_emails.LAST) := user_r.email;
I tried to do what you described so far; here you go:
Table that contains e-mail addresses (some of them are duplicates):
SQL> select * from test_mails;
EMAIL
----------
a#mail.com
b#mail.com
c#mail.com
a#mail.com
b#mail.com
Type you created; I think you used type within your PL/SQL procedure. That won't work if it is a function which is supposed to return result of that type because it then must be created at SQL level, so - that's what I'm doing:
SQL> create or replace type t_email_type is table of varchar2(100);
2 /
Type created.
Function: FOR loop selects e-mail addresses from the table and puts them into t_emails. What you're interested in is what follows in lines #12-14 as it shows how to return the result:
SQL> create or replace function f_test
2 return t_email_type
3 is
4 t_emails t_email_type := t_email_type();
5 retval t_email_type;
6 begin
7 for user_r in (select email from test_mails) loop
8 t_emails.extend;
9 t_emails(t_emails.last) := user_r.email;
10 end loop;
11
12 select distinct column_value
13 bulk collect into retval
14 from table(t_emails);
15 return retval;
16 end;
17 /
Function created.
OK, let's test it:
SQL> select * from table(f_test);
COLUMN_VALUE
--------------------------------------------------------------------------------
a#mail.com
b#mail.com
c#mail.com
SQL>
Distinct addresses; that's what you asked for.
Try it like this:
SET SERVEROUTPUT ON
Create or replace type t_email_type is table of varchar2(100);
/
Declare -- Outer block - just to populate variable emails of type t_email_type
Cursor c_emails IS
Select 'a#mail.com' "EMAIL" From Dual Union All
Select 'b#mail.com' "EMAIL" From Dual Union All
Select 'c#mail.com' "EMAIL" From Dual Union All
Select 'd#mail.com' "EMAIL" From Dual Union All
Select 'c#mail.com' "EMAIL" From Dual Union All
Select 'c#mail.com' "EMAIL" From Dual Union All
Select 'a#mail.com' "EMAIL" From Dual Union All
Select 'a#mail.com' "EMAIL" From Dual Union All
Select 'b#mail.com' "EMAIL" From Dual Union All
Select 'b#mail.com' "EMAIL" From Dual Union All
Select 'c#mail.com' "EMAIL" From Dual;
email VarChar2(100);
emails t_email_type := t_email_type();
i Number(3) := 0;
Begin
OPEN c_emails;
LOOP
FETCH c_emails Into email;
EXIT WHEN c_emails%NOTFOUND;
i := i + 1;
emails.extend;
emails(i) := email;
END LOOP;
CLOSE c_emails;
-- Inner block - get distinct emails and number of appearances from variable emails of type t_email_type
Declare
Cursor c Is
Select COLUMN_VALUE "EMAIL", Count(*) "NUM_OF_APPEARANCES"
From TABLE(emails)
Group By COLUMN_VALUE
Order By COLUMN_VALUE;
cSet c%ROWTYPE;
i Number(3) := 0;
Begin
OPEN c;
LOOP
FETCH c Into cSet;
EXIT WHEN c%NOTFOUND;
i := i + 1;
If i = 1 Then
DBMS_OUTPUT.PUT_LINE(RPAD('EMAIL', 20, ' ') || ' ' || LPAD('NUM_OF_APPEARANCES', 20, ' '));
DBMS_OUTPUT.PUT_LINE(RPAD('-', 20, '-') || ' ' || LPAD('-', 20, '-'));
End If;
DBMS_OUTPUT.PUT_LINE(RPAD(cSet.EMAIL, 20, ' ') || ' ' || LPAD(cSet.NUM_OF_APPEARANCES, 20, ' '));
END LOOP;
CLOSE c;
End;
End;
/* R e s u l t :
anonymous block completed
EMAIL NUM_OF_APPEARANCES
-------------------- --------------------
a#mail.com 3
b#mail.com 3
c#mail.com 4
d#mail.com 1
*/
Outer block of this code is here just to generate data and to insert it into emails variable that is of type you have defined (t_email_type). You, probably, need just the inner block to get you list of emails with number of appearances within the table type.
SQL that gives you expected result is, actualy, the cursor in the inner block:
Select COLUMN_VALUE "EMAIL", Count(*) "NUM_OF_APPEARANCES"
From TABLE(emails)
Group By COLUMN_VALUE
Order By COLUMN_VALUE;
Result:
EMAIL
NUM_OF_APPEARANCES
a#mail.com
3
b#mail.com
3
c#mail.com
4
d#mail.com
1
Regards...

Is this possible to apply a function to every fields of table (groupy by type)

I would like to replace every string are null by 'n' and every number by 0.
Is there a way to do that?
With a polymorphic table function I can select all the columns of a certain type but I can't modify the value of the columns.
Yes, it is possible with PTF also. You may modify columns in case you've set pass_through to false for the column in the describe method (to drop it) and copy it into new_columns parameter of the describe.
Below is the code:
create package pkg_nvl as
/*Package to implement PTF*/
function describe(
tab in out dbms_tf.table_t
) return dbms_tf.describe_t
;
procedure fetch_rows;
end pkg_nvl;
/
create package body pkg_nvl as
function describe(
tab in out dbms_tf.table_t
) return dbms_tf.describe_t
as
modif_cols dbms_tf.columns_new_t;
new_col_cnt pls_integer := 0;
begin
/*Mark input columns as used and as modifiable for subsequent row processing*/
for i in 1..tab.column.count loop
if tab.column(i).description.type in (
dbms_tf.type_number,
dbms_tf.type_varchar2
) then
/*Modifiable*/
tab.column(i).pass_through := FALSE;
/*Used in the PTF context*/
tab.column(i).for_read := TRUE;
/* Propagate column to the modified*/
modif_cols(new_col_cnt) := tab.column(i).description;
new_col_cnt := new_col_cnt + 1;
end if;
end loop;
/*Return the list of modified cols*/
return dbms_tf.describe_t(
new_columns => modif_cols
);
end;
procedure fetch_rows
/*Process rowset and replace nulls*/
as
rowset dbms_tf.row_set_t;
num_rows pls_integer;
in_col_vc2 dbms_tf.tab_varchar2_t;
in_col_num dbms_tf.tab_number_t;
new_col_vc2 dbms_tf.tab_varchar2_t;
new_col_num dbms_tf.tab_number_t;
begin
/*Get rows*/
dbms_tf.get_row_set(
rowset => rowset,
row_count => num_rows
);
for col_num in 1..rowset.count() loop
/*Loop through the columns*/
for rn in 1..num_rows loop
/*Calculate new values in the same row*/
/*Get column by index and nvl the value for return column*/
if rowset(col_num).description.type = dbms_tf.type_number then
dbms_tf.get_col(
columnid => col_num,
collection => in_col_num
);
new_col_num(rn) := nvl(in_col_num(rn), 0);
elsif rowset(col_num).description.type = dbms_tf.type_varchar2 then
dbms_tf.get_col(
columnid => col_num,
collection => in_col_vc2
);
new_col_vc2(rn) := nvl(in_col_vc2(rn), 'n');
end if;
end loop;
/*Put the modified column to the result*/
if rowset(col_num).description.type = dbms_tf.type_number then
dbms_tf.put_col(
columnid => col_num,
collection => new_col_num
);
elsif rowset(col_num).description.type = dbms_tf.type_varchar2 then
dbms_tf.put_col(
columnid => col_num,
collection => new_col_vc2
);
end if;
end loop;
end;
end pkg_nvl;
/
create function f_replace_nulls(tab in table)
/*Function to replace nulls using PTF*/
return table pipelined
row polymorphic using pkg_nvl;
/
with a as (
select
1 as id, 'q' as val_vc2, 1 as val_num
from dual
union all
select
2 as id, '' as val_vc2, null as val_num
from dual
union all
select
3 as id, ' ' as val_vc2, 0 as val_num
from dual
)
select
id
, a.val_num
, a.val_vc2
, n.val_num as val_num_repl
, n.val_vc2 as val_vc2_repl
from a
join f_replace_nulls(a) n
using(id)
ID | VAL_NUM | VAL_VC2 | VAL_NUM_REPL | VAL_VC2_REPL
-: | ------: | :------ | -----------: | :-----------
3 | 0 | | 0 |
2 | null | null | 0 | n
1 | 1 | q | 1 | q
db<>fiddle here
Use COALESCE or NVL and list the columns you want to apply them to:
SELECT COALESCE(col1, 'n') AS col1,
COALESCE(col2, 0) AS col2,
COALESCE(col3, 'n') AS col3
FROM table_name;
The way I understood the question, you actually want to modify table's contents. If that's so, you'll need dynamic SQL.
Here's an example; sample data first, with some numeric and character columns having NULL values:
SQL> desc test
Name Null? Type
----------------------------------------- -------- ----------------------------
ID NUMBER
NAME VARCHAR2(20)
SALARY NUMBER
SQL> select * from test order by id;
ID NAME SALARY
---------- ---------- ----------
1 Little 100
2 200
3 Foot 0
4 0
Procedure reads USER_TAB_COLUMNS, checking desired data types (you can add some more, if you want), composes the update statement and executes it:
SQL> declare
2 l_str varchar2(1000);
3 begin
4 for cur_r in (select column_name, data_type
5 from user_tab_columns
6 where table_name = 'TEST'
7 and data_type in ('CHAR', 'VARCHAR2')
8 )
9 loop
10 l_str := 'update test set ' ||
11 cur_r.column_name || ' = nvl(' || cur_r.column_name ||', ''n'')';
12 execute immediate l_str;
13 end loop;
14
15 --
16
17 for cur_r in (select column_name, data_type
18 from user_tab_columns
19 where table_name = 'TEST'
20 and data_type in ('NUMBER')
21 )
22 loop
23 l_str := 'update test set ' ||
24 cur_r.column_name || ' = nvl(' || cur_r.column_name ||', 0)';
25 execute immediate l_str;
26 end loop;
27 end;
28 /
PL/SQL procedure successfully completed.
Result:
SQL> select * from test order by id;
ID NAME SALARY
---------- ---------- ----------
1 Little 100
2 n 200
3 Foot 0
4 n 0
SQL>
You can create a table macro to intercept the columns and replace them with nvl if they're a number or varchar2:
create or replace function replace_nulls (
tab dbms_tf.table_t
)
return clob sql_macro as
stmt clob := 'select ';
begin
for i in 1..tab.column.count loop
if tab.column(i).description.type = dbms_tf.type_number
then
stmt := stmt || ' nvl ( ' ||
tab.column(i).description.name || ', 0 ) ' ||
tab.column(i).description.name || ',';
elsif tab.column(i).description.type = dbms_tf.type_varchar2
then
stmt := stmt || ' nvl ( ' ||
tab.column(i).description.name || ', ''n'' ) ' ||
tab.column(i).description.name || ',';
else
stmt := stmt || tab.column(i).description.name || ',';
end if;
end loop;
stmt := rtrim ( stmt, ',' ) || ' from tab';
return stmt;
end;
/
with a (
aa1,aa2,aa3
) as (
select 1, '2hhhh', sysdate from dual
union all
select null, null, null from dual
)
select * from replace_nulls(a);
AA1 AA2 AA3
---------- ----- -----------------
1 2hhhh 27-JUN-2022 13:17
0 n <null>

Columns into Rows - PIVOTING [duplicate]

... pivot (sum(A) for B in (X))
Now B is of datatype varchar2 and X is a string of varchar2 values separated by commas.
Values for X are select distinct values from a column(say CL) of same table. This way pivot query was working.
But the problem is that whenever there is a new value in column CL I have to manually add that to the string X.
I tried replacing X with select distinct values from CL. But query is not running.
The reason I felt was due to the fact that for replacing X we need values separated by commas.
Then i created a function to return exact output to match with string X. But query still doesn't run.
The error messages shown are like "missing righr parantheses", "end of file communication channel" etc etc.
I tried pivot xml instead of just pivot, the query runs but gives vlaues like oraxxx etc which are no values at all.
Maybe I am not using it properly.
Can you tell me some method to create a pivot with dynamic values?
You cannot put a dynamic statement in the PIVOT's IN statement without using PIVOT XML, which outputs some less than desirable output. However, you can create an IN string and input it into your statement.
First, here is my sample table;
myNumber myValue myLetter
---------- ---------- --------
1 2 A
1 4 B
2 6 C
2 8 A
2 10 B
3 12 C
3 14 A
First setup the string to use in your IN statement. Here you are putting the string into "str_in_statement". We are using COLUMN NEW_VALUE and LISTAGG to setup the string.
clear columns
COLUMN temp_in_statement new_value str_in_statement
SELECT DISTINCT
LISTAGG('''' || myLetter || ''' AS ' || myLetter,',')
WITHIN GROUP (ORDER BY myLetter) AS temp_in_statement
FROM (SELECT DISTINCT myLetter FROM myTable);
Your string will look like:
'A' AS A,'B' AS B,'C' AS C
Now use the String statement in your PIVOT query.
SELECT * FROM
(SELECT myNumber, myLetter, myValue FROM myTable)
PIVOT (Sum(myValue) AS val FOR myLetter IN (&str_in_statement));
Here is the Output:
MYNUMBER A_VAL B_VAL C_VAL
---------- ---------- ---------- ----------
1 2 4
2 8 10 6
3 14 12
There are limitations though. You can only concatenate a string up to 4000 bytes.
You can't put a non constant string in the IN clause of the pivot clause.
You can use Pivot XML for that.
From documentation:
subquery A subquery is used only in conjunction with the XML keyword.
When you specify a subquery, all values found by the subquery are used
for pivoting
It should look like this:
select xmlserialize(content t.B_XML) from t_aa
pivot xml(
sum(A) for B in(any)
) t;
You can also have a subquery instead of the ANY keyword:
select xmlserialize(content t.B_XML) from t_aa
pivot xml(
sum(A) for B in (select cl from t_bb)
) t;
Here is a sqlfiddle demo
For later readers, here is another solution
https://technology.amis.nl/2006/05/24/dynamic-sql-pivoting-stealing-antons-thunder/
allowing a query like
select * from table( pivot( 'select deptno, job, count(*) c from scott.emp group by deptno,job' ) )
I am not exactly going to give answer for the question OP has asked, instead I will be just describing how dynamic pivot can be done.
Here we have to use dynamic sql, by initially retrieving the column values into a variable and passing the variable inside dynamic sql.
EXAMPLE
Consider we have a table like below.
If we need to show the values in the column YR as column names and the values in those columns from QTY, then we can use the below code.
declare
sqlqry clob;
cols clob;
begin
select listagg('''' || YR || ''' as "' || YR || '"', ',') within group (order by YR)
into cols
from (select distinct YR from EMPLOYEE);
sqlqry :=
'
select * from
(
select *
from EMPLOYEE
)
pivot
(
MIN(QTY) for YR in (' || cols || ')
)';
execute immediate sqlqry;
end;
/
RESULT
If required, you can also create a temp table and do a select query in that temp table to see the results. Its simple, just add the CREATE TABLE TABLENAME AS in the above code.
sqlqry :=
'
CREATE TABLE TABLENAME AS
select * from
USE DYNAMIC QUERY
Test code is below
-- DDL for Table TMP_TEST
--------------------------------------------------------
CREATE TABLE "TMP_TEST"
( "NAME" VARCHAR2(20),
"APP" VARCHAR2(20)
);
/
SET DEFINE OFF;
Insert into TMP_TEST (NAME,APP) values ('suhaib','2');
Insert into TMP_TEST (NAME,APP) values ('suhaib','1');
Insert into TMP_TEST (NAME,APP) values ('shahzad','3');
Insert into TMP_TEST (NAME,APP) values ('shahzad','2');
Insert into TMP_TEST (NAME,APP) values ('shahzad','5');
Insert into TMP_TEST (NAME,APP) values ('tariq','1');
Insert into TMP_TEST (NAME,APP) values ('tariq','2');
Insert into TMP_TEST (NAME,APP) values ('tariq','6');
Insert into TMP_TEST (NAME,APP) values ('tariq','4');
/
CREATE TABLE "TMP_TESTAPP"
( "APP" VARCHAR2(20)
);
SET DEFINE OFF;
Insert into TMP_TESTAPP (APP) values ('1');
Insert into TMP_TESTAPP (APP) values ('2');
Insert into TMP_TESTAPP (APP) values ('3');
Insert into TMP_TESTAPP (APP) values ('4');
Insert into TMP_TESTAPP (APP) values ('5');
Insert into TMP_TESTAPP (APP) values ('6');
/
create or replace PROCEDURE temp_test(
pcursor out sys_refcursor,
PRESULT OUT VARCHAR2
)
AS
V_VALUES VARCHAR2(4000);
V_QUERY VARCHAR2(4000);
BEGIN
PRESULT := 'Nothing';
-- concating activities name using comma, replace "'" with "''" because we will use it in dynamic query so "'" can effect query.
SELECT DISTINCT
LISTAGG('''' || REPLACE(APP,'''','''''') || '''',',')
WITHIN GROUP (ORDER BY APP) AS temp_in_statement
INTO V_VALUES
FROM (SELECT DISTINCT APP
FROM TMP_TESTAPP);
-- designing dynamic query
V_QUERY := 'select *
from ( select NAME,APP
from TMP_TEST )
pivot (count(*) for APP in
(' ||V_VALUES|| '))
order by NAME' ;
OPEN PCURSOR
FOR V_QUERY;
PRESULT := 'Success';
Exception
WHEN OTHERS THEN
PRESULT := SQLcode || ' - ' || SQLERRM;
END temp_test;
I used the above method (Anton PL/SQL custom function pivot()) and it done the job! As I am not a professional Oracle developer, these are simple steps I've done:
1) Download the zip package to find pivotFun.sql in there.
2) Run once the pivotFun.sql to create a new function
3) Use the function in normal SQL.
Just be careful with dynamic columns names. In my environment I found that column name is limited with 30 characters and cannot contain a single quote in it. So, my query is now something like this:
SELECT
*
FROM
table(
pivot('
SELECT DISTINCT
P.proj_id,
REPLACE(substr(T.UDF_TYPE_LABEL, 1, 30), '''''''','','') as Attribute,
CASE
WHEN V.udf_text is null and V.udf_date is null and V.udf_number is NOT null THEN to_char(V.udf_number)
WHEN V.udf_text is null and V.udf_date is NOT null and V.udf_number is null THEN to_char(V.udf_date)
WHEN V.udf_text is NOT null and V.udf_date is null and V.udf_number is null THEN V.udf_text
ELSE NULL END
AS VALUE
FROM
project P
LEFT JOIN UDFVALUE V ON P.proj_id = V.proj_id
LEFT JOIN UDFTYPE T ON V.UDF_TYPE_ID = T.UDF_TYPE_ID
WHERE
P.delete_session_id IS NULL AND
T.TABLE_NAME = ''PROJECT''
')
)
Works well with up to 1m records.
Looks like it became possible without extra development effort since Oracle 19c with introduction of SQL_MACRO (and possibly Polymorphic Table Functions, which I haven't use yet).
create table t as
select
trunc(level/5) as id
, chr(65+mod(level, 5)) as code
, level as val
from dual
connect by level < 10
create function f_pivot
return varchar2 SQL_MACRO(TABLE)
is
l_codes varchar2(1000);
begin
select listagg(
distinct '''' || code
|| ''' as ' || code, ',')
into l_codes
from t;
return
'select *
from t
pivot (
max(val) for code in (
' || l_codes || '))';
end;
/
select *
from f_pivot()
ID | B | C | D | E | A
-: | -: | -: | -: | -: | ---:
0 | 1 | 2 | 3 | 4 | null
1 | 6 | 7 | 8 | 9 | 5
The only issue (in case of SQL_MACRO approach) is that result set doen't change its structure during one session:
insert into t
values(1, 'Q', 100);
commit;
select *
from f_pivot()
ID | B | C | D | E | A
-: | -: | -: | -: | -: | ---:
0 | 1 | 2 | 3 | 4 | null
1 | 6 | 7 | 8 | 9 | 5
But in separate session it works fine:
select dbms_xmlgen.getxml('select * from f_pivot()') as v
from dual
V
<?xml version="1.0"?><ROWSET> <ROW> <ID>0</ID> <B>1</B> <C>2</C> <D>3</D> <E>4</E> </ROW> <ROW> <ID>1</ID> <B>6</B> <C>7</C> <D>8</D> <E>9</E> <A>5</A> <Q>100</Q> </ROW></ROWSET>
Using with function feature dynamic pivot may be used in-place without predefined function:
with function f_pivot1
return varchar2 SQL_MACRO(TABLE)
is
l_codes varchar2(1000);
begin
select listagg(distinct '''' || code || ''' as ' || code, ',')
into l_codes
from t;
return
'select *
from t
pivot (
max(val) for code in (
' || l_codes || '))';
end;
select *
from f_pivot1()
ID | B | C | D | E | A | Q
-: | -: | -: | -: | -: | ---: | ---:
0 | 1 | 2 | 3 | 4 | null | null
1 | 6 | 7 | 8 | 9 | 5 | 100
db<>fiddle here
You cannot put a dynamic statement in the PIVOT's IN statement without using PIVOT XML, but you can use small Technic to use dynamic statement in PIVOT. In PL/SQL, within a string value, two apostrophe is equal to one apostrophes.
declare
sqlqry clob;
search_ids varchar(256) := '''2016'',''2017'',''2018'',''2019''';
begin
search_ids := concat( search_ids,'''2020''' ); -- you can append new search id dynamically as you wanted
sqlqry :=
'
select * from
(
select *
from EMPLOYEE
)
pivot
(
MIN(QTY) for YR in (' || search_ids || ')
)';
execute immediate sqlqry;
end;
There’s no straightforward method for dynamic pivoting in Oracle’s SQL, unless it returns XML type results.
For the non-XML results PL/SQL might be used through creating functions of SYS_REFCURSOR return type
With Conditional Aggregation
CREATE OR REPLACE FUNCTION Get_Jobs_ByYear RETURN SYS_REFCURSOR IS
v_recordset SYS_REFCURSOR;
v_sql VARCHAR2(32767);
v_cols VARCHAR2(32767);
BEGIN
SELECT LISTAGG( 'SUM( CASE WHEN job_title = '''||job_title||''' THEN 1 ELSE 0 END ) AS "'||job_title||'"' , ',' )
WITHIN GROUP ( ORDER BY job_title )
INTO v_cols
FROM ( SELECT DISTINCT job_title
FROM jobs j );
v_sql :=
'SELECT "HIRE YEAR",'|| v_cols ||
' FROM
(
SELECT TO_NUMBER(TO_CHAR(hire_date,''YYYY'')) AS "HIRE YEAR", job_title
FROM employees e
JOIN jobs j
ON j.job_id = e.job_id
)
GROUP BY "HIRE YEAR"
ORDER BY "HIRE YEAR"';
OPEN v_recordset FOR v_sql;
DBMS_OUTPUT.PUT_LINE(v_sql);
RETURN v_recordset;
END;
/
With PIVOT Clause
CREATE OR REPLACE FUNCTION Get_Jobs_ByYear RETURN SYS_REFCURSOR IS
v_recordset SYS_REFCURSOR;
v_sql VARCHAR2(32767);
v_cols VARCHAR2(32767);
BEGIN
SELECT LISTAGG( ''''||job_title||''' AS "'||job_title||'"' , ',' )
WITHIN GROUP ( ORDER BY job_title )
INTO v_cols
FROM ( SELECT DISTINCT job_title
FROM jobs j );
v_sql :=
'SELECT *
FROM
(
SELECT TO_NUMBER(TO_CHAR(hire_date,''YYYY'')) AS "HIRE YEAR", job_title
FROM employees e
JOIN jobs j
ON j.job_id = e.job_id
)
PIVOT
(
COUNT(*) FOR job_title IN ( '|| v_cols ||' )
)
ORDER BY "HIRE YEAR"';
OPEN v_recordset FOR v_sql;
DBMS_OUTPUT.PUT_LINE(v_sql);
RETURN v_recordset;
END;
/
But there's a drawback with LISTAGG() that's coded ORA-01489: result of string concatenation is too long raises whenever the concatenated string within the first argument exceeds the length of 4000 characters. In this case, the query returning the value of v_cols variable might be replaced with the XMLELEMENT() function nested within XMLAGG() such as
CREATE OR REPLACE FUNCTION Get_Jobs_ByYear RETURN SYS_REFCURSOR IS
v_recordset SYS_REFCURSOR;
v_sql VARCHAR2(32767);
v_cols VARCHAR2(32767);
BEGIN
SELECT RTRIM(DBMS_XMLGEN.CONVERT(
XMLAGG(
XMLELEMENT(e, 'SUM( CASE WHEN job_title = '''||job_title||
''' THEN 1 ELSE 0 END ) AS "'||job_title||'",')
).EXTRACT('//text()').GETCLOBVAL() ,1),',') AS "v_cols"
FROM ( SELECT DISTINCT job_title
FROM jobs j);
v_sql :=
'SELECT "HIRE YEAR",'|| v_cols ||
' FROM
(
SELECT TO_NUMBER(TO_CHAR(hire_date,''YYYY'')) AS "HIRE YEAR", job_title
FROM employees e
JOIN jobs j
ON j.job_id = e.job_id
)
GROUP BY "HIRE YEAR"
ORDER BY "HIRE YEAR"';
DBMS_OUTPUT.put_line(LENGTH(v_sql));
OPEN v_recordset FOR v_sql;
RETURN v_recordset;
END;
/
unless the upper limit 32767 for VARCHAR2 type is exceeded. This last method might also be applied for the database with version prior to Oracle 11g Release 2 as they don't contain LISTAGG() function.
Btw, yet LISTAGG() function can be used during the checkout of the v_cols even for very long concatenated string generated without getting ORA-01489 error while the trailing part of the string is truncated through use of ON OVERFLOW TRUNCATE clause if the version for the database is 12.2+ such as
LISTAGG( <concatenated string>,',' ON OVERFLOW TRUNCATE 'THE REST IS TRUNCATED' WITHOUT COUNT )
The function can be invoked as
VAR rc REFCURSOR
EXEC :rc := Get_Jobs_ByYear;
PRINT rc
from SQL Developer's command line
or
BEGIN
:result := Get_Jobs_ByYear;
END;
from Test window of PL/SQL Developer in order to get the result
set.
Demo for generated queries
You can dynamically pivot data in a single SQL statement with the open source program Method4.Pivot.
After installing the package, call the function and pass in a SQL statement as a string. The last column of your SQL statement defines the values, and the second-to-last column defines the column names. The default aggregation function is MAX, which works well for common entity-attribute-value queries like this one:
select * from table(method4.pivot(
q'[
select 'A' name, 1 value from dual union all
select 'B' name, 2 value from dual union all
select 'C' name, 3 value from dual
]'
));
A B C
- - -
1 2 3
The program also supports different aggregation functions through the parameter P_AGGREGATE_FUNCTION, and allows for a custom column name order if you add a column named PIVOT_COLUMN_ID.
The package uses an Oracle Data Cartridge approach similar to Anton's pivot, but Method4.Pivot has several important advantages:
Regular open source program with a repo, installation instructions, license, unit tests, documentation, and comments - not just a Zip file on a blog.
Handles unusual column names.
Handles unusual data types, like floats.
Handles up to 1000 columns.
Provides meaningful error messages for common mistakes.
Handles NULL column names.
Handles 128-character column names.
Prevents misleading implicit conversion.
Hard-parses statements each time to catch underlying table changes.
But most users are still better off creating a dynamic pivot at the application layer or with the pivot XML option.

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;

Oracle Query: Loop through columns dynamically to create rows

I have the following Oracle database that can have any number of columns that may grow over time by user intervention in the web application.
ID | Name | Football | WhoCreated | When | Baseball | Cheerleading | Swimming
1 | Billy | (null) | sam, smith | (Timestamp)| 1 | (null) | 1
2 | Susie | 1 | sam, smith | (Timestamp)| (null) | 1 | 1
3 | Johnny | 1 | Homer | (Timestamp)| 1 | (null) | (null)
I am trying to generate an output that looks like
2, Susie, Football
3, Johnny, Football
1, Billy, Baseball
3, Johnny, Baseball
2, Susie, Cheerleading
1, Billy, Swimming
2, Susie, Swimming
I can do this with a UNION, but I will have to adjust each for the specific name field. I'm already up to about 50 columns (50 unions), and that can grow at any time by users in the system. To further complicate things, I have a few columns for auditing purposes tucked in the middle of the list. I really need some sort of dynamic way of looping through the columns, I have searched, but none seem to address the issue I have.
This should give you an idea. I couldn't test it, but fairly simple to understand.
create or replace package test_pkg AS
TYPE REP_CURS IS REF CURSOR;
TYPE output_REC IS RECORD(
id_ number,
name_ varchar2(50),
field_ varchar2(50));
TYPE output_TAB IS TABLE OF output_REC;
FUNCTION Get_Data RETURN output_TAB
PIPELINED;
END test_pkg;
CREATE OR REPLACE PACKAGE BODY test_pkg IS
FUNCTION Get_Data RETURN output_TAB
PIPELINED IS
output_REC_ output_REC;
rep_lines_ REP_CURS;
stmt_ VARCHAR2(5000);
table_rec_ yourtable%ROWTYPE;
begin
stmt_ := ' YOUR QUERY HERE ';
OPEN rep_lines_ FOR stmt_;
LOOP
FETCH rep_lines_
INTO table_rec_;
EXIT WHEN rep_lines_%NOTFOUND;
output_REC_.id_ := table_rec_.id;
output_REC_.name_ := table_rec_.name;
if table_rec_.football IS not null then
output_REC_.field_ := table_rec_.football;
PIPE ROW(output_REC_);
end if;
if table_rec_.Baseball IS not null then
output_REC_.field_ := table_rec_.Baseball;
PIPE ROW(output_REC_);
end if;
if table_rec_.Cheerleading IS not null then
output_REC_.field_ := table_rec_.Cheerleading;
PIPE ROW(output_REC_);
end if;
if table_rec_.Swimming IS not null then
output_REC_.field_ := table_rec_.Swimming;
PIPE ROW(output_REC_);
end if;
END LOOP;
CLOSE rep_lines_;
RETURN;
exception
when others then
DBMS_OUTPUT.put_line('Error:' || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE ||
DBMS_UTILITY.FORMAT_ERROR_STACK ||
DBMS_UTILITY.FORMAT_CALL_STACK);
END Get_Data;
END test_pkg;
You should create a function and then use it, please consider the code below:
CREATE OR REPLACE FUNCTION commalist (par_id NUMBER) RETURN VARCHAR2 IS
TYPE curs IS REF CURSOR;
v_emp_cursor curs;
fieldvalue VARCHAR2(4000);
var_out VARCHAR2(4000);
CURSOR col IS
SELECT column_name
FROM user_tab_columns
WHERE table_name = 'TABLE';
BEGIN
FOR reccol IN col LOOP
OPEN v_emp_cursor FOR ('SELECT '||reccol.column_name||' val
FROM TABLE
WHERE id = '||par_id);
LOOP
FETCH v_emp_cursor INTO fieldvalue;
IF fieldvalue IS NOT NULL THEN
var_out := var_out||fieldvalue||', ';
END IF;
EXIT WHEN v_emp_cursor%NOTFOUND;
END LOOP;
CLOSE v_emp_cursor;
END LOOP;
RETURN SubStr(var_out, 0, Length(var_out) - 2);
END;
Try this
with src as (select 1 ID, 'Billy' name, 0 Football, 'sam smith' WhoCreated, sysTimestamp when,
1 Baseball, cast(null as number) Cheerleading, 1 Swimming
from dual
union all
select 2, 'Susie', 1, 'sam smith', sysTimestamp, null, 1, 1
from dual
union all
select 3, 'Johnny', 1, 'Homer', sysTimestamp, 1, null, null
from dual),
unpivottbl as (select *
from src
UNPIVOT
(
VAL
FOR descript
IN (Football, Baseball, Cheerleading, Swimming)
))
select ID, name, descript
from unpivottbl
where VAL = 1
Though it is not dynamic solution, you will need to add the columns for which you are going to unpivot the data. This is reference for unpivot.

Resources