SQL last status change date - sql

I am trying to get the dates of last status changes. Below is an example data table.
In brief I want to query the minimum DATE value of the latest STATUS (ordered by CHANGE_NO) for each PRODUCT_ID. Mentioned values are the ones filled with yellow.
So far, I could get only the latest dates for each product.
SELECT
*
FROM
(
SELECT
PRODUCT_ID, CHANGE_NO, STATUS, DATE
,MAX(CHANGE_NO) OVER(PARTITION BY PRODUCT_ID) MAX_CHANGE_NO
FROM TABLE
ORDER BY PRODUCT_ID, CHANGE_NO
)
WHERE MAX_CHANGE_NO = CHANGE_NO
Please kindly share the link if there is already a question/answer for a similar case; I've searched but couldn't find any.
Note: I am using Oracle SQL.
Thanks in advance.

Here's one way to do this with analytic functions (avoiding joins).
with
test_data ( product_id, change_no, status, dt ) as (
select 1, 1, 'A', date '2016-10-10' from dual union all
select 1, 2, 'B', date '2016-10-11' from dual union all
select 1, 3, 'C', date '2016-10-12' from dual union all
select 1, 4, 'D', date '2016-10-13' from dual union all
select 2, 1, 'Y', date '2016-02-02' from dual union all
select 2, 2, 'X', date '2016-02-03' from dual union all
select 2, 3, 'X', date '2016-02-04' from dual union all
select 3, 1, 'H', date '2016-06-20' from dual union all
select 3, 2, 'G', date '2016-06-21' from dual union all
select 3, 3, 'T', date '2016-06-22' from dual union all
select 3, 4, 'K', date '2016-06-23' from dual union all
select 3, 5, 'K', date '2016-06-24' from dual union all
select 3, 6, 'K', date '2016-06-25' from dual
)
-- End of test data (not part of the solution). SQL query begins below this line.
select product_id,
max(status) keep (dense_rank last order by change_no) as status,
max(dt) as dt
from (
select product_id, change_no, status, dt,
case when lead(status) over (partition by product_id
order by change_no desc)
= status then 0 else 1 end as flag
from test_data
)
where flag = 1
group by product_id
order by product_id -- if needed
;
Output
PRODUCT_ID STATUS DT
---------- ------ ----------
1 D 13/10/2016
2 X 03/02/2016
3 K 23/06/2016

SELECT * FROM (
SELECT PRODUCT_ID, CHANGE_NO, STATUS,DATE, MIN(DATE) OVER(PARTITION BY PRODUCT_ID,STATUS) as MIN_DATE_OF_LATEST_STATUS
FROM (SELECT PRODUCT_ID, CHANGE_NO, STATUS, DATE
,FIRST_VALUE(STATUS) OVER(PARTITION BY PRODUCT_ID ORDER BY CHANGE_NO DESC) LATEST_STATUS
FROM TABLE
) T
WHERE STATUS = LATEST_STATUS
) T
WHERE DATE = MIN_DATE_OF_LATEST_STATUS
Use the FIRST_VALUE window function to get the latest status for each product_id
Get the MIN date for those status rows
Finally get those rows where min_date = date
If change_no isn't needed in the final result, the query can be simplified to
SELECT PRODUCT_ID, STATUS, MIN(DATE) as MIN_DATE_OF_LATEST_STATUS
FROM (SELECT PRODUCT_ID, CHANGE_NO, STATUS, DATE
,FIRST_VALUE(STATUS) OVER(PARTITION BY PRODUCT_ID ORDER BY CHANGE_NO DESC) LATEST_STATUS
FROM TABLE
) T
WHERE STATUS = LATEST_STATUS
GROUP BY PRODUCT_ID, STATUS

Related

Use rank command to limit find last purchase

