Comparing two rows in oracle without using lead and lag functions - sql

how to compare two rows in a same table without using lead/lag functions in oracle?
I tried the self join of the table and using row number function but it didnt work for me.
suppose table is T1 and the data is stored in the format as mentioned below.
ID Name Address Date
1 A Noida 10-Apr-2019
1 A Gurugram 15-Apr-2019
1 A Mumbai 18-Apr-2019
and i want the output data to be like.
ID Name Old_value New_value Date
1 A Noida 10-Apr-2019
1 A Noida Gurugram 15-Apr-2019
1 A Gurugram Mumbai 18-Apr-2019

Here's one option:
SQL> with test (id, name, address, cdate) as
2 (select 1, 'A', 'Noida' , date '2019-04-10' from dual union all
3 select 1, 'A', 'Gurugram', date '2019-04-15' from dual union all
4 select 1, 'A', 'Mumbai' , date '2019-04-18' from dual
5 ),
6 temp as
7 (select t.*,
8 row_number() over (partition by name order by cdate) rn
9 from test t
10 )
11 select a.id, a.name, b.address old_value, a.address new_value, a.cdate
12 from temp a left join temp b on a.id = b.id and a.rn = b.rn + 1
13 order by a.cdate;
ID N OLD_VALU NEW_VALU CDATE
---------- - -------- -------- ----------
1 A Noida 10.04.2019
1 A Noida Gurugram 15.04.2019
1 A Gurugram Mumbai 18.04.2019
SQL>

If you don't want to use window functions, you can do it with a subquery:
select
t.ID, t.Name,
(select Address from tablename
where ID = t.ID
and "Date" = (
select max("Date")
from tablename
where ID = t.ID and "Date" < t."Date"
)
) Old_value,
t.Address New_value,
t."Date"
from tablename t
See the demo.
Results:
> ID | NAME | OLD_VALUE | NEW_VALUE | Date
> -: | :--- | :-------- | :-------- | :--------
> 1 | A | | Noida | 10-APR-19
> 1 | A | Noida | Gurugram | 15-APR-19
> 1 | A | Gurugram | Mumbai | 18-APR-19

In future questions, I strongly recommend:
Providing test data using CREATE TABLE and INSERT statements
Sharing your precise Oracle database version number.
Starting with version 12c, you can use row pattern matching:
with test (id, name, address, cdate) as
(select 1, 'A', 'Noida' , date '2019-04-10' from dual union all
select 1, 'A', 'Gurugram', date '2019-04-15' from dual union all
select 1, 'A', 'Mumbai' , date '2019-04-18' from dual
)
select * from test
match_recognize(
partition by id, name order by cdate
measures prev(address) old_address
all rows per match
pattern(a)
define a as 1=1
);
ID NAME CDATE OLD_ADDRESS ADDRESS
1 A 2019-04-10 00:00:00 Noida
1 A 2019-04-15 00:00:00 Noida Gurugram
1 A 2019-04-18 00:00:00 Gurugram Mumbai

Related

ORACLE: SQL; Select a distinct records if and only if one column changed

