Retrieve single row from a query - sql

I am creating a query to find salary details of an employee with date_to as '31-dec-4712' (Latest).
But, If date_to is 31-dec-4712 for two rows for an employee then the one with status 'Approved' should be picked in other cases when only
single rows comes then that should be returned as is.
I have created the below query for the salary details. need help with teh above scenario
select distinct PAPF.EMPLOYEE_NUMBER ,
TO_CHAR (EMP_DOJ (PAPF.PERSON_ID),'DD-MON-YYYY' ) DOJ ,
TO_CHAR(HR_EMPLOYEE_ORIGINAL_DOJ(PAPF.EMPLOYEE_NUMBER,42) ,'DD- MON-YYYY' ) ORIGINAL_DOJ,
PPP.CHANGE_DATE,
PPP.DATE_TO,
PPP.PROPOSED_SALARY_N TOTAL_REMUN,
HR_GENERAL.DECODE_LOOKUP('PER_SAL_PROPOSAL_STATUS',APPROVED) status
from PER_ALL_ASSIGNMENTS_F PAAF,
PER_ALL_PEOPLE_F PAPF,
PER_PAY_PROPOSALS PPP
where 1 = 1
and PAPF.PERSON_ID = PAAF.PERSON_ID
and PAPF.BUSINESS_GROUP_ID = 21
and PAPF.CURRENT_EMPLOYEE_FLAG = 'Y'
and papf.employee_number = '109575'
and :P_DATE1 between PAAF.EFFECTIVE_START_DATE
and PAAF.EFFECTIVE_END_DATE
and :P_DATE1 between PAPF.EFFECTIVE_START_DATE
and PAPF.EFFECTIVE_END_DATE
and :P_DATE1 between PPP.CHANGE_DATE(+)
and NVL(PPP.DATE_TO, HR_GENERAL.END_OF_TIME)
and PPP.ASSIGNMENT_ID(+) = PAAF.ASSIGNMENT_ID
order by TO_NUMBER(PAPF.EMPLOYEE_NUMBER);
Emp_num DOJ ORIGINAL_DOJ CHANGE_DATE DATE_TO TOTAL_REMUN STATUS
109575 01-DEC-2016 24-JUL-2014 01-MAY-19 31-DEC-12 250000 Proposed
109575 01-DEC-2016 24-JUL-2014 01-APR-19 31-DEC-12 100000 Approved

You can use conditional ordering for each employee separately, like here:
-- sample rows
with salaries (emp_id, name, salary, date_to, status) as (
select 1001, 'Orange', 1400, date '4712-12-31', 'Rejected' from dual union all
select 1001, 'Orange', 1200, date '4712-12-31', 'Approved' from dual union all
select 1002, 'Red', 2500, date '4712-12-31', 'Approved' from dual union all
select 1003, 'Blue', 2700, date '4712-12-31', 'Proposed' from dual union all
select 1004, 'Green', 2200, date '2012-07-31', 'Approved' from dual union all
select 1005, 'White', 1200, date '4712-12-31', 'Approved' from dual union all
select 1005, 'White', 1300, date '4712-12-31', 'Rejected' from dual )
-- end of sample data
select emp_id, name, salary, date_to, status
from (
select s.*,
row_number() over (partition by emp_id
order by case status when 'Approved' then 1 end) rn
from salaries s
where date_to = date '4712-12-31')
where rn = 1
Result:
EMP_ID NAME SALARY DATE_TO STATUS
---------- ------ ---------- ----------- --------
1001 Orange 1200 4712-12-31 Approved
1002 Red 2500 4712-12-31 Approved
1003 Blue 2700 4712-12-31 Proposed
1005 White 1200 4712-12-31 Approved

