Concatenate XML nodes using XPath and XQuery in Oracle - oracle

I have a XML data stored in CLOB column as follows:
<row id='123456' xml:space='preserve'>
<name>Martin H</name>
<phone>1111</phone>
<phone m='2'>2222</phone>
<phone m='3'></phone>
<sms m='2'>1212</sms>
<sms m='3'>2323</sms>
<email>abc#gmail.com</email>
<email m='3'>xyz#outlook.com</email></row>
How can I use Xpath and XQuery in Oracle DB to have the following expected output in only one row:
Name
Phone
Sms
Email
Martin H
1111#2222#
#1212#2323
abc#gmail.com##xyz#outlook.com
Basically, a '#' is used to separate values and any missing tag of any group from XML above is returned as a null value.
Any help will be much appreciated. Thanks!

Using XMLTABLE to parse your XML into columns, you can then use the string-join operator when defining the path to perform a similar functionality to LISTAGG.
WITH
example_table (xml_clob)
AS
(SELECT EMPTY_CLOB () || '<row id=''123456'' xml:space=''preserve''>
<name>Martin H</name>
<phone>1111</phone>
<phone m=''2''>2222</phone>
<phone m=''3''></phone>
<sms m=''2''>1212</sms>
<sms m=''3''>2323</sms>
<email>abc#gmail.com</email>
<email m=''3''>xyz#outlook.com</email>
</row>' FROM DUAL)
SELECT xt.*
FROM example_table et
CROSS JOIN
XMLTABLE (
'row'
PASSING xmltype (et.xml_clob)
COLUMNS name VARCHAR2 (100)
PATH 'name',
phone VARCHAR2 (100)
PATH 'string-join(phone,"#")',
sms VARCHAR2 (100)
PATH 'string-join(sms,"#")',
email VARCHAR2 (100)
PATH 'string-join(email,"#")') xt;
NAME PHONE SMS EMAIL
___________ _____________ ____________ ________________________________
Martin H 1111#2222# 1212#2323 abc#gmail.com#xyz#outlook.com
Update
After reading kfinity's comment and understanding the problem a bit clearer, the problem can still be solved using XMLTABLE, then CASE statements to contatenate the values together for the desired result.
WITH
example_table (xml_clob)
AS
(SELECT EMPTY_CLOB () || '<row id=''123456'' xml:space=''preserve''>
<name>Martin H</name>
<phone>1111</phone>
<phone m=''2''>2222</phone>
<phone m=''3''></phone>
<sms m=''2''>1212</sms>
<sms m=''3''>2323</sms>
<email>abc#gmail.com</email>
<email m=''3''>xyz#outlook.com</email>
</row>' FROM DUAL)
SELECT xt.name,
CASE
WHEN phone1_mtag IS NULL AND phone1 IS NOT NULL THEN phone1
WHEN phone2_mtag IS NULL AND phone2 IS NOT NULL THEN phone2
WHEN phone3_mtag IS NULL AND phone3 IS NOT NULL THEN phone3
END
|| '#'
|| CASE
WHEN phone1_mtag = '2' AND phone1 IS NOT NULL THEN phone1
WHEN phone2_mtag = '2' AND phone2 IS NOT NULL THEN phone2
WHEN phone3_mtag = '2' AND phone3 IS NOT NULL THEN phone3
END
|| '#'
|| CASE
WHEN phone1_mtag = '3' AND phone1 IS NOT NULL THEN phone1
WHEN phone2_mtag = '3' AND phone2 IS NOT NULL THEN phone2
WHEN phone3_mtag = '3' AND phone3 IS NOT NULL THEN phone3
END AS phone,
CASE
WHEN sms1_mtag IS NULL AND sms1 IS NOT NULL THEN sms1
WHEN sms2_mtag IS NULL AND sms2 IS NOT NULL THEN sms2
WHEN sms3_mtag IS NULL AND sms3 IS NOT NULL THEN sms3
END
|| '#'
|| CASE
WHEN sms1_mtag = '2' AND sms1 IS NOT NULL THEN sms1
WHEN sms2_mtag = '2' AND sms2 IS NOT NULL THEN sms2
WHEN sms3_mtag = '2' AND sms3 IS NOT NULL THEN sms3
END
|| '#'
|| CASE
WHEN sms1_mtag = '3' AND sms1 IS NOT NULL THEN sms1
WHEN sms2_mtag = '3' AND sms2 IS NOT NULL THEN sms2
WHEN sms3_mtag = '3' AND sms3 IS NOT NULL THEN sms3
END AS sms,
CASE
WHEN email1_mtag IS NULL AND email1 IS NOT NULL THEN email1
WHEN email2_mtag IS NULL AND email2 IS NOT NULL THEN email2
WHEN email3_mtag IS NULL AND email3 IS NOT NULL THEN email3
END
|| '#'
|| CASE
WHEN email1_mtag = '2' AND email1 IS NOT NULL THEN email1
WHEN email2_mtag = '2' AND email2 IS NOT NULL THEN email2
WHEN email3_mtag = '2' AND email3 IS NOT NULL THEN email3
END
|| '#'
|| CASE
WHEN email1_mtag = '3' AND email1 IS NOT NULL THEN email1
WHEN email2_mtag = '3' AND email2 IS NOT NULL THEN email2
WHEN email3_mtag = '3' AND email3 IS NOT NULL THEN email3
END AS email
FROM example_table et
CROSS JOIN XMLTABLE ('row'
PASSING xmltype (et.xml_clob)
COLUMNS name VARCHAR2 (100) PATH 'name',
phone1 VARCHAR2 (100) PATH 'phone[1]',
phone1_mtag VARCHAR2 (100) PATH 'phone[1]/#m',
phone2 VARCHAR2 (100) PATH 'phone[2]',
phone2_mtag VARCHAR2 (100) PATH 'phone[2]/#m',
phone3 VARCHAR2 (100) PATH 'phone[3]',
phone3_mtag VARCHAR2 (100) PATH 'phone[3]/#m',
sms1 VARCHAR2 (100) PATH 'sms[1]',
sms1_mtag VARCHAR2 (100) PATH 'sms[1]/#m',
sms2 VARCHAR2 (100) PATH 'sms[2]',
sms2_mtag VARCHAR2 (100) PATH 'sms[2]/#m',
sms3 VARCHAR2 (100) PATH 'sms[3]',
sms3_mtag VARCHAR2 (100) PATH 'sms[3]/#m',
email1 VARCHAR2 (100) PATH 'email[1]',
email1_mtag VARCHAR2 (100) PATH 'email[1]/#m',
email2 VARCHAR2 (100) PATH 'email[2]',
email2_mtag VARCHAR2 (100) PATH 'email[2]/#m',
email3 VARCHAR2 (100) PATH 'email[3]',
email3_mtag VARCHAR2 (100) PATH 'email[3]/#m') xt;
NAME PHONE SMS EMAIL
___________ _____________ _____________ _________________________________
Martin H 1111#2222# #1212#2323 abc#gmail.com##xyz#outlook.com

Related

Alternative to evaluating a string using a function

I'm trying to output a list of records but some may not have a value in the subject column.
I have an altSubject column that would specify what to output instead.
simplified for example
insert all
into myTable
(id, subject, altSubject, partNumber, serialNumber, startDate, endDate)
values
(1, 'test',null,'xyz','123','1/1/2019', '1/5/2019')
into myTable
(id, subject, altSubject, partNumber, serialNumber, startDate, endDate)
values
(2, null, '''SN: '' || serialNumber','abc','789','1/1/2019', '1/5/2019')
output should look like:
subject | Part Number | Start Date | End Date
test | xyz | 1/1/2019 | 1/5/2019
SN: 789 | abc | 1/1/2019 | 1/5/2019
I've been able to do this using a case with a function below but the problem I'm having is it takes 5 minutes to run on a 40k row table.
select
...
...
case when altSubject is not null then
fAltSubject(id,altSubject)
else
subject
end subject
from
myTable
where
status = 'closed'
the function:
create or replace function fAltSubject
(pID in number
, pAltSubject in varchar2)
return varchar2
as
vNewSubject varchar2(400) := '';
begin
vSql := 'select ' ||
pAltSubject ||
' from
myTable
where
id = ' || pID;
execute immediate vSql
into
vNewSubject;
return vNewSubject;
end faltsubject;
Is there a better way to do this that doesn't take 5 minutes?
Thanks in advance.
"how to use a user defined mask in a column when the mask could be a combination of fields and text".
This is the best I can do and get good performance.
The table defines Subject, AltSubject and DisplaySubject.
A trigger sets the DisplaySubject based on the other two fields.
The trigger must reference the specific column names, so it needs to be regenerated every time columns are added. Maybe a nightly job?
create table myTable (
id number,
subject varchar2(64),
altSubject varchar2(128),
displaySubject varchar2(128),
partNumber varchar2(16),
serialNumber varchar2(16),
startDate date,
endDate date
);
create or replace procedure generate_mytable_trigger is
l_newline constant varchar2(1) := chr(10);
l_text clob := to_clob(
'create or replace trigger mytable_displaysubject
before insert or update on mytable
for each row
declare
lt_column_names sys.odcivarchar2list;
begin
if :new.subject is not null then
:new.altsubject := null;
:new.displaysubject := :new.subject;
return;
end if;
:new.displaysubject := :new.altsubject;
-- start lines to be generated');
l_end_text constant varchar2(4000) :=
'-- end lines to be generated
return;
end mytable_displaysubject;';
begin
for rec in (
select l_newline ||
':new.displaysubject := replace(:new.displaysubject, ''#'||column_name||'#'', :new.'||column_name||');'
as text
from user_tab_columns where table_name = 'MYTABLE'
and column_name not in ('SUBJECT','ALTSUBJECT','DISPLAYSUBJECT')
) loop
l_text := l_text || rec.text;
end loop;
l_text := l_text || l_newline || l_end_text;
execute immediate l_text;
end;
/
exec generate_mytable_trigger;
Now a little test:
insert into mytable(id, subject, altsubject, partnumber, serialnumber, startdate, enddate)
select 1, 'test',null,'xyz','123',sysdate, sysdate+1 from dual
union all
select 2, null,'PN: #PARTNUMBER#','abc','789',sysdate, sysdate+1 from dual
union all
select 3, null,'PN: #PARTNUMBER#, SN: #SERIALNUMBER#','qsdf','789',sysdate, sysdate+1 from dual
union all
select 3, null,'PN: #PARTNUMBER#, ??: #BADCOLUMN#','qsdf','789',sysdate, sysdate+1 from dual;
commit;
select subject, altsubject, displaysubject from mytable;
SUBJECT ALTSUBJECT DISPLAYSUBJECT
test test
PN: #PARTNUMBER# PN: abc
PN: #PARTNUMBER#, SN: #SERIALNUMBER# PN: qsdf, SN: 789
PN: #PARTNUMBER#, ??: #BADCOLUMN# PN: qsdf, ??: #BADCOLUMN#
You have a recent version of Oracle: congratulations. So use it: virtual columns!
create table myTable (
id number,
subject varchar2(32),
altSubject varchar2(64)
generated always as (case when subject is null then 'SN: '||serialnumber end),
partNumber varchar2(16),
serialNumber varchar2(16),
startDate date,
endDate date
);
insert into mytable(id, subject, partnumber, serialnumber, startdate, enddate)
select 1, 'test','xyz','123',sysdate, sysdate+1 from dual
union all
select 2, null,'abc','789',sysdate, sysdate+1 from dual;
select ID, coalesce(SUBJECT, ALTSUBJECT) subject,
PARTNUMBER, SERIALNUMBER, STARTDATE, ENDDATE
from mytable;
ID SUBJECT PARTNUMBER SERIALNUMBER STARTDATE ENDDATE
-- -------- ----------- ------------- ------------------- -------------------
1 test xyz 123 2019-12-18 13:09:10 2019-12-19 13:09:10
2 SN: 789 abc 789 2019-12-18 13:09:10 2019-12-19 13:09:10
That may very well be overkill. You could always get rid of the extra column and say:
select ID, coalesce(SUBJECT, 'SN: '||serialnumber) subject,
PARTNUMBER, SERIALNUMBER, STARTDATE, ENDDATE
from mytable;
Best regards,
Stew Ashton

View based on function, correct query treated as bad

This is code:
CREATE TABLE emp_where (where_clause VARCHAR2(4000));
INSERT INTO emp_where (where_clause)
VALUES ('first_name=''KING'' or department_id = 20');
commit;
CREATE OR REPLACE TYPE t_emp_rec AS OBJECT (
EMPLOYEE_ID NUMBER(4),
FIRST_NAME VARCHAR2(10),
JOB_ID VARCHAR2(9),
MANAGER_ID NUMBER(4),
HIRE_DATE DATE,
SALARY NUMBER(7,2),
DEPARTMENT_ID NUMBER(2)
);
/
CREATE OR REPLACE TYPE t_emp_tab AS TABLE OF t_emp_rec;
/
CREATE OR REPLACE FUNCTION emp_fn RETURN t_emp_tab
PIPELINED IS
l_sql VARCHAR2(32767);
l_where VARCHAR2(4000);
TYPE l_cur_type IS REF CURSOR;
l_cur l_cur_type;
l_rec employees%ROWTYPE;
BEGIN
SELECT where_clause INTO l_where FROM emp_where;
l_sql := 'SELECT * FROM employees WHERE ' || l_where;
OPEN l_cur FOR l_sql;
LOOP
FETCH l_cur
INTO l_rec;
EXIT WHEN l_cur%NOTFOUND;
PIPE ROW(t_emp_rec(EMPLOYEE_ID => l_rec.EMPLOYEE_ID
,FIRST_NAME => l_rec.FIRST_NAME
,JOB_ID => l_rec.JOB_ID
,MANAGER_ID => l_rec.MANAGER_ID
,hire_date => l_rec.hire_date
,SALARY => l_rec.SALARY
,DEPARTMENT_ID => l_rec.DEPARTMENT_ID));
END LOOP;
RETURN;
EXCEPTION
WHEN OTHERS THEN
raise_application_error(-20000, SQLERRM || chr(10) || l_sql);
END;
/
CREATE OR REPLACE VIEW emp_vw AS
UPDATE emp_where SET where_clause = 'EMPLOYEE_ID BETWEEN 100 and 200';
COMMIT;
SELECT * FROM emp_vw;
When I execute SELECT * FROM emp_vw; with this clause: EMPLOYEE_ID BETWEEN 100 and 200
Oracle gives me an error
ORA-20000: ORA-06502: PL/SQL: numeric or value error: number precision too large
SELECT * FROM employees WHERE EMPLOYEE_ID BETWEEN 100 and 200
But when I execute query itself (SELECT * FROM employees WHERE EMPLOYEE_ID BETWEEN 100 and 200) there is no error. Another scenario -
when clause is 'deparment_id = 20', executing a view is correct. But when I change '=' to '>' (department_id > 20) - numeric or value error: number precision too large.
Can someone explain me how this is happening?
If I run:
DESCRIBE employees
Then I get the output:
Name Null Type
-------------- -------- ------------
EMPLOYEE_ID NOT NULL NUMBER(6)
FIRST_NAME VARCHAR2(20)
LAST_NAME NOT NULL VARCHAR2(25)
EMAIL NOT NULL VARCHAR2(25)
PHONE_NUMBER VARCHAR2(20)
HIRE_DATE NOT NULL DATE
JOB_ID NOT NULL VARCHAR2(10)
SALARY NUMBER(8,2)
COMMISSION_PCT NUMBER(2,2)
MANAGER_ID NUMBER(6)
DEPARTMENT_ID NUMBER(4)
If you compare it to your t_emp_rec object then you will see that most of the object attributes are a smaller size than the table columns.
Change the object to have the same sizes and it should work:
CREATE OR REPLACE TYPE t_emp_rec AS OBJECT (
EMPLOYEE_ID NUMBER(6),
FIRST_NAME VARCHAR2(20),
JOB_ID VARCHAR2(10),
MANAGER_ID NUMBER(6),
HIRE_DATE DATE,
SALARY NUMBER(8,2),
DEPARTMENT_ID NUMBER(4)
);
/

Writing a PLSQL Oracle function to query a table

I have the following table
CREATE TABLE Book
(
book_id INTEGER NOT NULL ,
isbn VARCHAR2 (20) NOT NULL,
tittle VARCHAR2 (100) NOT NULL ,
shelf_letter CHAR (1) NOT NULL ,
call_number INTEGER ,
no_of_copies INTEGER NOT NULL ,
) ;
I need to write a function to retrieve book_id, title,call_number, shelf_letter, no_of_copies for a given isbn.
Input parameters: isbn
Output parameters: title, no_of_copies,call_number,shelf_letter.
Return book_id if the query is successful and -1 if not.
How can I properly write this function?
create OR replace FUNCTION get_book_id
(
p_isbn IN VARCHAR2
, po_title OUT VARCHAR2
, po_no_of_copies OUT NUMBER
, po_call_number OUT NUMBER
, po_shelf_letter OUT NUMBER
)
RETURN NUMBER
IS
v_book_id NUMBER;
BEGIN
BEGIN
SELECT
book_id
, title
, no_of_copies
, call_number
, shelf_letter
INTO
v_book_id
, po_title
, po_no_of_copies
, po_call_number
, po_shelf_letter
FROM book
WHERE isbn = 'p_isbn'
;
EXCEPTION
WHEN NO_DATA_FOUND THEN
v_book_id := -1;
END;
RETURN v_book_id;
END;
/
DECLARE
TYPE book_info_rec IS RECORD
(
book_id NUMBER(1)
, title VARCHAR2(30)
, call_number NUMBER(1)
, shelf_letter VARCHAR2(30)
, no_of_copies NUMBER(1)
);
l_book book_info_rec;
FUNCTION get_book_info(isbn_in IN VARCHAR2) RETURN book_info_rec
AS
l_book_info book_info_rec;
BEGIN
SELECT 1
, 'A Book'
, 2
, 'A'
, 3
INTO l_book_info
FROM DUAL
WHERE dummy = isbn_in;
RETURN l_book_info;
END;
BEGIN
l_book := get_book_info('X');
DBMS_OUTPUT.PUT_LINE
(
l_book.book_id
|| ' ' || l_book.title
|| ' ' || l_book.call_number
|| ' ' || l_book.shelf_letter
|| ' ' || l_book.no_of_copies
);
END;

How to declare a Cursors With different conditions

I have a procedure EMPHIRESEPCHAN which is used to fetch the employees list who are hired, seperated and changed their titles based on a particular time frame. The procedure is as follows:
PROCEDURE EMPHIRESEPCHAN ( p_Start in VarChar2, p_End in VarChar2,
p_Hire IN VarChar2, p_Sep IN VarChar2, p_Changed IN VarChar2, p_Condition1 IN VarChar2, p_Condition2 IN VarChar2)
IS
CURSOR c_emplst ( p_listtype varchar2 ) IS
select e.emp_id, e.name, e.Rank
from person.emp e
where emp_id in (select distinct(emp_id) from person.promo
where pdate between p_startDate and p_endDate
and dcode in
(select adj from support.descr where typ = 'PROMO' and smeaning = p_listtype) );
CURSOR c_promolst ( p_emp_id varchar2 ) IS
select pdate
from person.promo
where emp_id = p_emp_id
order by 2 desc;
Begin
for EmpRec in c_emplst ('HIRE')
LOOP
for PromoRec in c_PromoLst ( EmpRec.emp )
LOOP
if PromoRec.Dcode in ('TEMPORARY','RETURN','APPOINTED' )
-- Do all the operation
end if;
end loop;
end loop;
end EMPHIRESEPCHAN;
I have to modify the procedure to retrieve the employee list based on p_Condition1 and p_Condition2 parameters.
If the p_Condition1 is not null and p_Condition2 is null, I have to retrieve the employees who have Rank = 'Developer'
If the p_Condition1 is null and p_Condition2 is not null I have to retrieve the employees who have Rank = 'Tester'
If the p_Condition1 and p_Condition2 is not null I have to retrieve the employees who have Rank both 'Developer' and 'Tester'.
I read so many posts in various sites and found answers which I was not able to follow.
Based on the posts, I made modifications to the cursor as follows
CURSOR c_emplst ( p_listtype varchar2 ) IS
select e.emp_id, e.name, e.Rank
from person.emp e
where ( p_Condition1 = null and p_Condition2 = null = and emp_id in (select distinct(emp_id) from person.promo
where pdate between p_startDate and p_endDate
and dcode in (select adj from support.descr where typ = 'PROMO' and smeaning = p_listtype) )
or ( p_Condition1 > null and p_Condition2 = null = and emp_id in (select distinct(emp_id) from person.promo
where pdate between p_startDate and p_endDate
and Rank ='Developer'
and dcode in (select adj from support.descr where typ = 'PROMO' and smeaning = p_listtype) )
or ( p_Condition1 = null and p_Condition2 > null = and emp_id in (select distinct(emp_id) from person.promo
where pdate between p_startDate and p_endDate
and Rank = 'Tester'
and dcode in (select adj from support.descr where typ = 'PROMO' and smeaning = p_listtype) );
However it's not working.
Thanks for your time and consideration.
I suspect these conditions are your problem:
p_Condition1 = null
Nothing is ever equal to NULL. NULL is not even equal to NULL. Instead, use:
p_Condition1 IS NULL

Update CLOB Column to NULL

I need to update an oracle clob column value to null. I tried to below but that didn't work. Any ideas?
update table_name
set CONTENT_TEMPLATE=empty_clob()
where table_key=12345;
Name Null Type
-------------------- -------- -------------
TEST_KEY NOT NULL NUMBER(10)
TEST_ID NOT NULL VARCHAR2(100)
TEST_TYPE NOT NULL VARCHAR2(30)
TEMPLATE_ID VARCHAR2(100)
ROUTE VARCHAR2(100)
MEDIUM VARCHAR2(100)
BEGIN_EFFECTIVE_DATE NOT NULL DATE
END_EFFECTIVE_DATE NOT NULL DATE
CUSTOMER_ID NOT NULL VARCHAR2(30)
VAR_ID NOT NULL VARCHAR2(30)
CREATION_DATE NOT NULL DATE
LAST_UPDATED_DATE NOT NULL DATE
LAST_UPDATED_BY NOT NULL VARCHAR2(30)
CONTENT_TEMPLATE CLOB
SUBJECT_TEMPLATE CLOB
UNIT_TYPE VARCHAR2(30)
OVERLOADED_ALERT_KEY NUMBER(10)
ALERT_TYPE_ALIAS VARCHAR2(100)
update table_name
set column_name = null
where table_key=12345;
NULL - Absense of data
empty_clob() - CLOB is initialized and empty, not same as NULL.
If you still get ORA-01407 with this column then that means there is a NOT NULL constraint on the column which is of CLOB datatype.
UPDATE:
Since the CLOB column is not NOT NULL setting it to CONTENT_TEMPLATE = NULL should work. CONTENT_TEMPLATE = empty_clob() will also work, but bear in mind that it is not same as NULL.

Resources