Thanks in advance for your help.
I need to create a query to select distinct columns if and only if another column changed.
However, each record has a date_code which makes every record unique.
I would like to pull the last record before the change as well as the last record after the change.
Here is an example:
A customer record with an account number, company name with a date code stamp. I want to get the last record before a company name change. Even if a customer goes back to the company, the last record for a customer for a specific company would be the last date code.
Table Example:
Expected Result:
You can use MATCH_RECOGNIZE:
SELECT datecode,
customer_account,
company_name
FROM table_name
MATCH_RECOGNIZE (
PARTITION BY customer_account
ORDER BY datecode
MEASURES
LAST( datecode ) AS datecode,
LAST( company_name ) AS company_name
ONE ROW PER MATCH
PATTERN ( company* same_company )
DEFINE
company AS FIRST( company_name ) = NEXT( company_name )
)
Which, for the sample data:
CREATE TABLE table_name ( datecode, customer_account, company_name ) AS
SELECT DATE '2020-11-01', 10001000000004, 'Apple' FROM DUAL UNION ALL
SELECT DATE '2020-11-02', 10001000000004, 'Apple' FROM DUAL UNION ALL
SELECT DATE '2020-11-03', 10001000000004, 'Apple' FROM DUAL UNION ALL
SELECT DATE '2020-11-04', 10001000000004, 'Apple' FROM DUAL UNION ALL
SELECT DATE '2020-11-05', 10001000000004, 'Microsoft' FROM DUAL UNION ALL
SELECT DATE '2020-11-06', 10001000000004, 'Microsoft' FROM DUAL UNION ALL
SELECT DATE '2020-11-07', 10001000000004, 'Google' FROM DUAL UNION ALL
SELECT DATE '2020-11-08', 10001000000004, 'Google' FROM DUAL UNION ALL
SELECT DATE '2020-11-09', 10001000000004, 'Apple' FROM DUAL UNION ALL
SELECT DATE '2020-11-10', 10001000000004, 'Apple' FROM DUAL;
Outputs:
DATECODE | CUSTOMER_ACCOUNT | COMPANY_NAME
:------------------ | ---------------: | :-----------
2020-11-04 00:00:00 | 10001000000004 | Apple
2020-11-06 00:00:00 | 10001000000004 | Microsoft
2020-11-08 00:00:00 | 10001000000004 | Google
2020-11-10 00:00:00 | 10001000000004 | Apple
db<>fiddle here
You can use analytic functions LAG/LEAD to calculate next/previous row's value and compare it with current value. This will also work in many other databases.
Fiddle here.
with q as (
select a.*,
lead(company_name, 1, '__')
over(partition by customer_account order by datecode asc) as next_comp
from cust_account a
)
select
datecode,
customer_account,
company_name
from q
where company_name != next_comp
order by datecode asc
+---------+------------------+--------------+
|DATECODE | CUSTOMER_ACCOUNT | COMPANY_NAME |
+---------+------------------+--------------+
|20201104 | 10001000000004 | Apple |
|20201106 | 10001000000004 | Microsoft |
|20201108 | 10001000000004 | Google |
|20201110 | 10001000000004 | Apple |
+---------+------------------+--------------+
You can use simple lead function as mentioned in one of the answers with simple case..when as follows:
select * from
(select t.*,
case
when lead(company_name) over(partition by customer_account order by datecode) = company_name
then 0
else 1
end as result
from cust_account t)
where result = 1

how to write sql query to select rows with max value in one column