I'm trying to find the last purchase for each customer_id. Since there are 3 customers I was expecting to get back 3 rows but I'm getting more.
Can someone tell me what's wrong and how to fix this issue. Any help would be greatly appreciated
ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'DD-MON-YYYY HH24:MI:SS.FF';
ALTER SESSION SET NLS_DATE_FORMAT = 'DD-MON-YYYY HH24:MI:SS';
CREATE TABLE customers
(CUSTOMER_ID, FIRST_NAME, LAST_NAME) AS
SELECT 1, 'Faith', 'Mazzarone' FROM DUAL UNION ALL
SELECT 2, 'Lisa', 'Saladino' FROM DUAL UNION ALL
SELECT 3, 'Jerry', 'Torchiano' FROM DUAL;
CREATE TABLE items
(PRODUCT_ID, PRODUCT_NAME) AS
SELECT 100, 'Black Shoes' FROM DUAL UNION ALL
SELECT 101, 'Brown Shoes' FROM DUAL UNION ALL
SELECT 102, 'White Shoes' FROM DUAL;
CREATE TABLE purchases
(CUSTOMER_ID, PRODUCT_ID, QUANTITY, PURCHASE_DATE) AS
SELECT 1, 100, 1, TIMESTAMP'2022-10-11 09:54:48' FROM DUAL UNION ALL
SELECT 1, 100, 1, TIMESTAMP '2022-10-11 19:04:18' FROM DUAL UNION ALL
SELECT 2, 101,1, TIMESTAMP '2022-10-11 09:54:48' FROM DUAL UNION ALL
SELECT 2,101,1, TIMESTAMP '2022-10-17 19:04:18' FROM DUAL UNION ALL
SELECT 3, 101,1, TIMESTAMP '2022-10-11 09:54:48' FROM DUAL UNION ALL
SELECT 3,102,1, TIMESTAMP '2022-10-17 19:04:18' FROM DUAL UNION ALL
SELECT 3,102, 4,TIMESTAMP '2022-10-10 17:00:00' + NUMTODSINTERVAL ( LEVEL * 2, 'DAY') FROM dual
CONNECT BY LEVEL <= 5;
with cte as
(select
CUSTOMER_ID,
PRODUCT_ID,
QUANTITY,
PURCHASE_DATE,
rank() over (partition by customer_id order by purchase_date desc) rnk
from purchases
)
SELECT p.customer_id,
c.first_name,
c.last_name,
p.product_id,
i.product_name,
p.quantity,
p.purchase_date
from cte p
JOIN customers c ON c.customer_id = p.customer_id
JOIN items i ON i.product_id = p.product_id
where rnk = 1:
First, don't use RANK or DENSE_RANK - they will assign identical purchase_date values with the same rank and hence give you more than one "1" value. Use ROW_NUMBER instead.
Second, you have "from cte p" in there twice. Remove the second one.
And lastly, the real answer to your question is that you have a semicolon before the "where rank = 1" and so nothing after the semicolon is being executed. Hence it isn't filtering. A semicolon ends the SQL, completely.

How can I use the LAG FUNCTION to show revenue this year vs last year in Snowflake with?

I would like to show the current revenue vs last's year's revenue in the same row per region per type. Example:For 2022-04-01, US, Type 1 --> REVENUE: 2456, REVENUE_LAST_YEAR: 4000
2021-04-01, US, Type 1 --> REVENUE: 4000, REVENUE_LAST_YEAR: 0
For some reason, the Lag formula in Snowflake is showing wrong values. Could someone please help ?
WITH
indata(dt,region,type,revenue) AS (
SELECT DATE '2021-04-01','US','Type 1',4000 UNION ALL SELECT DATE '2021-05-01','Europe','Type 2',5777
UNION ALL SELECT DATE '2021-06-01','US','Type 1',45433 UNION ALL SELECT DATE '2021-07-01','Europe','Type 2',8955
UNION ALL SELECT DATE '2021-08-01','US','Type 1',45777 UNION ALL SELECT DATE '2021-09-01','Asia','Type 1',7533
UNION ALL SELECT DATE '2021-10-01','US','Type 1',8866 UNION ALL SELECT DATE '2021-11-01','Asia','Type 2',5534
UNION ALL SELECT DATE '2021-12-01','US','Type 2',4000 UNION ALL SELECT DATE '2022-01-01','Asia','Type 1',7244
UNION ALL SELECT DATE '2022-02-01','US','Type 1',6678 UNION ALL SELECT DATE '2022-03-01','Asia','Type 1',5654
UNION ALL SELECT DATE '2022-04-01','US','Type 1',2456 UNION ALL SELECT DATE '2022-05-01','Asia','Type 1',4525
UNION ALL SELECT DATE '2022-06-01','US','Type 1',6677 UNION ALL SELECT DATE '2022-07-01','Asia','Type 1',6654
UNION ALL SELECT DATE '2022-08-01','US','Type 1',6677 UNION ALL SELECT DATE '2022-09-01','Asia','Type 2',5754
UNION ALL SELECT DATE '2022-10-01','US','Type 1',7744 UNION ALL SELECT DATE '2022-11-01','Asia','Type 2',5644
UNION ALL SELECT DATE '2022-12-01','Europe','Type 2',6775 UNION ALL SELECT DATE '2023-01-01','Asia','Type 2',6777
UNION ALL SELECT DATE '2023-02-01','Europe','Type 2',7755
)
SELECT indata.*,
lag(REVENUE, 1, 0) over (partition by region,type,revenue order by year(dt)) REVENUE_last_year
FROM indata
order by year(dt)
Partitioning by region, type and month-day:
SELECT indata.*,
LAG(REVENUE, 1, 0) over (partition by region,type, TO_VARCHAR(dt, 'mmdd')
order by dt) AS REVENUE_last_year
FROM indata
ORDER BY dt;
Output:

