Identify the columns where the value changed from previous version - oracle

I have a table where values in certain columns can change. My requirement is to identify the items(id) where the value has changed. E.g.
Input:
ID VALUE1 VALUE2 VALUE3
1 A B C
1 X B C
2 D E F
2 D E F
3 G H I
3 S H T
Output Needed:
ID VALUE1 VALUE2 VALUE3
1 X
3 S T
I am using Oracle SQL. ANy help will be appreciated

This could be a way, assuming that you need to order your records by some column (I added a row_num column just to explain):
select *
from (
select ID,
case when lag(value1) over (partition by ID order by row_num) != value1 then value1 end as value1,
case when lag(value2) over (partition by ID order by row_num) != value2 then value2 end as value2,
case when lag(value3) over (partition by ID order by row_num) != value3 then value3 end as value3
from yourTable
)
where value1 is not null
or value2 is not null
or value3 is not null
This uses lag to get the value in the preceding row (ordered by row_num) for the same ID and then simply checks if there's at least one difference.
With your sample data, this
with yourTable(row_num, ID, VALUE1, VALUE2, VALUE3) as (
select 1, 1, 'A', 'B', 'C' from dual union all
select 2, 1, 'X', 'B', 'C' from dual union all
select 3, 2, 'D', 'E', 'F' from dual union all
select 4, 2, 'D', 'E', 'F' from dual union all
select 5, 3, 'G', 'H', 'I' from dual union all
select 6, 3, 'S', 'H', 'T' from dual
)
select *
from (
select ID,
case when lag(value1) over (partition by ID order by row_num) != value1 then value1 end as value1,
case when lag(value2) over (partition by ID order by row_num) != value2 then value2 end as value2,
case when lag(value3) over (partition by ID order by row_num) != value3 then value3 end as value3
from yourTable
)
where value1 is not null
or value2 is not null
or value3 is not null
gives
ID VALUE1 VALUE2 VALUE3
---------- ------ ------ ------
1 X
3 S T
2 rows selected.

Your solution needs three parts.
You need to define an order for the changes. In your example it is "from top down to bottom". In your table, you'll have likely a date column or numerical id.
As #Aleksej suggests, you can then access the previous value with LAG(value) OVER (... ORDER BY ...)
You need a comparision function, which handles NULL values correctly. This is a bit painful, and there are more solutions to it, none of which is nice. I'd recommend DECODE(old_value, new_value, 0, 1)=1, see here for other examples.
I've added some extra rows to your table to test the changes involving NULL values:
CREATE TABLE mytable (id NUMBER, value1 VARCHAR2(1), value2 VARCHAR2(1), value3 VARCHAR2(1), t TIMESTAMP DEFAULT SYSTIMESTAMP);
INSERT INTO mytable (id,value1,value2,value3) VALUES (1, 'A','B','C');
INSERT INTO mytable (id,value1,value2,value3) VALUES (1, 'X','B','C');
INSERT INTO mytable (id,value1,value2,value3) VALUES (2, 'D','E','F');
INSERT INTO mytable (id,value1,value2,value3) VALUES (2, 'D','E','F');
INSERT INTO mytable (id,value1,value2,value3) VALUES (3, 'G','H','I');
INSERT INTO mytable (id,value1,value2,value3) VALUES (3, 'S','H','T');
INSERT INTO mytable (id,value1,value2,value3) VALUES (3, 'S','H',NULL);
INSERT INTO mytable (id,value1,value2,value3) VALUES (3, 'S','H',NULL);
INSERT INTO mytable (id,value1,value2,value3) VALUES (3, 'S','U','T');
SELECT ID,
CASE WHEN value1_changed=1 THEN value1 END AS value1,
CASE WHEN value2_changed=1 THEN value2 END AS value2,
CASE WHEN value3_changed=1 THEN value3 END AS value3,
value1_changed,
value2_changed,
value3_changed
FROM (
SELECT id, value1, value2, value3,
DECODE(value1, LAG(value1) OVER (PARTITION BY ID ORDER BY t), 0, 1) value1_changed,
DECODE(value2, LAG(value2) OVER (PARTITION BY ID ORDER BY t), 0, 1) value2_changed,
DECODE(value3, LAG(value3) OVER (PARTITION BY ID ORDER BY t), 0, 1) value3_changed,
row_number() OVER (PARTITION BY ID ORDER BY t) AS r, t
FROM mytable
)
WHERE r > 1
AND value1_changed + value2_changed + value3_changed >= 0;
ID value1 value2 value3 changed1 changed2 changed3
1 X 1 0 0
3 S T 1 0 1
3 0 0 1
3 U 0 0 1
Please not the 3rd line, when value3 changed from 'T' to NULL. It gets correctly reported, but only with it's new value NULL.