If the STATUS takes only two values, "Approved" and "Proposed", you can order by STATUS and fetch the first row. If you have (or in the future you'll have) more statuses and you want to define a priority add a column in the select with a "CASE" that assigns to each status the corresponding priority. Then you order by this column and you fetch the first row....

Related

To fetch the rehire period using lag n lead function

Wanted to check whether the employee got rehired to a contract or not. If he is rehired then return the rehire period .
If the multiple employees got rehired then return all their rehire period .
Sample data:(Table 'Contract')
Employee_id Period Contract
111 202204 1NA
111 202205 1NA
111 202206 1NA
112 202207 1NA
112 202208 1NA
111 202209 1NA
In the above case the output should be ,
Employee_id Period Contract
111 202209 1NA
The query should first check whether the employee got rehired or not, if so then return the rehire period.
If that contract has got no rehire's then return NULL.
Any other logic other than lag n lead will also be Appreciated!
Thanks in advance:)
Image of the sample data
Use LAG to identify if the previous period interval, then only select the ones where the interval > 1
create table contracts (employee_id,period,contract) as
(
SELECT 111, 202204,'1NA' FROM DUAL UNION ALL
SELECT 111, 202205,'1NA' FROM DUAL UNION ALL
SELECT 111, 202206,'1NA' FROM DUAL UNION ALL
SELECT 111, 202209,'1NA' FROM DUAL UNION ALL
SELECT 112, 202207,'1NA' FROM DUAL UNION ALL
SELECT 112, 202208,'1NA' FROM DUAL
);
Table CONTRACTS created.
with contracts_w_lags (
employee_id
,period
,last_period
,contract
) as ( select employee_id
,period
,lag(period)
over(partition by employee_id
order by period)
,contract
from contracts
)
select employee_id
,period
,contract
from contracts_w_lags
where period - nvl( last_period ,period ) > 1;
EMPLOYEE_ID PERIOD CON
----------- ---------- ---
111 202209 1NA
Note that your sample data only has periods within the same year. This example will fail if periods cross years.
To overcome that, create a pseudo "periods" table with a rownumber to identify consecutive rows:
create table contracts (employee_id,period,contract) as
(
SELECT 111, 202111,'1NA' FROM DUAL UNION ALL
SELECT 111, 202112,'1NA' FROM DUAL UNION ALL
SELECT 111, 202201,'1NA' FROM DUAL UNION ALL
SELECT 111, 202203,'1NA' FROM DUAL UNION ALL
SELECT 112, 202207,'1NA' FROM DUAL UNION ALL
SELECT 112, 202208,'1NA' FROM DUAL
);
Table CONTRACTS created.
with month_count ( cnt ) as
( select months_between(
to_date( max(period) ,'YYYYMM' )
,to_date( min(period) ,'YYYYMM' ))
from contracts
),contract_start ( dt ) as
( select to_date( min(period) ,'YYYYMM' )
from contracts
),contract_periods ( period ,rn ) as
( select to_char( add_months( c.dt ,level - 1 ) ,'YYYYMM' )
,row_number() over( order by add_months( c.dt ,level - 1 ) )
from contract_start c
,month_count m
connect by level <= m.cnt + 1
),contracts_w_lags ( employee_id ,period ,contract ,period_rn ,last_period_rn ) as
( select c.employee_id
,c.period
,c.contract
,p.rn
,lag(p.rn) over(partition by c.employee_id order by p.rn )
from contracts c
join contract_periods p on c.period = p.period
)
select employee_id
,period
,contract
from contracts_w_lags
where period_rn - nvl( last_period_rn ,period_rn ) > 1;
EMPLOYEE_ID PERIOD CON
----------- ---------- ---
111 202209 1NA
The answer (after comments) is at the end....
It is unclear what is your expected result with this sample data:
WITH
contracts (EMP_ID, PERIOD, CONTRACT) as
(
SELECT 111, 202204, '1NA' FROM DUAL UNION ALL
SELECT 111, 202205, '1NA' FROM DUAL UNION ALL
SELECT 111, 202206, '1NA' FROM DUAL UNION ALL
SELECT 112, 202207, '1NA' FROM DUAL UNION ALL
SELECT 112, 202208, '1NA' FROM DUAL UNION ALL
SELECT 111, 202209, '1NA' FROM DUAL
)
There are some multiple consecutive periods for both sample eployees. One of the options is to show first and last periods for emps with multiple periods:
SELECT EMP_ID, Min(PREV_PERIOD) "FIRST_PERIOD", Max(PERIOD) "LAST_PERIOD", CONTRACT
FROM (Select EMP_ID, PERIOD, CONTRACT,
LAG(PERIOD, 1, 0) OVER(Partition By EMP_ID Order By PERIOD) "PREV_PERIOD"
From contracts)
WHERE PREV_PERIOD != 0
GROUP BY EMP_ID, CONTRACT
--
-- R e s u l t :
-- EMP_ID FIRST_PERIOD LAST_PERIOD CONTRACT
-- ---------- ------------ ----------- --------
-- 111 202204 202209 1NA
-- 112 202207 202208 1NA
... another could be to show them all :
SELECT EMP_ID, PERIOD "PERIOD", PREV_PERIOD "PREV_PERIOD", CONTRACT
FROM (Select EMP_ID, PERIOD, CONTRACT,
LAG(PERIOD, 1, 0) OVER(Partition By EMP_ID Order By PERIOD) "PREV_PERIOD"
From contracts)
WHERE PREV_PERIOD != 0
--
-- R e s u l t :
-- EMP_ID PERIOD PREV_PERIOD CONTRACT
-- ---------- ---------- ----------- --------
-- 111 202205 202204 1NA
-- 111 202206 202205 1NA
-- 111 202209 202206 1NA
-- 112 202208 202207 1NA
... and if you want the same with LEAD() function
SELECT EMP_ID, PERIOD "PERIOD", NEXT_PERIOD "NEXT_PERIOD", CONTRACT
FROM (Select EMP_ID, PERIOD, CONTRACT,
LEAD(PERIOD, 1, 0) OVER(Partition By EMP_ID Order By PERIOD) "NEXT_PERIOD"
From contracts)
WHERE NEXT_PERIOD != 0
--
-- R e s u l t :
-- EMP_ID PERIOD NEXT_PERIOD CONTRACT
-- ---------- ---------- ----------- --------
-- 111 202204 202205 1NA
-- 111 202205 202206 1NA
-- 111 202206 202309 1NA
-- 112 202207 202208 1NA
-- 112 202208 202207 1NA
It is pretty much the same - just showing next period instead of previous.
NOTE: If rehire to a contract means the same contract then -
OVER(Partition By EMP_ID, CONTRACT ....)
To do the opposite (non-consecutive periods):
SELECT EMP_ID, PERIOD "PERIOD", NEXT_PERIOD "NEXT_PERIOD", CONTRACT
FROM (Select EMP_ID, PERIOD, CONTRACT,
LEAD(PERIOD, 1, 0) OVER(Partition By EMP_ID Order By PERIOD) "NEXT_PERIOD"
From contracts)
WHERE NEXT_PERIOD != 0 And CASE WHEN SubStr(NEXT_PERIOD, 1, 4) = SubStr(PERIOD, 1, 4)
THEN NEXT_PERIOD - PERIOD
ELSE NEXT_PERIOD - (PERIOD + 88) -- handling the year change
END > 1
--
-- R e s u l t :
-- EMP_ID PERIOD NEXT_PERIOD CONTRACT
-- ---------- ---------- ----------- --------
-- 111 202206 202209 1NA
From Oracle 12, you can use MATCH_RECOGNIZE to perform row-by-row pattern matching:
SELECT emp_id, period, contract
FROM contracts
MATCH_RECOGNIZE(
PARTITION BY contract
ORDER BY period
MEASURES
FIRST(emp_id) AS emp_id,
LAST(period) AS period
AFTER MATCH SKIP TO FIRST different_emp
PATTERN (emp+ different_emp+ emp)
DEFINE
emp AS FIRST(emp_id) = emp_id,
different_emp AS FIRST(emp_id) != emp_id
);
Which, for the sample data:
CREATE TABLE contracts (EMP_ID, PERIOD, CONTRACT) as
SELECT 111, 202204, '1NA' FROM DUAL UNION ALL
SELECT 111, 202205, '1NA' FROM DUAL UNION ALL
SELECT 111, 202206, '1NA' FROM DUAL UNION ALL
SELECT 112, 202207, '1NA' FROM DUAL UNION ALL
SELECT 112, 202208, '1NA' FROM DUAL UNION ALL
SELECT 111, 202209, '1NA' FROM DUAL;
Outputs:
EMP_ID
PERIOD
CONTRACT
111
202209
1NA
fiddle