My Table looks like this.
Id | Name | Ref | Date | From
10 | Ant | 100 | 2017-02-02 | David
10 | Ant | 300 | 2016-01-01 | David
2 | Cat | 90 | 2017-09-09 | David
2 | Cat | 500 | 2016-02-03 | David
3 | Bird | 150 | 2017-06-28 | David
This is the result I want.
Id | Name | Ref | Date | From
3 | Bird | 150 | 2017-06-28 | David
2 | Cat | 500 | 2016-02-03 | David
10 | Ant | 300 | 2016-01-01 | David
My target is the highest Ref per Id, ordered by Order Date desc.
Could you please tell me about how to write a sql query using pl/sql.
This kind of requirement (where you need the max or min by one column, grouped by another, but you need all the data from the max or min row) is pretty much what analytic functions are for. I used row_number - if ties are possible, you need to clarify the assignment (see my Comment under your question), and depending on the details, another analytic function may be more appropriate - perhaps rank().
with
my_table ( id, name, ref, dt, frm ) as (
select 10, 'Ant' , 100, date '2017-02-02', 'David' from dual union all
select 10, 'Ant' , 300, date '2016-01-01', 'David' from dual union all
select 2, 'Cat' , 90, date '2017-09-09', 'David' from dual union all
select 2, 'Cat' , 500, date '2016-02-03', 'David' from dual union all
select 3, 'Bird', 150, date '2017-06-28', 'David' from dual
)
-- End of simulated table (for testing purposes only, not part of the solution).
-- SQL query begins BELOW THIS LINE.
select id, name, ref, dt, frm
from (
select id, name, ref, dt, frm,
row_number() over (partition by id order by ref desc, dt desc) as rn
from my_table
)
where rn = 1
order by dt desc
;
ID NAME REF DT FRM
-- ---- --- ---------- -----
3 Bird 150 2017-06-28 David
2 Cat 500 2016-02-03 David
10 Ant 300 2016-01-01 David
You can use this
SELECT
Id
,Name
,Ref
,[Date]
FROM(
SELECT
*
, ROW_NUMBER() OVER(PARTITION BY ID ORDER BY Ref DESC) AS Row#
FROM yourtable
) A WHERE Row# = 1
ORDER BY A.[Date] DESC
Another solution with a self join (Idea came from here: How can I SELECT rows with MAX(Column value), DISTINCT by another column in SQL? ):
with
my_table ( id, name, ref, dt, frm ) as (
select 10, 'Ant' , 100, date '2017-02-02', 'David' from dual union all
select 10, 'Ant' , 300, date '2016-01-01', 'David' from dual union all
select 10, 'Ant' , 300, date '2015-01-01', 'David' from dual union all
select 2, 'Cat' , 90, date '2017-09-09', 'David' from dual union all
select 2, 'Cat' , 500, date '2016-02-03', 'David' from dual union all
select 3, 'Bird', 150, date '2017-06-28', 'David' from dual
)
-- End of simulated table (for testing purposes only, not part of the solution).
-- SQL query begins BELOW THIS LINE.
select m1.*
from my_table m1
left join my_table m2
on m1.id = m2.id and (
-- this is basically a comparator: order by ref desc, dt desc
m1.ref < m2.ref or (
m1.ref = m2.ref and
m1.dt < m2.dt
)
) where m2.id is null order by m1.dt desc
;
ID NAME REF DT FRM
---------- ---- ---------- --------- -----
3 Bird 150 28-JUN-17 David
2 Cat 500 03-FEB-16 David
10 Ant 300 01-JAN-16 David
Use the "better than" SQL principal:
select a.Id, a.Name, a.Ref, a.Dt, a.frm
from table_name a
left join table_name b on a.id = b.id and b.ref > a.ref -- b.ref > a.ref would make b.ref "better" that a
where b.id is null -- Now check and make sure there is nothing "better"
group by a.id;
SELECT Id, Name, Max(Ref) as Ref, Min(`Date`) as `Date`
From Forge
Group By Id, Name
Order by Min(`Date`) desc;

In Oracle, how to select specific row while aggregating all rows

I have a requirement that I need to both aggregate all rows by id, and find 1 specific row among the rows of the same id. It's like 2 SQL queries, but I want to make it in 1 SQL query. I'm using Oracle database.
for example,table t1 whose data looks like:
id | name | num
----- -------- -------
1 | 'a' | 1
2 | 'b' | 3
2 | 'c' | 6
2 | 'd' | 6
I want to aggregate the data by the id, find the 'name' with the highest 'count', and sum all count of the id to 'total_count'.
There are 2 rows with same num, pick up the first one.
id | highest_num | name_of_highest_num | total_num | avg_num
----- ------------- --------------------- ------------ -------------------
1 | 1 | 'a' | 1 | 1
2 | 6 | 'c' | 15 | 5
Can I get this result by 1 Oracle SQL query?
Thanks in advance for any replies.
Oracle Setup:
CREATE TABLE table_name ( id, name, num ) AS
SELECT 1, 'a', 1 FROM DUAL UNION ALL
SELECT 2, 'b', 3 FROM DUAL UNION ALL
SELECT 2, 'c', 6 FROM DUAL UNION ALL
SELECT 2, 'd', 6 FROM DUAL;
Query:
SELECT id,
MAX( num ) AS highest_num,
MAX( name ) KEEP ( DENSE_RANK LAST ORDER BY num ) AS name_of_highest_num,
SUM( num ) AS total_num,
AVG( num ) AS avg_num
FROM table_name
GROUP BY id
Output:
ID HIGHEST_NUM NAME_OF_HIGHEST_NUM TOTAL_NUM AVG_NUM
-- ----------- ------------------- --------- -------
1 1 a 1 1
2 6 d 15 5
Here's one option using row_number in a subquery with conditional aggregation:
select id,
max(num) as highest_num,
max(case when rn = 1 then name end) as name_of_highest_num,
sum(num) as total_num,
avg(num) as avg_num
from (
select id, name, num,
row_number() over (partition by id order by num desc) rn
from a
) t
group by id
SQL Fiddle Demo
Sounds like you want to use some analytic functions. Something like this should work
select id,
num highest_num,
name name_of_highest_num,
total total_num,
average avg_num
from (select id,
num,
name,
rank() over (partition by id
order by num desc, name asc) rnk,
sum(num) over (partition by id) total,
avg(num) over (partition by id) average
from table t1)
where rnk = 1