Related

ORACLE - How to use LAG to display strings from all previous rows into current row

I have data like below:
group
seq
activity
A
1
scan
A
2
visit
A
3
pay
B
1
drink
B
2
rest
I expect to have 1 new column "hist" like below:
group
seq
activity
hist
A
1
scan
NULL
A
2
visit
scan
A
3
pay
scan, visit
B
1
drink
NULL
B
2
rest
drink
I was trying to solve with LAG function, but LAG only returns one row from previous instead of multiple.
Truly appreciate any help!
Use a correlated sub-query:
SELECT t.*,
(SELECT LISTAGG(activity, ',') WITHIN GROUP (ORDER BY seq)
FROM table_name l
WHERE t."GROUP" = l."GROUP"
AND l.seq < t.seq
) AS hist
FROM table_name t
Or a hierarchical query:
SELECT t.*,
SUBSTR(SYS_CONNECT_BY_PATH(PRIOR activity, ','), 3) AS hist
FROM table_name t
START WITH seq = 1
CONNECT BY
PRIOR seq + 1 = seq
AND PRIOR "GROUP" = "GROUP"
Or a recursive sub-query factoring clause:
WITH rsqfc ("GROUP", seq, activity, hist) AS (
SELECT "GROUP", seq, activity, NULL
FROM table_name
WHERE seq = 1
UNION ALL
SELECT t."GROUP", t.seq, t.activity, r.hist || ',' || r.activity
FROM rsqfc r
INNER JOIN table_name t
ON (r."GROUP" = t."GROUP" AND r.seq + 1 = t.seq)
)
SEARCH DEPTH FIRST BY "GROUP" SET order_rn
SELECT "GROUP", seq, activity, SUBSTR(hist, 2) AS hist
FROM rsqfc
Which, for the sample data:
CREATE TABLE table_name ("GROUP", seq, activity) AS
SELECT 'A', 1, 'scan' FROM DUAL UNION ALL
SELECT 'A', 2, 'visit' FROM DUAL UNION ALL
SELECT 'A', 3, 'pay' FROM DUAL UNION ALL
SELECT 'B', 1, 'drink' FROM DUAL UNION ALL
SELECT 'B', 2, 'rest' FROM DUAL;
All output:
GROUP
SEQ
ACTIVITY
HIST
A
1
scan
null
A
2
visit
scan
A
3
pay
scan,visit
B
1
drink
null
B
2
rest
drink
db<>fiddle here
To aggregate strings in Oracle we use LISAGG function.
In general, you need a windowing_clause to specify a sliding window for analytic function to calculate running total.
But unfortunately LISTAGG doesn't support it.
To simulate this behaviour you may use model_clause of the select statement. Below is an example with explanation.
select
group_
, activity
, seq
, hist
from t
model
/*Where to restart calculation*/
partition by (group_)
/*Add consecutive numbers to reference "previous" row per group.
May use "seq" column if its values are consecutive*/
dimension by (
row_number() over(
partition by group_
order by seq asc
) as rn
)
measures (
/*Other columnns to return*/
activity
, cast(null as varchar2(1000)) as hist
, seq
)
rules update (
/*Apply this rule sequentially*/
hist[any] order by rn asc =
/*Previous concatenated result*/
hist[cv()-1]
/*Plus comma for the third row and tne next rows*/
|| presentv(activity[cv()-2], ',', '') /**/
/*lus previous row's value*/
|| activity[cv()-1]
)
GROUP_ | ACTIVITY | SEQ | HIST
:----- | :------- | --: | :---------
A | scan | 1 | null
A | visit | 2 | scan
A | pay | 3 | scan,visit
B | drink | 1 | null
B | rest | 2 | drink
db<>fiddle here
Few more variants (without subqueries):
SELECT--+ NO_XML_QUERY_REWRITE
t.*,
regexp_substr(
listagg(activity, ',')
within group(order by SEQ)
over(partition by "GROUP")
,'^([^,]+,){'||(row_number()over(partition by "GROUP" order by seq)-1)||'}'
)
AS hist1
,xmlcast(
xmlquery(
'string-join($X/A/B[position()<$Y]/text(),",")'
passing
xmlelement("A", xmlagg(xmlelement("B", activity)) over(partition by "GROUP")) as x
,row_number()over(partition by "GROUP" order by seq) as y
returning content
)
as varchar2(1000)
) hist2
FROM table_name t;
DBFIddle: https://dbfiddle.uk/?rdbms=oracle_21&fiddle=9b477a2089d3beac62579d2b7103377a
Full test case with output:
with table_name ("GROUP", seq, activity) AS (
SELECT 'A', 1, 'scan' FROM DUAL UNION ALL
SELECT 'A', 2, 'visit' FROM DUAL UNION ALL
SELECT 'A', 3, 'pay' FROM DUAL UNION ALL
SELECT 'B', 1, 'drink' FROM DUAL UNION ALL
SELECT 'B', 2, 'rest' FROM DUAL
)
SELECT--+ NO_XML_QUERY_REWRITE
t.*,
regexp_substr(
listagg(activity, ',')
within group(order by SEQ)
over(partition by "GROUP")
,'^([^,]+,){'||(row_number()over(partition by "GROUP" order by seq)-1)||'}'
)
AS hist1
,xmlcast(
xmlquery(
'string-join($X/A/B[position()<$Y]/text(),",")'
passing
xmlelement("A", xmlagg(xmlelement("B", activity)) over(partition by "GROUP")) as x
,row_number()over(partition by "GROUP" order by seq) as y
returning content
)
as varchar2(1000)
) hist2
FROM table_name t;
GROUP SEQ ACTIV HIST1 HIST2
------ ---------- ----- ------------------------------ ------------------------------
A 1 scan
A 2 visit scan, scan
A 3 pay scan,visit, scan,visit
B 1 drink
B 2 rest drink, drink