I want to get the count of roll number for each month

ID
STUDENT_ID
STATUS_DATE
1002
434120010026
25-FEB-22
1000
434120010026
03-MAY-03
1001
434120010026
25-FEB-22
1020
434120020023
18-MAR-22
1021
434120020025
18-MAR-22
1022
434120020025
16-MAR-22
Tried this
select count(*),
trunc(status_date, 'mm')
from test_studentattendance
group by trunc(status_date, 'mm');
got count of roll number in each month not the roll numbers.
COUNT(*)
TRUNC(STATUS_DATE,'MM')
1
01-MAY-03
2
01-FEB-22
3
01-MAR-22
COUNT(*) is counting the number of rows in each group and is counting students that appear in the same month multiple times.
To get the number on roll each month, you need to COUNT the DISTINCT identifier for each student (which, I assume would be student_id):
SELECT COUNT(DISTINCT student_id) AS number_of_students,
TRUNC(status_date, 'mm') AS month
FROM test_studentattendance
GROUP BY TRUNC(status_date, 'mm');
Which, for your sample data:
CREATE TABLE test_studentattendance (ID, STUDENT_ID, STATUS_DATE) AS
SELECT 1002, 434120010026, DATE '2022-02-25' FROM DUAL UNION ALL
SELECT 1000, 434120010026, DATE '2003-05-03' FROM DUAL UNION ALL
SELECT 1001, 434120010026, DATE '2022-02-25' FROM DUAL UNION ALL
SELECT 1020, 434120020023, DATE '2022-03-18' FROM DUAL UNION ALL
SELECT 1021, 434120020025, DATE '2022-03-18' FROM DUAL UNION ALL
SELECT 1022, 434120020025, DATE '2022-03-16' FROM DUAL;
Outputs:
NUMBER_OF_STUDENTS
MONTH
1
2022-02-01 00:00:00
1
2003-05-01 00:00:00
2
2022-03-01 00:00:00
db<>fiddle here

