How to make a query showing purchases of a client on the same day, but only if those were made in diffrent stores (oracle)? - sql

I want to show cases of clients with at least 2 purchases on the same day. But I only want to count those purchases that were made in different stores.
So far I have:
Select Purchase.PurClientId, Purchase.PurDate, Purchase.PurId
from Purchase
join
(
Select count(Purchase.PurId),
Purchase.PurClientId,
to_date(Purchase.PurDate)
from Purchases
group by Purchase.PurClientId,
to_date(Purchase.PurDate)
having count (Purchase.PurId) >=2
) k
on k.PurClientId=Purchase.PurClientId
But I have no clue how to make it count purchases only if those were made in different stores. The column which would allow to identify shop is Purchase.PurShopId.
Thanks for help!

You can use:
SELECT PurId,
PurDate,
PurClientId,
PurShopId
FROM (
SELECT p.*,
COUNT(DISTINCT PurShopId) OVER (
PARTITION BY PurClientId, TRUNC(PurDate)
) AS num_stores
FROM Purchase p
)
WHERE num_stores >= 2;
Or
SELECT *
FROM Purchase p
WHERE EXISTS(
SELECT 1
FROM Purchase x
WHERE p.purclientid = x.purclientid
AND p.purshopid != x.purshopid
AND TRUNC(p.purdate) = TRUNC(x.purdate)
);
Which, for the sample data:
CREATE TABLE purchase (
purid PRIMARY KEY,
purdate,
purclientid,
PurShopId
) AS
SELECT 1, DATE '2021-01-01', 1, 1 FROM DUAL UNION ALL
SELECT 2, DATE '2021-01-02', 1, 1 FROM DUAL UNION ALL
SELECT 3, DATE '2021-01-02', 1, 2 FROM DUAL UNION ALL
SELECT 4, DATE '2021-01-03', 1, 1 FROM DUAL UNION ALL
SELECT 5, DATE '2021-01-03', 1, 1 FROM DUAL UNION ALL
SELECT 6, DATE '2021-01-04', 1, 2 FROM DUAL;
Both output:
PURID
PURDATE
PURCLIENTID
PURSHOPID
2
2021-01-02 00:00:00
1
1
3
2021-01-02 00:00:00
1
2
db<>fiddle here

Related

How to calculate payment from user's registration date plus 30 days IN ORACLE SQL