How do i select a coma seperated list of values into a single column

I have a string of comma separated values "'a', 'b', 'c', 'd'".
select 'a', 'b', 'c', 'd' from dual;
How do i select these into a temp table all in a single column rather than a column per value? So my select would return output like this:
value
a
b
c
d
Note: Doing the normal multiple selects and unions is not allowed due to the comma separated values as the input.
I guess you could do something like this, taking the assumption that you can qualify your columns:
SQL> select val from
( select 'a' as c1, 'b' as c2, 'c' as c3, 'd' as c4 from dual ) unpivot include
nulls ( val for col in ( c1,c2,c3,c4 ) );
V
-
a
b
c
d
In this option, it does not matter what values you have in 'a' , 'b' , 'c' 'd' , as basically unpivot is transposing columns into rows.
Hope it helps
You can use hierarchy query as follows:
SQL> with YOUR_DATA as (select q'#'a', 'b', 'c', 'd'#' as str from dual)
2 -- Your query starts from here
3 SELECT TRIM('''' FROM TRIM(REGEXP_SUBSTR(STR,'[^,]+',1,LEVEL))) AS RESULT
4 FROM YOUR_DATA CONNECT BY
5 LEVEL <= 1 + REGEXP_COUNT(STR,',');
RESULT
------------------
a
b
c
d
SQL>
If you have version 12c or later, and if your data has no weird characters, then:
with input(x) as (select q'#'a', 'b', 'c', 'd'#' from dual)
select x_out from input,
json_table(
'['||replace(x, '''', '"')||']', '$[*]'
columns x_out path '$'
);
X_OUT
a
b
c
d