Difference between the values of multiple rows in SQL

My table in SQL is like:-
RN Name value1 value2 Timestamp
1 Mark 110 210 20160119
1 Mark 106 205 20160115
1 Mark 103 201 20160112
2 Steve 120 220 20151218
2 Steve 111 210 20151210
2 Steve 104 206 20151203
Desired Output:-
RN Name value1Lag1 value1lag2 value2lag1 value2lag2
1 Mark 4 3 5 4
2 Steve 9 7 10 4
The difference is calculated from the most recent to the second recent and then from second recent to the third recent for RN 1
value1lag1 = 110-106 =4
value1lag2 = 106-103 = 3
value2lag1 = 210-205 = 5
value2lag2 = 205-201 = 4
similarly for other RN's also.
Note: For each RN there are 3 and only 3 rows.
I have tried in several ways by taking help from similar posts but no luck.
I've assumed that RN and Name are linked here. It's a bit messy, but if each RN always has 3 values and you always want to check them in this order, then something like this should work.
SELECT
t1.Name
, AVG(CASE WHEN table_ranked.Rank = 1 THEN table_ranked.value1 ELSE NULL END) - AVG(CASE WHEN table_ranked.Rank = 2 THEN table_ranked.value1 ELSE NULL END) value1Lag1
, AVG(CASE WHEN table_ranked.Rank = 2 THEN table_ranked.value1 ELSE NULL END) - AVG(CASE WHEN table_ranked.Rank = 3 THEN table_ranked.value1 ELSE NULL END) value1Lag2
, AVG(CASE WHEN table_ranked.Rank = 1 THEN table_ranked.value2 ELSE NULL END) - AVG(CASE WHEN table_ranked.Rank = 2 THEN table_ranked.value2 ELSE NULL END) value2Lag1
, AVG(CASE WHEN table_ranked.Rank = 2 THEN table_ranked.value2 ELSE NULL END) - AVG(CASE WHEN table_ranked.Rank = 3 THEN table_ranked.value2 ELSE NULL END) value2Lag2
FROM table t1
INNER JOIN
(
SELECT
t1.Name
, t1.value1
, t1.value2
, COUNT(t2.TimeStamp) Rank
FROM table t1
INNER JOIN table t2
ON t2.name = t1.name
AND t1.TimeStamp <= t2.TimeStamp
GROUP BY t1.Name, t1.value1, t1.value2
) table_ranked
ON table_ranked.Name = t1.Name
GROUP BY t1.Name
There are other answers here, but I think your problem is calling for analytic functions, specifically LAG():
select
rn,
name,
-- calculate the differences
value1 - v1l1 value1lag1,
v1l1 - v1l2 value1lag2,
value2 - v2l1 value2lag1,
v2l1 - v2l2 value2lag2
from (
select
rn,
name,
value1,
value2,
timestamp,
-- these two are the values from the row before this one ordered by timestamp (ascending)
lag(value1) over(partition by rn, name order by timestamp asc) v1l1,
lag(value2) over(partition by rn, name order by timestamp asc) v2l1
-- these two are the values from two rows before this one ordered by timestamp (ascending)
lag(value1, 2) over(partition by rn, name order by timestamp asc) v1l2,
lag(value2, 2) over(partition by rn, name order by timestamp asc) v2l2
from (
select
1 rn, 'Mark' name, 110 value1, 210 value2, '20160119' timestamp
from dual
union all
select
1 rn, 'Mark' name, 106 value1, 205 value2, '20160115' timestamp
from dual
union all
select
1 rn, 'Mark' name, 103 value1, 201 value2, '20160112' timestamp
from dual
union all
select
2 rn, 'Steve' name, 120 value1, 220 value2, '20151218' timestamp
from dual
union all
select
2 rn, 'Steve' name, 111 value1, 210 value2, '20151210' timestamp
from dual
union all
select
2 rn, 'Steve' name, 104 value1, 206 value2, '20151203' timestamp
from dual
) data
)
where
-- return only the rows that have defined values
v1l1 is not null and
v1l2 is not null and
v2l1 is not null and
v2l1 is not null
This approach has the benefit that Oracle does all the necessary buffering internally, avoiding self-joins and the like. For big data sets this can be important from a performance viewpoint.
As an example, the explain plan for that query would be something like
-------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 6 | 150 | 13 (8)| 00:00:01 |
|* 1 | VIEW | | 6 | 150 | 13 (8)| 00:00:01 |
| 2 | WINDOW SORT | | 6 | 138 | 13 (8)| 00:00:01 |
| 3 | VIEW | | 6 | 138 | 12 (0)| 00:00:01 |
| 4 | UNION-ALL | | | | | |
| 5 | FAST DUAL | | 1 | | 2 (0)| 00:00:01 |
| 6 | FAST DUAL | | 1 | | 2 (0)| 00:00:01 |
| 7 | FAST DUAL | | 1 | | 2 (0)| 00:00:01 |
| 8 | FAST DUAL | | 1 | | 2 (0)| 00:00:01 |
| 9 | FAST DUAL | | 1 | | 2 (0)| 00:00:01 |
| 10 | FAST DUAL | | 1 | | 2 (0)| 00:00:01 |
-------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("V1L1" IS NOT NULL AND "V1L2" IS NOT NULL AND "V2L1" IS
Note that there are no joins, just a WINDOW SORT that buffers the necessary data from the "data source" (in our case, the VIEW 3 that is the UNION ALL of our SELECT ... FROM DUAL) to partition and calculate the different lags.
if just in this case, it's not that difficult.you need 2 steps
self join and get the result of minus
select t1.RN,
t1.Name,
t1.rm,
t2.value1-t1.value1 as value1,
t2.value2-t1.value2 as value2
from
(select RN,Name,value1,value2,
row_number(partition by Name order by Timestamp desc) as rm from table)t1
left join
(select RN,Name,value1,value2,
row_number(partition by Name order by Timestamp desc) as rm from table) t2
on t1.rm = t2.rm-1
where t2.RN is not null.
you set this as a table let's say table3.
2.you pivot it
select * from (
select t3.RN, t3.Name,t3.rm,t3.value1,t3.value2 from table3 t3
)
pivot
(
max(value1)
for rm in ('1','2')
)v1
3.you get 2 pivot table for value1 and value2 join them together to get the result.
but i think there may be a better way and i m not sure if we can just join pivot when we pivot it so i ll use join after i get the pivot result that will make 2 more tables. its not good but the best i can do
-- test data
with data(rn,
name,
value1,
value2,
timestamp) as
(select 1, 'Mark', 110, 210, to_date('20160119', 'YYYYMMDD')
from dual
union all
select 1, 'Mark', 106, 205, to_date('20160115', 'YYYYMMDD')
from dual
union all
select 1, 'Mark', 103, 201, to_date('20160112', 'YYYYMMDD')
from dual
union all
select 2, 'Steve', 120, 220, to_date('20151218', 'YYYYMMDD')
from dual
union all
select 2, 'Steve', 111, 210, to_date('20151210', 'YYYYMMDD')
from dual
union all
select 2, 'Steve', 104, 206, to_date('20151203', 'YYYYMMDD') from dual),
-- first transform value1, value2 to value_id (1,2), value
data2 as
(select d.rn, d.name, 1 as val_id, d.value1 as value, d.timestamp
from data d
union all
select d.rn, d.name, 2 as val_id, d.value2 as value, d.timestamp
from data d)
select * -- find previous row P of row D, evaluate difference and build column name as desired
from (select d.rn,
d.name,
d.value - p.value as value,
'value' || d.val_id || 'Lag' || row_number() over(partition by d.rn, d.val_id order by d.timestamp desc) as col
from data2 p, data2 d
where p.rn = d.rn
and p.val_id = d.val_id
and p.timestamp =
(select max(pp.timestamp)
from data2 pp
where pp.rn = p.rn
and pp.val_id = p.val_id
and pp.timestamp < d.timestamp))
-- pivot
pivot(sum(value) for col in('value1Lag1',
'value1Lag2',
'value2Lag1',
'value2Lag2'));

Sorting by max value [duplicate]

This question already has answers here:
How to select records with maximum values in two columns?
(2 answers)
Closed 9 years ago.
I have a table that looks like this in an Oracle DB:
TransactionID Customer_id Sequence Activity
---------- ------------- ---------- -----------
1 85 1 Forms
2 51 2 Factory
3 51 1 Forms
4 51 3 Listing
5 321 1 Forms
6 321 2 Forms
7 28 1 Text
8 74 1 Escalate
And I want to be able to sort out all rows where sequence is the highest for each customer_id.
I there a MAX() function I could use on sequence but based on customer_id somehow?
I would like the result of the query to look like this:
TransactionID Customer_id Sequence Activity
---------- ------------- ---------- -----------
1 85 1 Forms
4 51 3 Listing
6 321 2 Forms
7 28 1 Text
8 74 1 Escalate
select t1.*
from your_table t1
inner join
(
select customer_id, max(Sequence) mseq
from your_table
group by customer_id
) t2 on t1.customer_id = t2.customer_id and t1.sequence = t2.mseq
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE tbl ( TransactionID, Customer_id, Sequence, Activity ) AS
SELECT 1, 85, 1, 'Forms' FROM DUAL
UNION ALL SELECT 2, 51, 2, 'Factory' FROM DUAL
UNION ALL SELECT 3, 51, 1, 'Forms' FROM DUAL
UNION ALL SELECT 4, 51, 3, 'Listing' FROM DUAL
UNION ALL SELECT 5, 321, 1, 'Forms' FROM DUAL
UNION ALL SELECT 6, 321, 2, 'Forms' FROM DUAL
UNION ALL SELECT 7, 28, 1, 'Text' FROM DUAL
UNION ALL SELECT 8, 74, 1, 'Escalate' FROM DUAL;
Query 1:
SELECT
MAX( TransactionID ) KEEP ( DENSE_RANK LAST ORDER BY Sequence ) AS TransactionID,
Customer_ID,
MAX( Sequence ) KEEP ( DENSE_RANK LAST ORDER BY Sequence ) AS Sequence,
MAX( Activity ) KEEP ( DENSE_RANK LAST ORDER BY Sequence ) AS Activity
FROM tbl
GROUP BY Customer_ID
ORDER BY TransactionID
Results:
| TRANSACTIONID | CUSTOMER_ID | SEQUENCE | ACTIVITY |
|---------------|-------------|----------|----------|
| 1 | 85 | 1 | Forms |
| 4 | 51 | 3 | Listing |
| 6 | 321 | 2 | Forms |
| 7 | 28 | 1 | Text |
| 8 | 74 | 1 | Escalate |
Please Try it
with cte as
(
select Customer_id,MAX(Sequence) as p from Tablename group by Customer_id
)
select b.* from cte a join Tablename b on a.p = b.Sequence where a.p = b.Sequence and a.Customer_id=b.Customer_id order by b.TransactionID