How to calculate payments by a certain period.
Specifically, I wonder if the user registered on the x date, how much payment he has in 30 days from that date
There are two tables:
create table user (user_id, contact, registration_date) as
select 1, 111 111, 1/18/2022 3:57:32 PM from dual union all
select 2, 222 222, 8/12/2021 12:00:12 AM from dual union all
select 3, 333 333, 12/11/2015 5:08:35 PM from dual union all
select 4, 444 444, 5/25/2020 10:59:10 AM from dual;
create table transaction (day, user_id, payment) as
select 1/20/2022, 1, 5 from dual union all
select 1/30/2022, 1, 8 from dual union all
select 2/20/2022, 1, 6 from dual union all
select 8/12/2021 , 2, 10 from dual union all
select 8/15/2021 , 2, 5 from dual union all
select 9/25/2021 , 2, 12 from dual union all
select 12/11/2015 , 3, 18 from dual union all
select 12/20/2015 , 3, 10 from dual union all
select 1/1/2016 , 3, 10 from dual union all
select 5/26/2020 , 4, 7 from dual union all
select 6/1/2020 , 4, 2 from dual;
I wonder something like this, but this query does not work,
select t.user_id,u.registration_date,sum(t.payment) from transaction t
left join user u on t.user_id = u.user_id
where t.day >= u.registration_date and t.day <= u.registration_date + interval '30' days
group by t.user_id
My expected table:
user_id
registration_date
payment
1
1/18/2022 3:57:32 PM
13
2
8/12/2021 12:00:12 AM
15
3
12/11/2015 5:08:35 PM
38
4
5/25/2020 10:59:10 AM
9
First of all, you probably want to outer join the transactions to the users not vice versa. Thus you also show users with no trasactions in the 30 days after registration.
Then, Oracle requires you to either put u.registration_date in GROUP BY or pseudo aggregate it (e.g. MIN(u.registration_date)). This doesn't comply with standard SQL, but it seems that Oracle hasn't mananged yet to properly detect functional dependencies, so they simply don't offer this feature.
At last, the registration date, despite its name, is not a date but a datetime. In order to compare it with the transaction date, truncate it. Then decide whether you want to include the 30th after the registration date or not (i.e. either add 30 or 31 days and use < anyway).
select u.user_id, u.registration_date, sum(t.payment)
from users u
left join transactions t
on t.user_id = u.user_id
and t.day >= trunc(u.registration_date)
and t.day < trunc(u.registration_date) + interval '31' day
group by u.user_id, u.registration_date
order by u.user_id;
Demo: https://dbfiddle.uk/?rdbms=oracle_21&fiddle=a6d8d13c994eff6a1c0f8afa6fcb176f
Looks like
SQL> SELECT u.user_id, u.registration_date, SUM (t.payment) payment
2 FROM tuser u JOIN transaction t ON t.user_id = t.user_id
3 WHERE t.day BETWEEN u.registration_date
4 AND u.registration_date + INTERVAL '30' DAY
5 GROUP BY u.user_id, u.registration_date
6 ORDER BY u.user_id;
USER_ID REGISTRATI PAYMENT
---------- ---------- ----------
1 01/18/2022 13
2 08/12/2021 15
3 12/11/2015 38
4 05/25/2020 9
SQL>
Your query doesn't work because you forgot to add the u.registration_date in your GROUP BY clause. When you do a GROUP BY, all fields mentioned in the SELECT need to be either added to the GROUP BY or be used in a aggregate function like SUM, MIN, MAX, ...

Complex query analyzing historical records