Insert values from different rows into one row and multiple columns grouping by specific value of column x

Sorry if Im not clear in title, I have some language problems. I have a table (quite big) like
Person value Letter
------------------------------------------
Tom value1 A
Tom value2 T
Ann value1 F
Ann value2 R
Ann value3 Y
Jim value3 W
I would like to shorten it into:
Person value1 value2 value3
------------------------------------------
Tom A T (null)
Ann F R Y
Jim (null) (null) W
Something like listagg, but into different columns. Looks simple but I'm stuck...
edit: There are 8 values, not just 3, I wanted made it simplier, values don't repeat for the same person (but can be null/not appear))
Here's one option: the idea is to use an aggregate function (such as MIN, MAX, SUM). As your sample data contain only 3 values, that's what I did as well - you'd use 5 more lines like this (including them from line #13 onward).
Note that lines #1 - 8 represent your sample data; you already have them stored in the table so you wouldn't be typing that. Code you actually need begins at line #9.
SQL> with test (person, value, letter) as
2 (select 'Tom', 'val1', 'A' from dual union all
3 select 'Tom', 'val2', 'T' from dual union all
4 select 'Ann', 'val1', 'F' from dual union all
5 select 'Ann', 'val2', 'R' from dual union all
6 select 'Ann', 'val3', 'Y' from dual union all
7 select 'Jim', 'val3', 'W' from dual
8 )
9 select
10 person,
11 max(decode(value, 'val1', letter)) value1,
12 max(decode(value, 'val2', letter)) value2,
13 max(decode(value, 'val3', letter)) value3
14 from test
15 group by person
16 order by person;
PERSON VALUE1 VALUE2 VALUE3
------ ------ ------ ------
Ann F R Y
Jim W
Tom A T
SQL>

Oracle Hierarchical Queries

