I have this XML fragment:
<ArrayOfItemsElement>
<ItemsElement>
<code>92100141</code>
<description>BLABLA</description>
<number>1</number>
<value>10</value>
<taxPercentage>0</taxPercentage>
<currencyCode>EUR</currencyCode>
</ItemsElement>
<ItemsElement>
<code>92200118</code>
<description>BBBBBB</description>
<number>1</number>
<value>999</value>
<taxPercentage>0</taxPercentage>
<currencyCode>EUR</currencyCode>
</ItemsElement>
<ItemsElement>
<code>92100999</code>
<description>TESTEST</description>
<number>1</number>
<value>10</value>
<taxPercentage>0</taxPercentage>
<currencyCode>EUR</currencyCode>
</ItemsElement>
<ItemsElement>
<code>92200118</code>
<description>AAAAAAAA</description>
<number>1</number>
<value>15</value>
<taxPercentage>0</taxPercentage>
<currencyCode>EUR</currencyCode>
</ItemsElement>
</ArrayOfItemsElement>
I have a string with the specific order for each element in this XML: "92200118;92200118;92100141;92100999".
In PL/SQL, how can I reorder the XML above using the specific order in the string and get a new XML fragment.
Note: code 92200118 appears two times in the fragment. Can be one or the other at first.
Thanks.
With a query you could decompose, order and recompose your XML document as follows:
Having
create table test_xml (id int, xmldata xmltype);
insert into test_xml values (1,
'<ArrayOfItemsElement>
<ItemsElement>...
</ItemsElement>
</ArrayOfItemsElement>');
Then
with table1 as (
SELECT t.* FROM test_xml, XMLTABLE('/ArrayOfItemsElement/ItemsElement'
PASSING xmldata COLUMNS
"code" VARCHAR2(100) PATH 'code',
"description" VARCHAR2(100) PATH 'description',
"number" VARCHAR2(100) PATH 'number',
"value" VARCHAR2(100) PATH 'value',
"taxPercentage" VARCHAR2(100) PATH 'taxPercentage',
"currencyCode" VARCHAR2(100) PATH 'currencyCode') t
order by decode("code", '92200118', 1, '92100141', 2, '92100999', 3))--custom order
select xmlelement("ArrayOfItemsElement" , xmlagg(xmlelement("ItemsElement",
xmlelement("code", "code") ,
xmlelement("description", "description") ,
xmlelement("number", "number") ,
xmlelement("value", "value") ,
xmlelement("taxPercentage", "taxPercentage") ,
xmlelement("currencyCode", "currencyCode")
))) as xmldata from table1;
If you prefer PL/SQL it would be like
DECLARE
j test_xml%rowtype;
document VARCHAR2(1000);
xml_document xmltype;
BEGIN
document := '<ArrayOfItemsElement>';
FOR j IN (SELECT t.* FROM test_xml, XMLTABLE('/ArrayOfItemsElement/ItemsElement'
PASSING xmldata COLUMNS
"code" VARCHAR2(100) PATH 'code',
"description" VARCHAR2(100) PATH 'description',
"number" VARCHAR2(100) PATH 'number',
"value" VARCHAR2(100) PATH 'value',
"taxPercentage" VARCHAR2(100) PATH 'taxPercentage',
"currencyCode" VARCHAR2(100) PATH 'currencyCode') t
order by decode("code", '92200118', 1, '92100141', 2, '92100999', 3), "description")
LOOP
--reassemble xml_document
document := document || '<ItemsElement>' ||
'<code>' || j."code" || '</code>' ||
'<description>' || j."description" || '</description>' ||
'<number>' || j."number" || '</number>' ||
'<value>' || j."value" || '</value>' ||
'<taxPercentage>' || j."taxPercentage" || '</taxPercentage>' ||
'<currencyCode>' || j."currencyCode" || '</currencyCode>' ||
'</ItemsElement>';
END LOOP;
document := document || '</ArrayOfItemsElement>';
xml_document := xmltype(document);
--insert into sample table
insert into test_xml values (2, xml_document);
END;
I get manage one possible solution for this issue.
I can transform XML in a inline view. Order that inline view using the string with order values and then transform again in a XML.
SELECT XMLELEMENT (
"ArrayOfItemsElement",
XMLAGG (
XMLELEMENT (
"ItemsElement",
XMLCONCAT (
XMLSequenceType (
xml_utils.getXmlTag (pv_tagName => 'code', pv_value => code),
xml_utils.getXmlTag (pv_tagName => 'description', pv_value => description),
xml_utils.getXmlTag (pv_tagName => 'number', pv_value => quantity),
xml_utils.getXmlTag (pv_tagName => 'VALUE', pv_value => VALUE),
xml_utils.getXmlTag (pv_tagName => 'taxPercentage',
pv_value => taxPercentage),
xml_utils.getXmlTag (pv_tagName => 'currencyCode',
pv_value => currencyCode))))))
FROM (SELECT code,
description,
quantity,
VALUE,
taxPercentage,
currencyCode
FROM ( SELECT code,
description,
quantity,
VALUE,
taxPercentage,
currencyCode,
RANK () OVER (ORDER BY ROWNUM ASC) row_rank
FROM XMLTABLE ('ArrayOfItemsElement/ItemsElement'
PASSING XMLTYPE ('<ArrayOfItemsElement>
<ItemsElement>
<code>92100141</code>
<description>BLABLA</description>
<number>1</number>
<value>10</value>
<taxPercentage>0</taxPercentage>
<currencyCode>EUR</currencyCode>
</ItemsElement>
<ItemsElement>
<code>92200118</code>
<description>BBBBBB</description>
<number>1</number>
<value>999</value>
<taxPercentage>0</taxPercentage>
<currencyCode>EUR</currencyCode>
</ItemsElement>
<ItemsElement>
<code>92100999</code>
<description>TESTEST</description>
<number>1</number>
<value>10</value>
<taxPercentage>0</taxPercentage>
<currencyCode>EUR</currencyCode>
</ItemsElement>
<ItemsElement>
<code>92200118</code>
<description>AAAAAAAA</description>
<number>1</number>
<value>15</value>
<taxPercentage>0</taxPercentage>
<currencyCode>EUR</currencyCode>
</ItemsElement>
</ArrayOfItemsElement>')
COLUMNS code VARCHAR2 (10) PATH 'code',
description VARCHAR2 (50) PATH 'description',
quantity VARCHAR2 (2) PATH 'number',
VALUE VARCHAR2 (10) PATH 'value',
taxPercentage VARCHAR2 (5) PATH 'taxPercentage',
currencyCode VARCHAR2 (3) PATH 'currencyCode') x,
(SELECT ROWNUM, COLUMN_VALUE
FROM TABLE (UTILS.SPLIT ('92200118;92100141;92100999', ';'))) a
WHERE a.COLUMN_VALUE = x.code
ORDER BY row_rank));
Note: Utils.Split is a function that creates a table using ';' as a separator. xml_utils.getXmlTag create a XML tag with the value assigned.
Just check!
UPDATE(06/02/2023): The last solution didn't work because the DISTINCT will order by code and not by the order of string codes. So we need to remove duplicates in string codes and apply a rank.
Related
We have 2 tables as shown below:
Table A:
ROWNUM
description
1
{"to": "+1111", "from": "9999"}
2
{"to": "+5555", "from": "8888"}
Table B:
COL1
COL2
+1111
222
+5555
666
Please help me with an Oracle query which replaces part of the description column present in Table A from above table.
The numbers present after text "to:" i.e., +1111 and +5555 of Table A (description column)should be compared with COL1 of Table B and replace with corresponding COL2 value.
For example : replace +1111 with 222 in Table A
replace +5555 with 666 in Table A
Table A should look like this post running of the query.
Table A:
ROWNUM
description
1
{"to": "222", "from": "9999"}
2
{"to": "666", "from": "8888"}
Thanks in advance :)
You can use techniques dedicated to JSON within a PL/SQL code values such as
DECLARE
v_jsoncol tableA.description%TYPE;
v_json_obj json_object_t;
v_new_jsoncol tableA.description%TYPE;
v_col1 tableB.col1%TYPE;
v_col2 VARCHAR2(25);
l_key_list json_key_list;
BEGIN
FOR c IN
(
SELECT *
FROM tableA
)
LOOP
v_json_obj := TREAT(json_element_t.parse(c.description) AS json_object_t);
l_key_list := v_json_obj.get_keys;
FOR i IN 1 .. l_key_list.COUNT
LOOP
IF l_key_list (i) = 'to' THEN
v_col1 := v_json_obj.get_string (l_key_list (i));
SELECT TO_CHAR(col2)
INTO v_col2
FROM tableB
WHERE col1 = v_col1;
v_json_obj.put(l_key_list (i),v_col2);
v_new_jsoncol := v_json_obj.to_string;
UPDATE tableA SET description = v_new_jsoncol WHERE row_num = c.row_num;
END IF;
END LOOP;
END LOOP;
END;
/
Demo
I used instr to get 3rd and 4th " chars position to get the value inside and replace it with other query .
Note : ROWNUM , description is reserved keywords, so i advise not to use them as column names
here is the final code:
SELECT ROWNUM ,
REPLACE (description ,
SUBSTR( description , INSTR(description, '"', 1, 3)+1,
INSTR(description, '"', 1, 4) - INSTR(description, '"', 1, 3)-1) ,
(select COL2 from tblB where COL1 =
SUBSTR( description , INSTR(description, '"', 1, 3)+1,
INSTR(description, '"', 1, 4) - INSTR(description, '"', 1, 3)-1)
)
)
from tblA
Don't use string functions for this. You should use JSON functions and can use JSON_MERGEPATCH:
MERGE INTO table_a dst
USING (
SELECT a.ROWID AS rid,
b.col2
FROM table_a a
INNER JOIN table_b b
ON JSON_VALUE(a.description, '$.to' RETURNING VARCHAR2(10)) = b.col1
) src
ON (dst.ROWID = src.RID)
WHEN MATCHED THEN
UPDATE
SET description = JSON_MERGEPATCH(
dst.description,
JSON_OBJECT(KEY 'to' VALUE src.col2)
);
Which, for your sample data:
CREATE TABLE Table_A (description CLOB CHECK (description IS JSON));
INSERT INTO table_a (description)
SELECT '{"to": "+1111", "from": "9999"}' FROM DUAL UNION ALL
SELECT '{"to": "+5555", "from": "8888"}' FROM DUAL;
CREATE TABLE Table_B (COL1, COL2) AS
SELECT '+1111', 222 FROM DUAL UNION ALL
SELECT '+5555', 666 FROM DUAL;
Then:
SELECT * FROM table_a;
Outputs:
DESCRIPTION
{"to":222,"from":"9999"}
{"to":666,"from":"8888"}
db<>fiddle here
have some PLSQL code that generates a list of dates from a range, which seems to be working fine.
As part of a larger project I want to generate a procedure that will create a list of absences for each employee.
My first step is to use the MINUS command to remove all the holidays, which fall into the range of dates. Is there an easy way of doing this instead of comparing each holiday one at a time (there maybe several in the table) against the GENERATED dates.
If possible, I would prefer breaking all these tasks into small procedures or functions for easy debugging and legibility.
If there is an easier way to do this I am open to all suggestions. Thanks in advance for your help, expertise and patience.
ALTER SESSION SET
NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
create table holidays(
holiday_date DATE,
holiday_name VARCHAR2(20)
);
INSERT into holidays
(holiday_date,
holiday_name)
VALUES
(
TO_DATE('2021/07/21 00:00:00', 'yyyy/mm/dd hh24:mi:ss'), 'July 21 2021');
CREATE OR REPLACE PROCEDURE generate_dates
(
p_start_date IN DATE,
p_end_date IN DATE
)
AS
l_day DATE := p_start_date;
BEGIN
WHILE l_day <= p_end_date
LOOP
DBMS_OUTPUT.PUT_LINE(l_day);
l_day := l_day + 1;
END LOOP;
END generate_dates;
EXEC generate_dates(TRUNC(SYSDATE),TRUNC(SYSDATE+10));
Create table employees(
employee_id NUMBER(6),
first_name VARCHAR2(20),
last_name VARCHAR2(20),
card_num VARCHAR2(10),
work_days VARCHAR2(7)
);
ALTER TABLE employees
ADD ( CONSTRAINT employees_pk
PRIMARY KEY (employee_id));
INSERT INTO employees
(
EMPLOYEE_ID,
first_name,
last_name,
card_num,
work_days
)
WITH names AS (
SELECT 1, 'Jane', 'Doe', 'F123456', 'NYYYYYN' FROM dual UNION ALL
SELECT 2, 'Madison', 'Smith', 'R33432','NYYYYYN'
FROM dual UNION ALL
SELECT 3, 'Justin', 'Case', 'C765341','NYYYYYN'
FROM dual UNION ALL
SELECT 4, 'Mike', 'Jones', 'D564311','NYYYYYN' FROM dual
) SELECT * FROM names
-- check to see if working for that day. Byte=Y for Yes
SELECT SUBSTR( work_days, to_char(TRUNC(SYSDATE), 'D'),1) Work_Day
FROM employees
create table timeoff(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
employee_id NUMBER(6),
timeoff_date DATE,
timeoff_type VARCHAR2(1),
constraint timeoff_chk check (timeoff_date=trunc(timeoff_date, 'dd')),
constraint timeoff_pk primary key (employee_id, timeoff_date)
);
INSERT INTO timeoff (EMPLOYEE_ID,TIMEOFF_DATE,TIMEOFF_TYPE
)
WITH dts AS (
SELECT 1, to_date('20210726 00:00:00','YYYYMMDD HH24:MI:SS'),'V' FROM dual UNION ALL
SELECT 2, to_date('20210726 00:00:00','YYYYMMDD HH24:MI:SS'),'V' FROM dual UNION ALL
SELECT 2, to_date('20210727 00:00:00','YYYYMMDD HH24:MI:SS'),'V' FROM dual )
SELECT * FROM dts
CREATE TABLE emp_attendance(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
employee_id NUMBER(6),
start_date DATE,
end_date DATE,
week_number NUMBER(2),
create_date DATE DEFAULT SYSDATE
);
create table absences(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
employee_id NUMBER(6),
absent_date DATE,
constraint absence_chk check (absent_date=trunc(absent_date, 'dd')),
constraint absence_pk primary key (employee_id, absent_date)
);
INSERT INTO emp_attendance ( EMPLOYEE_ID, START_DATE,END_DATE,WEEK_NUMBER)
WITH dts AS (
SELECT 1, to_date('20210728 13:10:00','YYYYMMDD HH24:MI:SS'),
to_date('20210728 23:15:00','YYYYMMDD HH24:MI:SS'), 30 FROM dual UNION ALL
SELECT 2, to_date('20210728 12:10:10','YYYYMMDD HH24:MI:SS'),
to_date('20210728 20:15:01','YYYYMMDD HH24:MI:SS'), 30 FROM dual)
SELECT * FROM dts
CREATE OR REPLACE TYPE obj_date IS OBJECT (
date_val DATE
);
CREATE OR REPLACE TYPE nt_date IS TABLE OF obj_date;
CREATE OR REPLACE FUNCTION generate_dates(
p_from IN DATE
,p_to IN DATE)
RETURN nt_date PIPELINED
IS
-- normalize inputs to be as-of midnight
v_from DATE :=
TRUNC(NVL(p_from, SYSDATE));
v_to DATE := TRUNC(NVL(p_to, SYSDATE));
BEGIN
LOOP
EXIT WHEN v_from > v_to;
PIPE ROW (obj_date(v_from));
v_from := v_from + 1; -- next. calendar day
END LOOP;
RETURN;
END generate_dates;
INSERT INTO absences
(employee_id, absent_date)
SELECT e.employee_id,
c.date_val
FROM employees e
INNER JOIN table(generate_dates(date '2021-07-20', DATE '2021-07-31')) c
PARTITION BY ( e.employee_id )
ON (SUBSTR(e.work_days,
TRUNC(c.date_val) -
TRUNC(c.date_val, 'IW') + 1, 1) = 'Y')
WHERE NOT EXISTS (
SELECT 1
FROM holidays h
WHERE c.date_val = h.holiday_date
)
AND NOT EXISTS(
SELECT 1
FROM timeoff t
WHERE e.employee_id = t.employee_id
AND t.timeoff_date = c.date_val
)
AND NOT EXISTS(
SELECT 1
FROM emp_attendance ea
WHERE e.employee_id = ea.employee_id
AND TRUNC(ea.start_date) = c.date_val
)
ORDER BY
e.employee_id,
c.date_val
;
Don't use lots of procedures and/or a functions; just use a single query:
SELECT e.employee_id,
c.day
FROM employees e
INNER JOIN (
WITH calendar ( start_date, end_date ) AS (
SELECT DATE '2021-07-01', DATE '2021-07-30' FROM DUAL
UNION ALL
SELECT start_date + 1, end_date
FROM calendar
WHERE start_date + 1 <= end_date
)
SELECT start_date AS day
FROM calendar
) c
PARTITION BY ( e.employee_id )
ON (SUBSTR(e.work_days, TRUNC(c.day) - TRUNC(c.day, 'IW') + 1, 1) = 'Y')
WHERE NOT EXISTS (
SELECT 1
FROM holidays h
WHERE c.day = h.holiday_date
)
AND NOT EXISTS(
SELECT 1
FROM timeoff t
WHERE e.employee_id = t.employee_id
AND t.timeoff_date = c.day
)
ORDER BY
e.employee_id,
c.day
Notes:
This assumes that your work_days column aligns with the ISO week; if it does not then you will need to adjust the substring.
Do not use TO_CHAR(date_value, 'D') as users will get different results depending on their NLS_TERRITORY session setting.
db<>fiddle here
I have a Oracle procedure inside a package like this
PROCEDURE getEmployee
(
pinLanguage IN VARCHAR2,
pinPage IN NUMBER,
pinPageSize IN NUMBER,
pinSortColumn IN VARCHAR2,
pinSortOrder IN VARCHAR2,
poutEmployeeCursor OUT SYS_REFCURSOR
)
AS
BEGIN
OPEN poutEmployeeCursor FOR
SELECT * FROM (
SELECT EMPLOYEE_ID, USERNAME, FULL_NAME, DATE_OF_BIRTH, EMP.GENDER_ID, GEN_TR.GENDER, EMP.WORK_TYPE_ID, WT_TR.WORK_TYPE, SALARY, EMAIL, PROFILE_IMAGE,
ROW_NUMBER() OVER (ORDER BY EMPLOYEE_ID ASC) RN
FROM EMPLOYEES EMP
INNER JOIN GENDERS GEN ON EMP.GENDER_ID = GEN.GENDER_ID
LEFT JOIN GENDERS_MLD GEN_TR ON GEN.GENDER_ID = GEN_TR.GENDER_ID AND GEN_TR.LANGUAGE = pinLanguage
INNER JOIN WORK_TYPES WT ON EMP.WORK_TYPE_ID = WT.WORK_TYPE_ID
LEFT JOIN WORK_TYPES_MLD WT_TR ON WT.WORK_TYPE_ID = WT_TR.WORK_TYPE_ID AND WT_TR.LANGUAGE = pinLanguage
)
WHERE RN BETWEEN (((pinPage - 1) * pinPageSize) + 1) AND (pinPage * pinPageSize);
END;
I need to make the sort order of the above query dynamic
If I pass the text FullName to pinSortColumn parameter, it need to sort FULL_NAME column
If I pass the text DateOfBirth to pinSortColumn parameter, it need to sort DATE_OF_BIRTH column
If I pass the text Gender to pinSortColumn parameter, it need to sort GEN_TR.GENDER column
I can pass the text asc or desc to pinSortOrder parameter and the query need to be sorted accordingly.
Can you please help me to achieve this?
You can use separate order by for asc and desc as following:
ORDER BY
CASE pinSortOrder WHEN 'asc' THEN
CASE pinSortColumn
WHEN 'FullName' THEN FULL_NAME
WHEN 'DateOfBirth' THEN to_char(DATE_OF_BIRTH,'yyyymmddhh24miss')
WHEN 'Gender' THEN GEN_TR.GENDER
END
END,
CASE pinSortOrder WHEN 'desc' THEN
CASE pinSortColumn
WHEN 'FullName' THEN FULL_NAME
WHEN 'DateOfBirth' THEN to_char(DATE_OF_BIRTH,'yyyymmddhh24miss')
WHEN 'Gender' THEN GEN_TR.GENDER
END
END DESC
Let's say you have passed pinSortColumn as FullName and pinSortOrder as asc then order by clause will be ORDER BY FULL_NAME, NULL DESC (please note that default order will be asc so I have not write it in the code. Query will be ordered by FULL_NAME in ascending manner)
Now, If you have passed pinSortColumn as FullName and pinSortOrder as desc then order by clause will be ORDER BY NULL, FULL_NAME DESC.
Null will not impact ordering.
I hope it is clear now.
Cheers!!
Try this one:
WHERE ...
ORDER BY
CASE pinSortColumn
WHEN 'FullName' THEN FULL_NAME
WHEN 'DateOfBirth' THEN DATE_OF_BIRTH
WHEN 'Gender' THEN GEN_TR.GENDER
END;
However, I don't think you can use ASC, DESC is this way. You to create a dynamic query. For an OPEN ... FOR ... statement it is trivial:
sqlstr := 'SELECT * FROM (
SELECT EMPLOYEE_ID, USERNAME, FULL_NAME, DATE_OF_BIRTH, EMP.GENDER_ID, GEN_TR.GENDER, EMP.WORK_TYPE_ID, WT_TR.WORK_TYPE, SALARY, EMAIL, PROFILE_IMAGE,
ROW_NUMBER() OVER (ORDER BY EMPLOYEE_ID ASC) RN
FROM EMPLOYEES EMP
INNER JOIN GENDERS GEN ON EMP.GENDER_ID = GEN.GENDER_ID
LEFT JOIN GENDERS_MLD GEN_TR ON GEN.GENDER_ID = GEN_TR.GENDER_ID AND GEN_TR.LANGUAGE = :pinLanguage
INNER JOIN WORK_TYPES WT ON EMP.WORK_TYPE_ID = WT.WORK_TYPE_ID
LEFT JOIN WORK_TYPES_MLD WT_TR ON WT.WORK_TYPE_ID = WT_TR.WORK_TYPE_ID AND WT_TR.LANGUAGE = :pinLanguage
)
WHERE RN BETWEEN (((:pinPage - 1) * :pinPageSize) + 1) AND (:pinPage * :pinPageSize)
ORDER BY '
CASE pinSortColumn
WHEN 'FullName' THEN sqlstr := sqlstr || 'FULL_NAME ';
WHEN 'DateOfBirth' THEN sqlstr := sqlstr || 'DATE_OF_BIRTH ';
WHEN 'Gender' THEN sqlstr := sqlstr || 'GEN_TR.GENDER ';
END CASE;
sqlstr := sqlstr || pinSortOrder;
OPEN poutEmployeeCursor FOR sqlstr USING pinLanguage, pinLanguage, pinPage, pinPageSize, pinPage, pinPageSize;
Not in case you run Oracle 12c or newer you can use the Row Limiting Clause instead of dealing with row number.
You can avoid all these duplicated cases. Multiply row number by -1 when descending order is required:
order by
case pinSortOrder when 'desc' then -1 else 1 end *
row_number() over (
order by case pinSortColumn when 'FullName' then full_name end,
case pinSortColumn when 'Gender' then gender end,
case pinSortColumn when 'DateOfBirth' then date_of_birth end)
dbfiddle demo
Also this way you do not have to convert data the same type, no need to use to_char.
CREATE TABLE tst_tbl
(
id NUMBER,
last_name VARCHAR2 (50),
first_name VARCHAR2 (50),
dob DATE,
register_dt DATE,
register_loc VARCHAR2 (50),
visit_dt DATE,
visit_loc VARCHAR2 (50),
visit_comments VARCHAR2 (30)
);
INSERT INTO tst_tbl VALUES(1234, 'John', 'Smith', to_date('12/01/1980','MM/DD/YYYY') , to_date('09/05/2017' ,'MM/DD/YYYY') ,'NEW YORK', to_date('02/26/2018','MM/DD/YYYY'), 'NEW JERSEY', '');
INSERT INTO tst_tbl VALUES(1234, 'John', 'Smith', to_date('12/01/1980','MM/DD/YYYY') , to_date('09/05/2017' ,'MM/DD/YYYY') ,'NEW YORK', to_date('2/12/2018', 'MM/DD/YYYY'),'NEW JERSEY', '');
INSERT INTO tst_tbl VALUES(1234, 'John', 'Smith', to_date('12/01/1980','MM/DD/YYYY') , to_date('09/05/2017' ,'MM/DD/YYYY') ,'NEW YORK', to_date('11/6/2017', 'MM/DD/YYYY'),'NEW JERSEY', '');
INSERT INTO tst_tbl VALUES(1234, 'John', 'Smith', to_date('12/01/1980','MM/DD/YYYY') , to_date('09/05/2017' ,'MM/DD/YYYY') ,'NEW YORK', to_date('10/23/2017','MM/DD/YYYY'), 'NEW JERSEY', '');
INSERT INTO tst_tbl VALUES(1234, 'John', 'Smith', to_date('12/01/1980','MM/DD/YYYY') , to_date('09/05/2017' ,'MM/DD/YYYY') ,'NEW YORK', to_date('3/27/2018', 'MM/DD/YYYY'),'NEW JERSEY', '');
INSERT INTO tst_tbl VALUES(1234, 'John', 'Smith', to_date('12/01/1980','MM/DD/YYYY') , to_date('09/05/2017' ,'MM/DD/YYYY') ,'NEW YORK', to_date('3/19/2018', 'MM/DD/YYYY'),'NEW JERSEY', '');
INSERT INTO tst_tbl VALUES(1234, 'John', 'Smith', to_date('12/01/1980','MM/DD/YYYY') , to_date('09/05/2017' ,'MM/DD/YYYY') ,'NEW YORK', to_date('9/11/2017', 'MM/DD/YYYY'),'NEW JERSEY', '');
INSERT INTO tst_tbl VALUES(1234, 'John', 'Smith', to_date('12/01/1980','MM/DD/YYYY') , to_date('2/7/2018' ,'MM/DD/YYYY') , 'NEW YORK',to_date('11/6/2017 ','MM/DD/YYYY'), 'NEW JERSEY', '');
INSERT INTO tst_tbl VALUES(1234, 'John', 'Smith', to_date('12/01/1980','MM/DD/YYYY') , to_date('2/7/2018' ,'MM/DD/YYYY') , 'NEW YORK',to_date('3/19/2018', 'MM/DD/YYYY'),'NEW JERSEY', '');
INSERT INTO tst_tbl VALUES(1234, 'John', 'Smith', to_date('12/01/1980','MM/DD/YYYY') , to_date('2/7/2018' ,'MM/DD/YYYY') , 'NEW YORK',to_date('9/11/2017', 'MM/DD/YYYY'),'NEW JERSEY', '');
INSERT INTO tst_tbl VALUES(1234, 'John', 'Smith', to_date('12/01/1980','MM/DD/YYYY') , to_date('2/7/2018' ,'MM/DD/YYYY') , 'NEW YORK',to_date('3/27/2018', 'MM/DD/YYYY'),'NEW JERSEY', '');
INSERT INTO tst_tbl VALUES(1234, 'John', 'Smith', to_date('12/01/1980','MM/DD/YYYY') , to_date('2/7/2018' ,'MM/DD/YYYY') , 'NEW YORK',to_date('2/26/2018', 'MM/DD/YYYY'),'NEW JERSEY', '');
INSERT INTO tst_tbl VALUES(1234, 'John', 'Smith', to_date('12/01/1980','MM/DD/YYYY') , to_date('2/7/2018' ,'MM/DD/YYYY') , 'NEW YORK',to_date('10/23/2017','MM/DD/YYYY'), 'NEW JERSEY', '');
INSERT INTO tst_tbl VALUES(1234, 'John', 'Smith', to_date('12/01/1980','MM/DD/YYYY') , to_date('2/7/2018' ,'MM/DD/YYYY') , 'NEW YORK',to_date('2/12/2018', 'MM/DD/YYYY'),'NEW JERSEY', '');
COMMIT;
I want immediate visit information(visit_dt, visit_loc, ..) following a register date.
eg:
1234, John, Smith, 12/01/1980, 09/05/2017, NEW YORK, 9/11/2017, NEW JERSEY
1234, John, Smith, 12/01/1980, 2/7/2018, NEW YORK, 2/12/2018, NEW JERSEY
I tried with the below logic to sort the register dates and visit dates and then use lead to retrieve the following date and filter only for register dates. But, i am unable to add other fields as shown above..
SELECT
dt, vst_type, register_dt, vst_dt
FROM
(
SELECT
id, dt, vst_type,
dt AS register_dt,
ROW_NUMBER () OVER
(
PARTITION BY id, dt ORDER BY
CASE
WHEN vst_type = 'REGISTER_DT' THEN 1 ELSE 2 END
)
AS vst_dt_rnum,
LEAD(dt) OVER (PARTITION BY id ORDER BY dt) AS vst_dt
FROM
(
SELECT id, register_dt AS dt, 'REGISTER_DT' vst_type FROM tst_tbl
UNION
SELECT id, visit_dt AS dt, 'VISIT_DT' vst_type FROM tst_tbl
)
)
WHERE vst_dt_rnum = 1 AND vst_type = 'REGISTER_DT'
If I understand correctly, and assuming that the ID is a unique identifier, you want to group the rows by (ID, REGISTER_DT) and from each group to keep just the rows with VISIT_DT >= REGISTER_DT, and then from each group select the earliest (oldest) row by VISIT_DT. If so, something like this should work:
select (columns you want)
from (
select t.*
, row_number() over (partition by id, register_dt
order by visit_dt) as rn
from tst_tbl t
where visit_dt >= register_dt
)
where rn = 1
;
I feel like you're overcomplicating things. Here's a way you can add an indicator if the visit date follows the register date by <30 days. If there's some reason this doesn't work for you, please edit your question and add more data (e.g. what should the output look like?)
select r.*,
case when (floor(visit_dt - register_dt) between 0 and 29)
then 'Y' else 'N'
end as less_than_30_days
from tst_tbl r;
But that's based on your sample output. Based on your query, it looks like you're trying to look for all visits which have any register date within the past 30 days (for that ID). If you'd prefer that instead, here's a simpler way to do it.
select v.*,
(select nvl(max('Y'),'N')
from tst_tbl r
where r.id = v.id
and (floor(v.visit_dt - r.register_dt) between 0 and 29))
as reg_within_30_days
from tst_tbl v;
Try this
Select id, last_name, first_name, dob, register_dt, register_loc,
visit_dt
From (
Select r.id, r.last_name, r.first_name, r.dob, r.register_dt, r.register_loc,
r.visit_dt, r.visit_loc, row_number() over (partition by r.register_dt order by r.visit_dt) r
From tst_tbl r
where register_dt < visit_dt
) x
Where x.r = 1
Have to compare the data differences between the below two tables. I have achieved this by writing a MINUS query but that does not fit for current assignment. Because few tables have 50- 60 columns and each time have to mention the columns before execution.
I have followed Expert's response and not succeeded in achieving the goal. Basically I want to write a procedure which:
Accepts both table names as parameters.
Fetch all the columns of CustomerTable.
Then MINUS query between CustomerTable and StagingCustTable only with the columns fetched in step-2.
Logging any differences.
CustomerTable
Custromer_Number
Address
order_Number
Contact
Country
Post_Code
Amount
StagingCustTable
Custromer_Number
Address
order_Number
Contact
Country
Post_Code
Amount
Run_Id
Record_Id
I would not use a procedure but a query to generate a final query.
Kind of dynamic SQL.
Simple example - let say we have the following tables and data in them:
CREATE TABLE CustomerTable(
Custromer_Number int,
Address varchar2(100),
order_Number int,
Contact int,
Country varchar2(10),
Post_Code varchar2(10),
Amount number
);
INSERT ALL
INTO CustomerTable VALUES (1, 'aaa', 1, 1, 'AA', '111', 111.11 )
INTO CustomerTable VALUES (2, 'bbb', 2, 2, 'BB', '222', 222.22 )
SELECT 1 FROM dual;
CREATE TABLE StagingCustTable
AS SELECT t.*, 1 As run_id, 1 as record_id
FROM CustomerTable t
WHERE 1=0;
INSERT ALL
INTO StagingCustTable VALUES (1, 'aaa', 1, 1, 'AA', '111', 111.11, 1, 1 )
INTO StagingCustTable VALUES (3, 'ccc', 3, 3, 'CC', '333', 333.33, 3, 3 )
SELECT 1 FROM dual;
commit;
Now when you run this simple query:
SELECT 'SELECT ' || listagg( column_name, ',' ) WITHIN GROUP ( ORDER BY column_id )
|| chr(10) || ' FROM ' || max( table_name )
|| chr(10) || ' MINUS '
|| chr(10) || 'SELECT ' || listagg( column_name, ',' ) WITHIN GROUP ( ORDER BY column_id )
|| chr(10) || ' FROM StagingCustTable ' as MySql
FROM user_tab_columns
WHERE table_name = upper( 'CustomerTable' );
you will get the following result:
MYSQL
-------------------------------------------------------------------------
SELECT CUSTROMER_NUMBER,ADDRESS,ORDER_NUMBER,CONTACT,COUNTRY,POST_CODE,AMOUNT
FROM CUSTOMERTABLE
MINUS
SELECT CUSTROMER_NUMBER,ADDRESS,ORDER_NUMBER,CONTACT,COUNTRY,POST_CODE,AMOUNT
FROM StagingCustTable
Now just copy the above query, paste it to your SQL client, run it - and the task is done in a few minutes.