I am using Oracle and trying to retrieve the total number of days a person was out of the office during the year. I have 2 tables involved:
Statuses
1 - Active
2 - Out of the Office
3 - Other
ScheduleHistory
RecordID - primary key
PersonID
PreviousStatusID
NextStatusID
DateChanged
I can easily find when the person went on vacation and when they came back, using
SELECT DateChanged FROM ScheduleHistory WHERE PersonID=111 AND NextStatusID = 2
and
SELECT DateChanged FROM ScheduleHistory WHERE PersonID=111 AND PreviousStatusID = 2
But in case a person went on vacation more than once, how can I can I calculate total number of days a person was out of the office. Is it possible to do programmatically, given only PersonID?
Here is some sample data:
RecordID PersonID PreviousStatusID NextStatusID DateChanged
-----------------------------------------------------------------------------
1 111 1 2 03/11/2020
2 111 2 1 03/13/2020
3 111 1 3 04/01/2020
4 111 3 1 04/07/2020
5 111 1 2 06/03/2020
6 111 2 1 06/05/2020
7 111 1 2 09/14/2020
8 111 2 1 09/17/2020
So from the data above, for the year 2020 for PersonID 111 the query should return 7
Try this:
with aux1 AS (
SELECT
a.*,
to_date(datechanged, 'MM/DD/YYYY') - LAG(to_date(datechanged, 'MM/DD/YYYY')) OVER(
PARTITION BY personid
ORDER BY
recordid
) lag_date
FROM
ScheduleHistory a
)
SELECT
personid,
SUM(lag_date) tot_days_ooo
FROM
aux1
WHERE
previousstatusid = 2
GROUP BY
personid;
If you want total days (or weekdays) for each year (and to account for periods when it goes over the year boundary) then:
WITH date_ranges ( personid, status, start_date, end_date ) AS (
SELECT personid,
nextstatusid,
datechanged,
LEAD(datechanged, 1, datechanged) OVER(
PARTITION BY personid
ORDER BY datechanged
)
FROM table_name
),
split_year_ranges ( personid, year, start_date, end_date, max_date ) AS (
SELECT personid,
TRUNC( start_date, 'YY' ),
start_date,
LEAST(
end_date,
ADD_MONTHS( TRUNC( start_date, 'YY' ), 12 )
),
end_date
FROM date_ranges
WHERE status = 2
UNION ALL
SELECT personid,
end_date,
end_date,
LEAST( max_date, ADD_MONTHS( end_date, 12 ) ),
max_date
FROM split_year_ranges
WHERE end_date < max_date
)
SELECT personid,
EXTRACT( YEAR FROM year) AS year,
SUM( end_date - start_date ) AS total_days,
SUM(
( TRUNC( end_date, 'IW' ) - TRUNC( start_date, 'IW' ) ) * 5 / 7
+ LEAST( end_date - TRUNC( end_date, 'IW' ), 5 )
- LEAST( start_date - TRUNC( start_date, 'IW' ), 5 )
) AS total_weekdays
FROM split_year_ranges
GROUP BY personid, year
ORDER BY personid, year
Which, for the sample data:
CREATE TABLE table_name ( RecordID, PersonID, PreviousStatusID, NextStatusID, DateChanged ) AS
SELECT 1, 111, 1, 2, DATE '2020-03-11' FROM DUAL UNION ALL
SELECT 2, 111, 2, 1, DATE '2020-03-13' FROM DUAL UNION ALL
SELECT 3, 111, 1, 3, DATE '2020-04-01' FROM DUAL UNION ALL
SELECT 4, 111, 3, 1, DATE '2020-04-07' FROM DUAL UNION ALL
SELECT 5, 111, 1, 2, DATE '2020-06-03' FROM DUAL UNION ALL
SELECT 6, 111, 2, 1, DATE '2020-06-05' FROM DUAL UNION ALL
SELECT 7, 111, 1, 2, DATE '2020-09-14' FROM DUAL UNION ALL
SELECT 8, 111, 2, 1, DATE '2020-09-17' FROM DUAL UNION ALL
SELECT 9, 222, 1, 2, DATE '2019-12-31' FROM DUAL UNION ALL
SELECT 10, 222, 2, 2, DATE '2020-12-01' FROM DUAL UNION ALL
SELECT 11, 222, 2, 2, DATE '2021-01-02' FROM DUAL;
Outputs:
PERSONID
YEAR
TOTAL_DAYS
TOTAL_WEEKDAYS
111
2020
7
7
222
2019
1
1
222
2020
366
262
222
2021
1
1
db<>fiddle here
Provided no vacation crosses a year boundary
with grps as (
SELECT sh.*,
row_number() over (partition by PersonID, NextStatusID order by DateChanged) grp
FROM ScheduleHistory sh
WHERE NextStatusID in (1,2) and 3 not in (NextStatusID, PreviousStatusID)
), durations as (
SELECT PersonID, min(DateChanged) DateChanged, max(DateChanged) - min(DateChanged) duration
FROM grps
GROUP BY PersonID, grp
)
SELECT PersonID, sum(duration) days_out
FROM durations
GROUP BY PersonID;
db<>fiddle
year_span is used to split an interval spanning across two years in two different records
H1 adds a row number dependent from PersonID to get the right sequence for each person
H2 gets the periods for each status change and extract 1st day of the year of the interval end
H3 split records that span across two years and calculate the right date_start and date_end for each interval
H calculates days elapsed in each interval for each year
final query sum up the records to get output
EDIT
If you need workdays instead of total days, you should not use total_days/7*5 because it is a bad approximation and in some cases gives weird results.
I have posted a solution to jump on fridays to mondays here
with
statuses (sid, sdescr) as (
select 1, 'Active' from dual union all
select 2, 'Out of the Office' from dual union all
select 3, 'Other' from dual
),
ScheduleHistory(RecordID, PersonID, PreviousStatusID, NextStatusID , DateChanged) as (
select 1, 111, 1, 2, date '2020-03-11' from dual union all
select 2, 111, 2, 1, date '2020-03-13' from dual union all
select 3, 111, 1, 3, date '2020-04-01' from dual union all
select 4, 111, 3, 1, date '2020-04-07' from dual union all
select 5, 111, 1, 2, date '2020-06-03' from dual union all
select 6, 111, 2, 1, date '2020-06-05' from dual union all
select 7, 111, 1, 2, date '2020-09-14' from dual union all
select 8, 111, 2, 1, date '2020-09-17' from dual union all
SELECT 9, 222, 1, 2, date '2019-12-31' from dual UNION ALL
SELECT 10, 222, 2, 2, date '2020-12-01' from dual UNION ALL
SELECT 11, 222, 2, 2, date '2021-01-02' from dual
),
year_span (n) as (
select 1 from dual union all
select 2 from dual
),
H1 AS (
SELECT ROW_NUMBER() OVER (PARTITION BY PersonID ORDER BY RecordID) PID, H.*
FROM ScheduleHistory H
),
H2 as (
SELECT
H1.*, H2.DateChanged DateChanged2,
EXTRACT(YEAR FROM H2.DateChanged) - EXTRACT(YEAR FROM H1.DateChanged) + 1 Y,
trunc(H2.DateChanged,'YEAR') Y2
FROM H1 H1
LEFT JOIN H1 H2 ON H1.PID = H2.PID-1 AND H1.PersonID = H2.PersonID
),
H3 AS (
SELECT Y, N, H2.PID, H2.RecordID, H2.PersonID, H2.NextStatusID,
CASE WHEN Y=1 THEN H2.DateChanged ELSE CASE WHEN N=1 THEN H2.DateChanged ELSE Y2 END END D1,
CASE WHEN Y=1 THEN H2.DateChanged2 ELSE CASE WHEN N=1 THEN Y2 ELSE H2.DateChanged2 END END D2
FROM H2
JOIN year_span N ON N.N <=Y
),
H AS (
SELECT PersonID, NextStatusID, EXTRACT(year FROM d1) Y, d2-d1 D
FROM H3
)
select PersonID, sdescr Status, Y, sum(d) d
from H
join statuses s on NextStatusID = s.sid
group by PersonID, sdescr, Y
order by PersonID, sdescr, Y
output
PersonID Status Y d
111 Active 2020 177
111 Other 2020 6
111 Out of the Office 2020 7
222 Out of the Office 2019 1
222 Out of the Office 2020 366
222 Out of the Office 2021 1
check the fiddle here