I am struggling with a query with the below requirement:
Table A
ID Name Key
1 A1 Key1
2 A2 Key2
3 A3 Key3
Table B
ID A_ID NAME CONTAINER_A_ID
1 1 B1 NULL
2 1 B2 NULL
3 1 B3 2
4 2 B4 NULL
5 2 B5 NULL
6 3 B6 NULL
7 3 B7 NULL
The Key column in table A is unique
The A_ID column in table B is a foreign key of table A
The CONTAINER_A_ID column in table B means the row in table B can be a
container, it contains other data rows indicated by the CONTAINER_A_ID value.
Below is the example:
the input parameter is table A key column value, let's say A.Key = 'key1', and the result based on the above sample data will be:
A.ID A.NAME A.KEY B.ID B.A_ID B.NAME B.CONTAINER_A_ID
1 A1 KEY1 1 1 B1 NULL
1 A1 KEY1 2 1 B2 NULL
1 A1 KEY1 3 1 B3 2
2 A2 KEY2 4 2 B4 NULL
2 A2 KEY2 5 2 B5 NULL
if the input parameter is A.Key = 'key2', then the result will be:
A.ID A.NAME A.KEY B.ID B.A_ID B.NAME B.CONTAINER_A_ID
2 A2 KEY2 4 2 B4 NULL
2 A2 KEY2 5 2 B5 NULL
Thanks
This is on Oracle 11g.
If you are specifically looking for CONNECT BY I am not aware of that yet.
drop table t1; drop table t2;
create table t1 (id int primary key, name char(5), key char(5));
create table t2 (id int primary key, a_id int, name char(5) , container int);
insert into t1 values (1, 'A1', 'K1');
insert into t1 values (2, 'A2', 'K2');
insert into t1 values (3, 'A3', 'K3');
insert into t2 values (1, 1, 'B1', null);
insert into t2 values (2, 1, 'B2', null);
insert into t2 values (3, 1, 'B3', 2);
insert into t2 values (4, 2, 'B4', null);
insert into t2 values (5, 2, 'B5', null);
insert into t2 values (6, 3, 'B6', null);
insert into t2 values (7, 3, 'B7', null);
with t(id, name, key, bid, aid, bname, con) as (
select a.id, a.name, a.key, b.id, b.a_id, b.name, b.container
from t1 a
inner join
t2 b
on a.id = b.a_id
where a.key = 'K1'
union all
select a.id, a.name, a.key, b.id, b.a_id, b.name, b.container
from t t
inner join
t1 a
on a.id = t.con
inner join
t2 b
on a.id = b.a_id
) select * from t;
EDIT: Reponse to Jorge's comment
insert into t2 values (4, 2, 'B4', 3);
This is for Hierarchical Query
with TableA as
(
select 1 id, 'A1' Name, 'Key1' key from dual union all
select 2, 'A2', 'Key2' from dual union all
select 3, 'A3', 'Key3' from dual
)
, tableb as
(
select 1 id, 1 a_id, 'B1' name , null CONTAINER_A_ID from dual union all
select 2 , 1 , 'B2' , null from dual union all
select 3 , 1 , 'B3' , 2 from dual union all
select 4 , 2 , 'B4' , null from dual union all
select 5 , 2 , 'B5' , null from dual union all
select 6 , 3 , 'B6' , null from dual union all
select 7 , 3 , 'B7' , null from dual
)
select
a.id, a.name, a.key, b.id, b.a_id, b.name, b.container_a_id
from
tableb b
left join
tablea a
on
a.id = b.a_id
start with
A.Key = 'Key1'
connect by
prior b.container_a_id = b.a_id;
If you need order then add order by a.id, b.id,a.name,...; to the end.
CTEs are fine to use in 11g Oracle. I just saw Jorge is in before me. It is easier to see how the recursion works if you just use tableB in the CTE and then join to the CTE to get all the fields, like this
with
recurse (a_id, b_id, parent_id)
as
(select a_id, id, container_a_id as parent_id
from tableB
WHERE A_ID = 1 -- Put your parameter here
union all
select b.a_id, b.id, b.container_a_id
from recurse r, tableB b
where b.a_id = r.parent_id
)
select r.a_id, a.name, a.key, b.id, b.a_id, b.name, b.container_a_id
from recurse r, tableA a, tableB b
where r.a_id = a.id and r.b_id = b.id
;
This gets the same results, but although you have to use a_id and not a_key for the condition, it is a little bit easier to understand the recursion.
So, leaving this here in case it helps someone to understand a bit about CTEs.

find nearest row of different type in oracle