ORACLE SQL: Subtract dates of two or more consecutive rows

I would like to subtract dates of two consecutive rows, using ORACLE's LAG-function (ORACLE version 19g):
SELECT CLIENT, ID, GROUP_A, GROUP_B, GROUP_C, DATE_A, DATE_B
(DATE_A - LAG(DATE_B, 1) OVER (PARTITION BY GROUP_A, ID
ORDER BY ID ASC, GROUP_A ASC, GROUP_B ASC)) AS DELTA_TIME_IN_DAYS
FROM MY_TABLE
As you can see, the problem is, that there can be multiple entries like in line 3 and 4.
Of course column "DELTA_TIME_IN_DAYS" entry in line 4 shouldn't be negative.
It should be "1" as result.
Do you have any suggestions to solve this problem?
I think you want to order by date_b, if you want non-negative numbers:
SELECT CLIENT, ID, GROUP_A, GROUP_B, GROUP_C, DATE_A, DATE_B
(DATE_A - LAG(DATE_B, 1) OVER (PARTITION BY GROUP_A, ID
ORDER BY ID DATE_B ASC
)
) AS DELTA_TIME_IN_DAYS
FROM MY_TABLE;
Note that you do not need to repeat the partitioning keys in the ORDER BY.
You may use range between to offset by groups of rows with identical sort ordinal rather than by physical rows like lag do by default. So, let's use last_value with offset of 1 group to do the same that lag does.
with a (id, start_dt, end_dt) as (
select 1, date '2021-01-01', date '2021-01-31' from dual union all
select 2, date '2021-02-01', date '2021-02-27' from dual union all
select 3, date '2021-03-01', date '2021-03-25' from dual union all
select 3, date '2021-03-01', date '2021-03-25' from dual union all
select 4, date '2021-04-01', date '2021-05-31' from dual union all
select 4, date '2021-04-01', date '2021-05-31' from dual
)
select
a.*
, last_value(end_dt) over(order by id asc range between unbounded preceding and 1 preceding) as dt_diff
from a
D
START_DT
END_DT
DT_DIFF
1
2021-01-01T00:00:00Z
2021-01-31T00:00:00Z
(null)
2
2021-02-01T00:00:00Z
2021-02-27T00:00:00Z
2021-01-31T00:00:00Z
3
2021-03-01T00:00:00Z
2021-03-25T00:00:00Z
2021-02-27T00:00:00Z
3
2021-03-01T00:00:00Z
2021-03-25T00:00:00Z
2021-02-27T00:00:00Z
4
2021-04-01T00:00:00Z
2021-05-31T00:00:00Z
2021-03-25T00:00:00Z
4
2021-04-01T00:00:00Z
2021-05-31T00:00:00Z
2021-03-25T00:00:00Z

Count only when the next row is different from the previous row