Return Data based Relevant Dates

I have an occupancy table and a pay history table. I want to return the state that the employee is in from the occupancy at the time of the relevant pay.
Occupancy Table
Emp#|Commence Date|State
-----|-------------|----
101 | 1/01/2016 | VIC
101 | 1/04/2016 | NSW
101 | 1/08/2016 | ACT
Pay History Table
Emp#|Pay Date
----|--------
101 |15/01/2016
101 |15/02/2016
101 |15/03/2016
101 |15/04/2016
101 |15/05/2016
101 |15/06/2016
101 |15/07/2016
101 |15/08/2016
101 |15/09/2016
I'm wanting to return the following
Emp#|:Pay Date:|State
----|----------|-----
101 |15/01/2016|VIC
101 |15/02/2016|VIC
101 |15/03/2016|VIC
101 |15/04/2016|NSW
101 |15/05/2016|NSW
101 |15/06/2016|NSW
101 |15/07/2016|NSW
101 |15/08/2016|ACT
101 |15/09/2016|ACT
Can someone assist, please
You need to generate the end_date in the occupancy table in a subquery; the lead() function is perfect for this purpose. I use it with all three arguments - the third argument gives a "default" date which I chose arbitrarily as 15 December 2099 for the "current" status. Then its a simple join on empno and a between condition on dates.
I assume you have more than one empno in your data, so I accommodated that. Then: I don't know if # is legal in Oracle column names, but I didn't want to try; I changed to empno. And names definitely can't have spaces in them unless you quote the names, which has many disadvantages; I worked around that too.
with
occupancy ( empno, commence_date, state ) as (
select 101, to_date('1/01/2016', 'dd/mm/yyyy'), 'VIC' from dual union all
select 101, to_date('1/04/2016', 'dd/mm/yyyy'), 'NSW' from dual union all
select 101, to_date('1/08/2016', 'dd/mm/yyyy'), 'ACT' from dual
),
pay_history ( empno, pay_date ) as (
select 101, to_date('15/01/2016', 'dd/mm/yyyy') from dual union all
select 101, to_date('15/02/2016', 'dd/mm/yyyy') from dual union all
select 101, to_date('15/03/2016', 'dd/mm/yyyy') from dual union all
select 101, to_date('15/04/2016', 'dd/mm/yyyy') from dual union all
select 101, to_date('15/05/2016', 'dd/mm/yyyy') from dual union all
select 101, to_date('15/06/2016', 'dd/mm/yyyy') from dual union all
select 101, to_date('15/07/2016', 'dd/mm/yyyy') from dual union all
select 101, to_date('15/08/2016', 'dd/mm/yyyy') from dual union all
select 101, to_date('15/09/2016', 'dd/mm/yyyy') from dual
)
-- end of test data (not part of the SQL query); query begins below this line
select p.empno, p.pay_date, o.state
from pay_history p inner join (
select empno, commence_date,
lead(commence_date, 1, date '2099-12-15')
over (partition by empno order by commence_date) as end_date,
state
from occupancy ) o
on p.empno = o.empno
and p.pay_date between o.commence_date and o.end_date
order by empno, pay_date -- if needed
;
Output:
EMPNO PAY_DATE STATE
----- ---------- -----
101 15/01/2016 VIC
101 15/02/2016 VIC
101 15/03/2016 VIC
101 15/04/2016 NSW
101 15/05/2016 NSW
101 15/06/2016 NSW
101 15/07/2016 NSW
101 15/08/2016 ACT
101 15/09/2016 ACT
9 rows selected.
Negotiating against myself here :-) I am posting this as a separate Answer, rather than editing my earlier one, because this is indeed a different answer.
Please refer to my other Answer for the input data and sample output - they are the same. Only the query is different. Instead of a join, we can UNION ALL the two tables (with some necessary adjustments: add a null column for state to the pay_history table, and a flag of 0 for the occupancy table and 1 for the pay_history table); then use the last_value() analytic function on the resulting union, and filter out the rows from the occupancy table in the outermost query. This may be quite a bit faster than the join-based solution.
select empno, dt as pay_date, state
from (
select empno, dt, flag,
last_value(state ignore nulls)
over (partition by empno order by dt, flag) as state
from (
select empno, commence_date as dt, state, 0 as flag
from occupancy
union all
select empno, pay_date, null, 1
from pay_history
)
)
where flag = 1
order by empno, pay_date -- if needed
;

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