My table looks like
__ Key type timeStamp flag
1 ) 1 B 2015-06-28 22:19:26 Y
2 ) 1 B 2015-06-28 22:20:22 Y
3 ) 1 C 2015-06-28 22:22:06 N
4 ) 1 A 2015-06-28 22:25:11 N
5 ) 1 B 2015-06-28 22:29:44 Y
6 ) 1 A 2015-06-28 22:33:33 N
7 ) 1 B 2015-06-28 22:35:21 N
8 ) 1 B 2015-06-28 22:39:34 Y
9 ) 1 B 2015-06-28 22:43:53 N
10) 1 A 2015-06-28 22:45:53 N
I need to find out all the types of A whose flag='N' with respect to which there exist type B whose timestampOF(B)<timestampOF(A) and Flag(B)='Y' and key(A)=key(B).
note: If there exist two B previous than A than take the B with max timestamp.(ROW[8,9,10] 9 is taken instead of 8)
OUTPUT
__ Key type timeStamp flag
4 ) 1 A 2015-06-28 22:25:11 N
6 ) 1 A 2015-06-28 22:33:33 N
My approach
SELECT *
FROM tab TAB_OUT
WHERE TAB_OUT.TYPE='A'
AND TAB_OUT.FLAG='N'
AND EXISTS(
SELECT *
FROM tab TAB_IN
WHERE TAB_IN.KEY = TAB_OUT.KEY
AND TAB_IN.TYPE='B'
AND TAB_OUT.FLAG='Y'
AND TAB_IN.timestamp<TAB_OUT.timestamp
AND TAB_IN.timestamp = (SELECT MAX(timestamp) from
tab where timestamp< `TAB_OUT.timestamp`)
);
But in this i can not use TAB_OUT.timestamp in third level query. Is there any alternative solution to solve this problem.
In my query note: part is not satisfied as my query as it skips no. 9) and satisfy condition with no. 8)
A solution that only requires a single table scan:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE table_name ( Key, type, timeStamp, flag ) AS
SELECT 1, 'B', CAST( TIMESTAMP '2015-06-28 22:19:26' AS DATE ), 'Y' FROM DUAL
UNION ALL SELECT 1, 'B', CAST( TIMESTAMP '2015-06-28 22:20:22' AS DATE ), 'Y' FROM DUAL
UNION ALL SELECT 1, 'C', CAST( TIMESTAMP '2015-06-28 22:22:06' AS DATE ), 'N' FROM DUAL
UNION ALL SELECT 1, 'A', CAST( TIMESTAMP '2015-06-28 22:25:11' AS DATE ), 'N' FROM DUAL
UNION ALL SELECT 1, 'B', CAST( TIMESTAMP '2015-06-28 22:29:44' AS DATE ), 'Y' FROM DUAL
UNION ALL SELECT 1, 'A', CAST( TIMESTAMP '2015-06-28 22:33:33' AS DATE ), 'N' FROM DUAL
UNION ALL SELECT 1, 'B', CAST( TIMESTAMP '2015-06-28 22:35:21' AS DATE ), 'N' FROM DUAL
UNION ALL SELECT 1, 'B', CAST( TIMESTAMP '2015-06-28 22:39:34' AS DATE ), 'Y' FROM DUAL
UNION ALL SELECT 1, 'B', CAST( TIMESTAMP '2015-06-28 22:43:53' AS DATE ), 'N' FROM DUAL
UNION ALL SELECT 1, 'A', CAST( TIMESTAMP '2015-06-28 22:45:53' AS DATE ), 'N' FROM DUAL
Query 1:
SELECT Key,
type,
timeStamp,
flag
FROM (
SELECT Key,
type,
timeStamp,
flag,
LAG( CASE WHEN type = 'B' THEN flag END ) IGNORE NULLS OVER ( PARTITION BY Key ORDER BY timeStamp ) AS prev_b_flag
FROM table_name t
WHERE type IN ( 'A', 'B' )
)
WHERE type = 'A'
AND flag = 'N'
AND prev_b_flag = 'Y'
Results:
| KEY | TYPE | TIMESTAMP | FLAG |
|-----|------|------------------------|------|
| 1 | A | June, 28 2015 22:25:11 | N |
| 1 | A | June, 28 2015 22:33:33 | N |
SELECT
*
FROM
tab A
WHERE
flag = 'N' AND type = 'A'
AND EXISTS (
SELECT
NULL
FROM
tab B
WHERE
type = 'B'
AND A.timestamp > timestamp AND A.Key = Key
GROUP BY
Key
HAVING
MAX(flag) KEEP (DENSE_RANK LAST ORDER BY timestamp) = 'Y'
);
There is no need to make correlated query to select flag from the the last record. Using aggregate KEEP clause is more efficient way. In this case it sort the groups by timestamp and keeps only the last value for the aggregation (last timestamp you wanted), so there comes only single record to the MAX function and we just take the FLAG value from it.
Here is simple example:
WITH sample (value1, value2) AS (
SELECT 1, 'Y' FROM DUAL UNION ALL
SELECT 2, 'X' FROM DUAL
)
SELECT
MIN(value2) KEEP (DENSE_RANK LAST ORDER BY value1) value2
FROM
sample
This returns value2 from the record with highest value1.

Resources