I have a table with 12 registers. I want to count the row just if the column "AREA_OPERATIVA" is different from the previous row (ordering by date asc).
For example, from row 1 to row 2 it shouldn't count anything because both have same area 'CROSS' but between rows 2 and 3, it should count (or sum 1, I don't care) since 'CROSS' and 'UTRDANIOS' are different. So the final count should be 3 for the whole table.
Is it possible to do this via query or do I need to make a script with a cursor for this purpose?
I've tried this:
SELECT a.creclama,
sum (CASE WHEN b.area_operativa NOT LIKE a.area_operativa THEN 1 ELSE 0 END) AS increment
FROM TR_ASGAREOPE a
INNER JOIN TR_ASGAREOPE b ON a.creclama = b.creclama
and a.cdistribuidora = b.cdistribuidora
and a.secuencia = b.secuencia
WHERE a.creclama = 10008354
group by a.creclama;
But is counting the full 12 rows.
EDIT:
Finally I could resolve this by the next query:
select sum (
CASE WHEN (comparacion.area_operativa not like comparacion.siguiente_fila THEN 1 ELSE 0 END) AS incremento
from (
select creclama,
area_operativa,
lead(area_operativa) over (order by fmodifica) as siguiente_fila
from TR_ASGAREOPE
where creclama = 10008354
order by fmodifica
);
Hope it is useful for someone in the future, it really got me stuck for a day. Thank you all guys.
You could try using analytic functions like lead or lag, for example
SELECT CRECLAMA,
CASE WHEN AREA_OPERATIVA <> NEXTROW THEN 1 ELSE 0 END AS INCREMENT
FROM (
SELECT CRECLAMA,
AREA_OPERATIVA,
LEAD(AREA_OPERATIVA) OVER (PARTITION BY 1 ORDER BY CRECLAMA) AS NEXTROW
FROM TR_ASGAREOPE
)
Here's an approach using LEAD:
WITH TR_ASGAREOPE(CRECLAMA, AREA_OPERATIVA, DATE_FIELD) AS
(SELECT 10008354, 'CROSS', DATE '2019-01-01' FROM DUAL UNION ALL
SELECT 10008354, 'CROSS', DATE '2019-01-02' FROM DUAL UNION ALL -- 1
SELECT 10008354, 'UTRDANIOS', DATE '2019-01-03' FROM DUAL UNION ALL -- 2
SELECT 10008354, 'EXP263', DATE '2019-01-04' FROM DUAL UNION ALL -- 3
SELECT 10008354, 'EXP6', DATE '2019-01-05' FROM DUAL UNION ALL
SELECT 10008354, 'EXP6', DATE '2019-01-06' FROM DUAL UNION ALL
SELECT 10008354, 'EXP6', DATE '2019-01-07' FROM DUAL UNION ALL
SELECT 10008354, 'EXP6', DATE '2019-01-08' FROM DUAL UNION ALL
SELECT 10008354, 'EXP6', DATE '2019-01-09' FROM DUAL UNION ALL
SELECT 10008354, 'EXP6', DATE '2019-01-10' FROM DUAL UNION ALL
SELECT 10008354, 'EXP6', DATE '2019-01-11' FROM DUAL UNION ALL
SELECT 10008354, 'EXP6', DATE '2019-01-12' FROM DUAL UNION ALL
SELECT 12345678, 'AREA49', DATE '2019-02-01' FROM DUAL UNION ALL
SELECT 12345678, 'AREA49', DATE '2019-02-02' FROM DUAL UNION ALL -- 1
SELECT 12345678, 'AREA50', DATE '2019-02-03' FROM DUAL UNION ALL
SELECT 12345678, 'AREA50', DATE '2019-02-04' FROM DUAL UNION ALL -- 2
SELECT 12345678, 'AREA52', DATE '2019-02-05' FROM DUAL UNION ALL
SELECT 12345678, 'AREA52', DATE '2019-02-06' FROM DUAL UNION ALL
SELECT 12345678, 'AREA52', DATE '2019-02-07' FROM DUAL UNION ALL -- 3
SELECT 12345678, 'AREA53', DATE '2019-02-08' FROM DUAL UNION ALL -- 4
SELECT 12345678, 'AREA52', DATE '2019-02-09' FROM DUAL UNION ALL -- 5
SELECT 12345678, 'AREA53', DATE '2019-02-10' FROM DUAL),
cteData AS (SELECT CRECLAMA,
LEAD(CRECLAMA) OVER (ORDER BY DATE_FIELD) AS NEXT_CRECLAMA,
AREA_OPERATIVA,
LEAD(AREA_OPERATIVA) OVER (ORDER BY DATE_FIELD) AS NEXT_AREA_OPERATIVA
FROM TR_ASGAREOPE)
SELECT CRECLAMA, COUNT(*)
FROM cteData
WHERE CRECLAMA = NEXT_CRECLAMA AND
AREA_OPERATIVA <> NEXT_AREA_OPERATIVA
GROUP BY CRECLAMA
ORDER BY CRECLAMA;
I added data for another CRECLAMA value to show how it would work.
Results:
CRECLAMA COUNT(*)
10008354 3
12345678 5
dbfiddle here
You can use lag() analytic function :
with t as
(
select a.*,
lag(a.area_operativa,1,a.area_operativa) over (order by a."date") as lg
from asgareope a
where a.creclama = 10008354
)
select t.creclama, sum(case when lg = area_operativa then 0 else 1 end) as "increment"
from t
group by t.creclama
I think you can simple use COUNT(DISTINCT ...) for your problem, if the AREA_OPERATIVA could not return to previously used value:
SELECT CRECLAMA, COUNT(DISTINCT AREA_OPERATIVA)
FROM TR_ASGAREOPE
GROUP BY CRECLAMA

SQL counting days with gap / overlapping

I am working on a "counting days" problem almost identical to this one. I have a list of date(s), and need to count how many days used excluding duplicate, and handling the gaps. Same input and output.
From: Markus Jarderot
Input
ID d1 d2
1 2011-08-01 2011-08-08
1 2011-08-02 2011-08-06
1 2011-08-03 2011-08-10
1 2011-08-12 2011-08-14
2 2011-08-01 2011-08-03
2 2011-08-02 2011-08-06
2 2011-08-05 2011-08-09
Output
ID hold_days
1 11
2 8
SQL to find time elapsed from multiple overlapping intervals
But for the life of me I couldn't understand Markus Jarderot's solution.
SELECT DISTINCT
t1.ID,
t1.d1 AS date,
-DATEDIFF(DAY, (SELECT MIN(d1) FROM Orders), t1.d1) AS n
FROM Orders t1
LEFT JOIN Orders t2 -- Join for any events occurring while this
ON t2.ID = t1.ID -- is starting. If this is a start point,
AND t2.d1 <> t1.d1 -- it won't match anything, which is what
AND t1.d1 BETWEEN t2.d1 AND t2.d2 -- we want.
GROUP BY t1.ID, t1.d1, t1.d2
HAVING COUNT(t2.ID) = 0
Why is DATEDIFF(DAY, (SELECT MIN(d1) FROM Orders), t1.d1) picking from the min(d1) from the entire list? Is that regardless of ID.
And what does t1.d1 BETWEEN t2.d1 AND t2.d2 do? Is that to ensure only overlapped interval are calculated?
Same thing with group by, I think because if in the event the same identical period will be discarded? I tried to trace the solution by hand but getting more confused.
This is mostly a duplicate of my answer here (including explanation) but with the inclusion of grouping on an id column. It should use a single table scan and does not require a recursive sub-query factoring clause (CTE) or self joins.
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE your_table ( id, usr, start_date, end_date ) AS
SELECT 1, 'A', DATE '2017-06-01', DATE '2017-06-03' FROM DUAL UNION ALL
SELECT 1, 'B', DATE '2017-06-02', DATE '2017-06-04' FROM DUAL UNION ALL -- Overlaps previous
SELECT 1, 'C', DATE '2017-06-06', DATE '2017-06-06' FROM DUAL UNION ALL
SELECT 1, 'D', DATE '2017-06-07', DATE '2017-06-07' FROM DUAL UNION ALL -- Adjacent to previous
SELECT 1, 'E', DATE '2017-06-11', DATE '2017-06-20' FROM DUAL UNION ALL
SELECT 1, 'F', DATE '2017-06-14', DATE '2017-06-15' FROM DUAL UNION ALL -- Within previous
SELECT 1, 'G', DATE '2017-06-22', DATE '2017-06-25' FROM DUAL UNION ALL
SELECT 1, 'H', DATE '2017-06-24', DATE '2017-06-28' FROM DUAL UNION ALL -- Overlaps previous and next
SELECT 1, 'I', DATE '2017-06-27', DATE '2017-06-30' FROM DUAL UNION ALL
SELECT 1, 'J', DATE '2017-06-27', DATE '2017-06-28' FROM DUAL UNION ALL -- Within H and I
SELECT 2, 'K', DATE '2011-08-01', DATE '2011-08-08' FROM DUAL UNION ALL -- Your data below
SELECT 2, 'L', DATE '2011-08-02', DATE '2011-08-06' FROM DUAL UNION ALL
SELECT 2, 'M', DATE '2011-08-03', DATE '2011-08-10' FROM DUAL UNION ALL
SELECT 2, 'N', DATE '2011-08-12', DATE '2011-08-14' FROM DUAL UNION ALL
SELECT 3, 'O', DATE '2011-08-01', DATE '2011-08-03' FROM DUAL UNION ALL
SELECT 3, 'P', DATE '2011-08-02', DATE '2011-08-06' FROM DUAL UNION ALL
SELECT 3, 'Q', DATE '2011-08-05', DATE '2011-08-09' FROM DUAL;
Query 1:
SELECT id,
SUM( days ) AS total_days
FROM (
SELECT id,
dt - LAG( dt ) OVER ( PARTITION BY id
ORDER BY dt ) + 1 AS days,
start_end
FROM (
SELECT id,
dt,
CASE SUM( value ) OVER ( PARTITION BY id
ORDER BY dt ASC, value DESC, ROWNUM ) * value
WHEN 1 THEN 'start'
WHEN 0 THEN 'end'
END AS start_end
FROM your_table
UNPIVOT ( dt FOR value IN ( start_date AS 1, end_date AS -1 ) )
)
WHERE start_end IS NOT NULL
)
WHERE start_end = 'end'
GROUP BY id
Results:
| ID | TOTAL_DAYS |
|----|------------|
| 1 | 25 |
| 2 | 13 |
| 3 | 9 |
The brute force method is to create all days (in a recursive query) and then count:
with dates(id, day, d2) as
(
select id, d1 as day, d2 from mytable
union all
select id, day + 1, d2 from dates where day < d2
)
select id, count(distinct day)
from dates
group by id
order by id;
Unfortunately there is a bug in some Oracle versions and recursive queries with dates don't work there. So try this code and see whether it works in your system. (I have Oracle 11.2 and the bug still exists there; so I guess you need Oracle 12c.)
I guess Markus' idea is to find all starting points that are not within other ranges and all ending points that aren't. Then just take the first starting point till the first ending point, then the next starting point till the next ending point, etc. As Markus isn't using a window function to number starting and ending points, he must find a more complicated way to achieve this. Here is the query with ROW_NUMBER. Maybe this gives you a start what to look for in Markus' query.
select startpoint.id, sum(endpoint.day - startpoint.day)
from
(
select id, d1 as day, row_number() over (partition by id order by d1) as rn
from mytable m1
where not exists
(
select *
from mytable m2
where m1.id = m2.id
and m1.d1 > m2.d1 and m1.d1 <= m2.d2
)
) startpoint
join
(
select id, d2 as day, row_number() over (partition by id order by d1) as rn
from mytable m1
where not exists
(
select *
from mytable m2
where m1.id = m2.id
and m1.d2 >= m2.d1 and m1.d2 < m2.d2
)
) endpoint on endpoint.id = startpoint.id and endpoint.rn = startpoint.rn
group by startpoint.id
order by startpoint.id;
If all your intervals start at different dates, consider them in ascending order by d1 counting how many days are from d1 to the next interval.
You can discard an interval of it is contained in another one.
The last interval won't have a follower.
This query should give you how many days each interval give
select a.id, a.d1,nvl(min(b.d1), a.d2) - a.d1
from orders a
left join orders b
on a.id = b.id and a.d1 < b.d1 and a.d2 between b.d1 and b.d2
group by a.id, a.d1
Then group by id and sum days