Oracle : Get average count for last 30 business days

Oracle version 11g.
My table has records similar to these.
calendar_date ID record_count
25-OCT-2017 1 20
25-OCT-2017 2 40
25-OCT-2017 3 60
24-OCT-2017 1 70
24-OCT-2017 2 50
24-OCT-2017 3 10
20-OCT-2017 1 35
20-OCT-2017 2 60
20-OCT-2017 3 90
18-OCT-2017 1 80
18-OCT-2017 2 50
18-OCT-2017 3 45
i.e for each ID, there is one record count for a given calendar day. The days are NOT continuous, i.e there may be missing records for weekends/holidays etc. On such days, there will not be records available for any ID. However on working days there are entries available for each ID .
I need to get the average record count for last 30 business days for each id
I want an output like this. ( Don't go by the values. It is just a sample )
ID avg_count_last_30
1 150
2 130
3 110
I am trying to figure out the most efficient way to do this. I thought of using RANGE BETWEEN , ROWS BETWEEN etc , but unsure it would work.
Off course a query like this won't help as there are holidays in between.
select id, AVG(record_count) FROM mytable
where calendar_date between SYSDATE - 30 and SYSDATE - 1
group by id;
what I need is something like
select id , AVG(record_count) FROM mytable
where calendar_date between last_30th_business_day and last_business_day
group by id;
last_30th_business_day will be count(DISTINCT business_days ) starting from most recent business day going backwards till I count 30.
last_business_day will be most recent business day
Would like to know experts opinion on this and best approach.
Based on your comment try this one:
WITH mytable (calendar_date, ID, record_count) AS (
SELECT TO_DATE('25-10-2017', 'DD-MM-YYYY'), 1, 20 FROM dual UNION ALL
SELECT TO_DATE('25-10-2017', 'DD-MM-YYYY'), 2, 40 FROM dual UNION ALL
SELECT TO_DATE('25-10-2017', 'DD-MM-YYYY'), 3, 60 FROM dual UNION ALL
SELECT TO_DATE('24-10-2017', 'DD-MM-YYYY'), 1, 70 FROM dual UNION ALL
SELECT TO_DATE('24-10-2017', 'DD-MM-YYYY'), 2, 50 FROM dual UNION ALL
SELECT TO_DATE('24-10-2017', 'DD-MM-YYYY'), 3, 10 FROM dual UNION ALL
SELECT TO_DATE('20-10-2017', 'DD-MM-YYYY'), 1, 35 FROM dual UNION ALL
SELECT TO_DATE('20-10-2017', 'DD-MM-YYYY'), 2, 60 FROM dual UNION ALL
SELECT TO_DATE('20-10-2017', 'DD-MM-YYYY'), 3, 90 FROM dual UNION ALL
SELECT TO_DATE('18-10-2017', 'DD-MM-YYYY'), 1, 80 FROM dual UNION ALL
SELECT TO_DATE('18-10-2017', 'DD-MM-YYYY'), 2, 50 FROM dual UNION ALL
SELECT TO_DATE('18-10-2017', 'DD-MM-YYYY'), 3, 45 FROM dual),
t AS (
SELECT calendar_date, ID, record_count,
ROW_NUMBER() OVER (PARTITION BY ID ORDER BY calendar_date desc) AS RN
FROM mytable)
SELECT ID, AVG(RECORD_COUNT)
FROM t
WHERE rn <= 30
group by ID;

How to identify positive minimum or negative maximum in a column for a key?

I have the following columns - Person_ID Days. For one person id, multiple days are possible. Something like this:
Person_Id Days
1000 100
1000 200
1000 -50
1000 -10
1001 100
1001 200
1001 50
1001 10
1002 -50
1002 -10
I need to address the following scenarios:
If all values for days column are positive, I need minimum of the days for a person_id. If the days column has both positive and negative, I need minimum of positive. If all negatives, I need maximum of negative.
The output like:
Person_id Days
1000 100
1001 10
1002 -10
I tried using case statement, but I am unable to use a same column in the condition as well as grouping.
Try this (Postgres 9.4+):
select person_id, coalesce(min(days) filter (where days > 0), max(days))
from a_table
group by 1
order by 1;
Oracle Setup:
CREATE TABLE table_name ( Person_Id, Days ) AS
SELECT 1000, 100 FROM DUAL UNION ALL
SELECT 1000, 200 FROM DUAL UNION ALL
SELECT 1000, -50 FROM DUAL UNION ALL
SELECT 1000, -10 FROM DUAL UNION ALL
SELECT 1001, 100 FROM DUAL UNION ALL
SELECT 1001, 200 FROM DUAL UNION ALL
SELECT 1001, 50 FROM DUAL UNION ALL
SELECT 1001, 10 FROM DUAL UNION ALL
SELECT 1002, -50 FROM DUAL UNION ALL
SELECT 1002, -10 FROM DUAL;
Query:
SELECT person_id, days
FROM (
SELECT t.*,
ROW_NUMBER() OVER ( PARTITION BY person_id
ORDER BY SIGN( ABS( days ) ),
SIGN( DAYS ) DESC,
ABS( DAYS )
) AS rn
FROM table_name t
)
WHERE rn = 1;
Output:
PERSON_ID DAYS
---------- ----------
1000 100
1001 10
1002 -10
Oracle solution:
with
input_data ( person_id, days) as (
select 1000, 100 from dual union all
select 1000, 200 from dual union all
select 1000, -50 from dual union all
select 1000, -10 from dual union all
select 1001, 100 from dual union all
select 1001, 200 from dual union all
select 1001, 50 from dual union all
select 1001, 10 from dual union all
select 1002, -50 from dual union all
select 1002, -10 from dual
)
select person_id,
NVL(min(case when days > 0 then days end), max(days)) as days
from input_data
group by person_id;
PERSON_ID DAYS
---------- ----------
1000 100
1001 10
1002 -10
For each person_id, if there is at least one days value that is strictly positive, then the min will be taken over positive days only and will be returned by NVL(). Otherwise the min() will return null, and NVL() will return max() over all days (all of which are, in this case, negative or 0).
select Person_id, min(abs(days)) * days/abs(days) from table_name
group by Person_id
-- + handle zero_divide .. SORRY.. the above works only in MySQL .
Something like this will work anywhere which is equivalent of above query:
select t.Person_id , min(t.days) from table_name t,
(select Person_id, min(abs(days)) as days from table_name group by Person_id) v
where t.Person_id = v.Person_id
and abs(t days) = v.days
group by Person_id;
OR
select id, min(Days) from (
select Person_id, min(abs(Days)) as Days from temp group by Person_id
union
select Person_id, max(Days) as Days from temp group by Person_id
) temp
group by Person_id;
You can do this by using GroupBy clause in sql server. Take a look into below query:-
CREATE TABLE #test(Person_Id INT, [Days] INT)
DECLARE #LargestNumberFromTable INT;
INSERT INTO #test
SELECT 1000 , 100 UNION
SELECT 1000 , 200 UNION
SELECT 1000 , -50 UNION
SELECT 1000 , -10 UNION
SELECT 1001 , 100 UNION
SELECT 1001 , 200 UNION
SELECT 1001 , 50 UNION
SELECT 1001 , 10 UNION
SELECT 1002 , -50 UNION
SELECT 1002 , -10
SELECT #LargestNumberFromTable = ISNULL(MAX([Days]), 0)
FROM #test
SELECT Person_Id
,CASE WHEN SUM(IIF([Days] > 0,[Days] , 0)) = 0 THEN MAX([Days]) -- All Negative
WHEN SUM([Days]) = SUM(IIF([Days] > 0, [Days], 0)) THEN MIN ([Days]) -- ALL Positive
WHEN SUM([Days]) <> SUM(IIF([Days] > 0, [Days], 0)) THEN MIN(IIF([Days] > 0, [Days], #LargestNumberFromTable)) --Mix (Negative And positive)
END AS [Days]
FROM #test
GROUP BY Person_Id
DROP TABLE #test

How to fetch records that have an alternate entry

I need some help to fetch records having alternate set of entries associated with Unique value(ex: user_id)
I want output to be only (1111,2222,3333)
Here is the scenario:
user_id 1111 attended .net course from 2005-01-01 to 2006-12-31
he later attended java from 2007-01-01 to 2009-12-31
he later came back to .net
so i want to retrieve these kind of user_id's
user_id 4444 should not be in the output, because there is no alternative courses.
UPDATE: 4444 started his Java course from 2007 to 2009 he again
attended Java from 2010 - 2012 Later he attended .net but never came
back to Java so he must be excluded from output
If Group by is used, it will consider records irrespective of alternate course name.
We can create a procedure to accomplish this by looping and comparing the alternate course name but i want to know if a query can do this?
You can use two INNER JOIN operations:
SELECT DISTINCT user_id
FROM mytable AS t1
INNER JOIN mytable AS t2
ON t1.user_id = t2.user_id AND t1.id < t2.id AND t1.course_name <> t2.course_name
INNER JOIN mytable AS t3
ON t2.user_id = t3.user_id AND t2.id < t3.id AND t1.course_name = t3.course_name
I assume that id is an auto-increment field that reflects the order the rows have been inserted in the DB. Otherwise, you should use a date field in its place.
Same as Girogos Betsos' answer, only with select distinct to prevent duplicates.
SELECT DISTINCT user_id
FROM mytable AS t1
INNER JOIN mytable AS t2
ON t1.user_id = t2.user_id AND t1.Start_Date < t2.Start_Date AND
t1.course_name <> t2.course_name
INNER JOIN mytable AS t3
ON t2.user_id = t3.user_id AND t2.Start_Date < t3.Start_Date AND
t1.course_name = t3.course_name
EDIT: Using Start_Date since the answer has been updated and IDs are not necessarily sequential.
This is a version utilizing Windowed Aggregate Fuctions instead of multiple self joins:
SELECT DISTINCT user_id
FROM
(
SELECT user_id
,course_name
,start_date
,RANK() -- number all courses
OVER (PARTITION BY user_id
ORDER BY start_date)
-
RANK() -- number each course
OVER (PARTITION BY user_id, course_name
ORDER BY start_date) AS x
FROM tab
) dt
GROUP BY user_id, course_name
HAVING MIN(x) <> MAX(x) -- same course but another inbetween
If a user has a course multiple times in a series that x will stay the same, if there was another course inbetween it will change:
java 1 - 1 = 0
java 2 - 2 = 0 <--- min
.net 3 - 1 = 2
java 4 - 3 = 1 <--- max
java 1 - 1 = 0
java 2 - 2 = 0
.net 3 - 1 = 2
.net 4 - 2 = 2
Using a single table scan and does not rely on GROUP BY:
WITH table_name ( user_id, start_date, end_date, course_name, id ) AS (
SELECT 1111, DATE '2005-01-01', DATE '2006-12-31', '.net', 1 FROM DUAL UNION ALL
SELECT 1111, DATE '2007-01-01', DATE '2009-12-31', 'java', 2 FROM DUAL UNION ALL
SELECT 1111, DATE '2010-01-01', DATE '2020-12-31', '.net', 3 FROM DUAL UNION ALL
SELECT 2222, DATE '2005-01-01', DATE '2006-12-31', 'java', 4 FROM DUAL UNION ALL
SELECT 2222, DATE '2007-01-01', DATE '2008-12-31', '.net', 5 FROM DUAL UNION ALL
SELECT 2222, DATE '2009-01-01', DATE '2012-12-31', '.net', 6 FROM DUAL UNION ALL
SELECT 2222, DATE '2013-01-01', DATE '2016-12-31', 'java', 7 FROM DUAL UNION ALL
SELECT 3333, DATE '2005-01-01', DATE '2007-12-31', 'java', 8 FROM DUAL UNION ALL
SELECT 3333, DATE '2007-01-01', DATE '2008-12-31', '.net', 9 FROM DUAL UNION ALL
SELECT 3333, DATE '2009-01-01', DATE '2013-12-31', 'java', 10 FROM DUAL UNION ALL
SELECT 3333, DATE '2014-01-01', DATE '2016-12-31', '.net', 11 FROM DUAL UNION ALL
SELECT 4444, DATE '2007-01-01', DATE '2009-12-31', 'java', 12 FROM DUAL UNION ALL
SELECT 4444, DATE '2010-01-01', DATE '2012-12-31', 'java', 13 FROM DUAL UNION ALL
SELECT 4444, DATE '2013-01-01', DATE '2015-12-31', '.net', 14 FROM DUAL UNION ALL
SELECT 4444, DATE '2016-01-01', DATE '2016-12-31', '.net', 15 FROM DUAL
)
SELECT DISTINCT user_id
FROM (
SELECT user_id,
LEAD( course_name )
OVER ( PARTITION BY user_id, course_name ORDER BY start_date )
AS next_same_course,
LEAD( course_name )
OVER ( PARTITION BY user_id ORDER BY start_date )
AS next_course
FROM table_name
)
WHERE next_same_course IS NOT NULL
AND next_course <> next_same_course;