Finding missing dates in a sequence

I have following table with ID and DATE
ID DATE
123 7/1/2015
123 6/1/2015
123 5/1/2015
123 4/1/2015
123 9/1/2014
123 8/1/2014
123 7/1/2014
123 6/1/2014
456 11/1/2014
456 10/1/2014
456 9/1/2014
456 8/1/2014
456 5/1/2014
456 4/1/2014
456 3/1/2014
789 9/1/2014
789 8/1/2014
789 7/1/2014
789 6/1/2014
789 5/1/2014
789 4/1/2014
789 3/1/2014
In this table, I have three customer ids, 123, 456, 789 and date column which shows which month they worked.
I want to find out which of the customers have gap in their work.
Our customers work record is kept per month...so, dates are monthly..
and each customer have different start and end dates.
Expected results:
ID First_Absent_date
123 10/01/2014
456 06/01/2014
To get a simple list of the IDs with gaps, with no further details, you need to look at each ID separately, and as #mikey suggested you can count the number of months and look at the first and last date to see if how many months that spans.
If your table has a column called month (since date isn't allowed unless it's a quoted identifier) you could start with:
select id, count(month), min(month), max(month),
months_between(max(month), min(month)) + 1 as diff
from your_table
group by id
order by id;
ID COUNT(MONTH) MIN(MONTH) MAX(MONTH) DIFF
---------- ------------ ---------- ---------- ----------
123 8 01-JUN-14 01-JUL-15 14
456 7 01-MAR-14 01-NOV-14 9
789 7 01-MAR-14 01-SEP-14 7
Then compare the count with the month span, in a having clause:
select id
from your_table
group by id
having count(month) != months_between(max(month), min(month)) + 1
order by id;
ID
----------
123
456
If you can actually have multiple records in a month for an ID, and/or the date recorded might not be the start of the month, you can do a bit more work to normalise the dates:
select id,
count(distinct trunc(month, 'MM')),
min(trunc(month, 'MM')),
max(trunc(month, 'MM')),
months_between(max(trunc(month, 'MM')), min(trunc(month, 'MM'))) + 1 as diff
from your_table
group by id
order by id;
select id
from your_table
group by id
having count(distinct trunc(month, 'MM')) !=
months_between(max(trunc(month, 'MM')), min(trunc(month, 'MM'))) + 1
order by id;
Oracle Setup:
CREATE TABLE your_table ( ID, "DATE" ) AS
SELECT 123, DATE '2015-07-01' FROM DUAL UNION ALL
SELECT 123, DATE '2015-06-01' FROM DUAL UNION ALL
SELECT 123, DATE '2015-05-01' FROM DUAL UNION ALL
SELECT 123, DATE '2015-04-01' FROM DUAL UNION ALL
SELECT 123, DATE '2014-09-01' FROM DUAL UNION ALL
SELECT 123, DATE '2014-08-01' FROM DUAL UNION ALL
SELECT 123, DATE '2014-07-01' FROM DUAL UNION ALL
SELECT 123, DATE '2014-06-01' FROM DUAL UNION ALL
SELECT 456, DATE '2014-11-01' FROM DUAL UNION ALL
SELECT 456, DATE '2014-10-01' FROM DUAL UNION ALL
SELECT 456, DATE '2014-09-01' FROM DUAL UNION ALL
SELECT 456, DATE '2014-08-01' FROM DUAL UNION ALL
SELECT 456, DATE '2014-05-01' FROM DUAL UNION ALL
SELECT 456, DATE '2014-04-01' FROM DUAL UNION ALL
SELECT 456, DATE '2014-03-01' FROM DUAL UNION ALL
SELECT 789, DATE '2014-09-01' FROM DUAL UNION ALL
SELECT 789, DATE '2014-08-01' FROM DUAL UNION ALL
SELECT 789, DATE '2014-07-01' FROM DUAL UNION ALL
SELECT 789, DATE '2014-06-01' FROM DUAL UNION ALL
SELECT 789, DATE '2014-05-01' FROM DUAL UNION ALL
SELECT 789, DATE '2014-04-01' FROM DUAL UNION ALL
SELECT 789, DATE '2014-03-01' FROM DUAL;
Query:
SELECT ID,
MIN( missing_date )
FROM (
SELECT ID,
CASE WHEN LEAD( "DATE" ) OVER ( PARTITION BY ID ORDER BY "DATE" )
= ADD_MONTHS( "DATE", 1 ) THEN NULL
WHEN LEAD( "DATE" ) OVER ( PARTITION BY ID ORDER BY "DATE" )
IS NULL THEN NULL
ELSE ADD_MONTHS( "DATE", 1 )
END AS missing_date
FROM your_table
)
GROUP BY ID
HAVING COUNT( missing_date ) > 0;
Output:
ID MIN(MISSING_DATE)
---------- -------------------
123 2014-10-01 00:00:00
456 2014-06-01 00:00:00
You could use a Lag() function to see if records have been skipped for a particular date or not.Lag() basically helps in comparing the data in current row with previous row. So if we order by DATE, we could easily compare and find any gaps.
select * from
(
select ID,DATE_, case when DATE_DIFF>1 then 1 else 0 end comparison from
(
select ID, DATE_ ,DATE_-LAG(DATE_, 1) OVER (PARTITION BY ID ORDER BY DATE_) date_diff from trial
)
)
where comparison=1 order by ID,DATE_;
This groups all the entries by id, and then arranges the records by date. If a customer is always present, there would not be a gap in his date. So anyone who has a date difference greater than 1 had a gap. You could tweak this as per your requirement.
EDIT : Just observed that you are storing data in mm/dd/yyyy format, when I closely observed above answers.You are storing only first date of every month. So, the above query can be tweaked as :
select * from
(
select ID,DATE_,PREV_DATE,last_day(PREV_DATE)+1 ABSENT_DATE, case when DATE_DIFF>31 then 1 else 0 end comparison from
(
select ID, DATE_ ,LAG(DATE_,1) OVER (PARTITION BY ID ORDER BY DATE_) PREV_DATE,DATE_-LAG(DATE_, 1) OVER (PARTITION BY ID ORDER BY DATE_) date_diff from trial
)
)
where comparison=1 order by ID,DATE_;