user915331
user915331

Reputation:

SQL query for getting active employees in specific period

Having the following table:

    ID     EmployeeID      Status       EffectiveDate
  ------------------------------------------------------
     1       110545        Active        01AUG2011
     2       110700        Active        05JAN2012
     3       110060        Active        05JAN2012
     4       110222        Active        30JUN2012
     5       110545        Resigned      01JUL2012
     6       110545        Active        12FEB2013

How do I get the number of active (or partially active) in a specific period? For example, if I want to know all active (or partially active) employees from 01JAN2011 to 01AUG2012 I should get 4 (according to the table above). If I want to know all active employees from 01AUG2012 to 01JAN2013 it should be 3 only (because employee 110454 is resigned).

How will I do that?

Upvotes: 8

Views: 16264

Answers (6)

Oleksandr Fedorenko
Oleksandr Fedorenko

Reputation: 16904

Another solution using the PIVOT operator

DECLARE @StartDate date = '20120801',
        @EndDate date = '20130101'
SELECT COUNT(*)
FROM (
      SELECT EffectiveDate, EmployeeID, [Status]
      FROM EmployeeActivity
      WHERE EffectiveDate < @EndDate
      ) x
PIVOT
 (
  MAX(EffectiveDate) FOR [Status] IN([Resigned], [Active])
  ) p
WHERE ISNULL(Resigned, '99991231') > @StartDate

See demo on SQLFiddle

Upvotes: 1

user440595
user440595

Reputation:

Sample data:

CREATE TABLE #Employee
(
    ID              integer NOT NULL,
    EmployeeID      integer NOT NULL,
    [Status]        varchar(8) NOT NULL,
    EffectiveDate   date NOT NULL,

    CONSTRAINT [PK #Employee ID]
        PRIMARY KEY CLUSTERED (ID)
);

INSERT #Employee
    (ID, EmployeeID, [Status], EffectiveDate)
VALUES
     (1, 110545, 'Active', '20110801'),
     (2, 110700, 'Active', '20120105'),
     (3, 110060, 'Active', '20120105'),
     (4, 110222, 'Active', '20120630'),
     (5, 110545, 'Resigned', '20120701'),
     (6, 110545, 'Active', '20130212');

Helpful indexes:

CREATE NONCLUSTERED INDEX Active
ON #Employee
    (EffectiveDate)
INCLUDE
    (EmployeeID)
WHERE
    [Status] = 'Active';

CREATE NONCLUSTERED INDEX Resigned
ON #Employee
    (EmployeeID, EffectiveDate)
WHERE
    [Status] = 'Resigned';

Solution with comments in-line:

CREATE TABLE #Selected (EmployeeID integer NOT NULL);

DECLARE 
    @start date = '20110101',
    @end   date = '20120801';

INSERT #Selected (EmployeeID)
SELECT
    E.EmployeeID
FROM #Employee AS E
WHERE
    -- Employees active before the end of the range
    E.[Status] = 'Active'
    AND E.EffectiveDate <= @end
    AND NOT EXISTS
    (
        SELECT * 
        FROM #Employee AS E2
        WHERE
            -- No record of the employee
            -- resigning before the start of the range
            -- and after the active date
            E2.EmployeeID = E.EmployeeID
            AND E2.[Status] = 'Resigned'
            AND E2.EffectiveDate >= E.EffectiveDate
            AND E2.EffectiveDate <= @start
    )
OPTION (RECOMPILE);

-- Return a distinct list of employees
SELECT DISTINCT
    S.EmployeeID 
FROM #Selected AS S;

Execution plan:

Execution plan

SQLFiddle here

Upvotes: 8

Cristian Lupascu
Cristian Lupascu

Reputation: 40576

You can use this query to build a list of employees and their start/resignation dates:

select 
  start.*,
  resignation.EffectiveDate as ResignationDate
from Employment start
outer apply (
  select top 1 
    Id,
    EmployeeId,
    EffectiveDate
  from Employment
  where EmployeeId = start.EmployeeId
  and Status = 'Resigned'
  and Id > start.Id
  order by Id  
) resignation
where start.Status='Active'

The key here is the use of OUTER APPLY, which allows us to use a pretty "funky" join criterion.

Here's how it works: http://www.sqlfiddle.com/#!3/ec969/7


From here, it's just a matter of querying the records whose the employment interval overlaps the target interval.

There are many ways to write this, but I personally like using a CTE, because I find it a bit more readable:

;with EmploymentPeriods as (
    select 
      start.EmployeeId,
      start.EffectiveDate as StartDate,
      isnull(resignation.EffectiveDate, '9999-01-01') as EndDate 
    from Employment start
    outer apply (
      select top 1 
        Id,
        EmployeeId,
        EffectiveDate
      from Employment
      where EmployeeId = start.EmployeeId
      and Status = 'Resigned'
      and Id > start.Id
      order by Id  
    ) resignation
    where start.Status='Active'
)
select distinct EmployeeId
from EmploymentPeriods
where EndDate >= @QueryStartDate
  and StartDate <= @QueryEndDate

SQLFiddles:

Upvotes: 0

Andriy M
Andriy M

Reputation: 77737

1. Turn your events into ranges:

ID EmployeeID Status   EffectiveDate   ID EmployeeID Status   StartDate EndDate
-- ---------- -------- -------------   -- ---------- -------- --------- ---------
1  110545     Active   01AUG2011       1  110545     Active   01AUG2011 01JUL2012
2  110700     Active   05JAN2012       2  110700     Active   05JAN2012 31DEC9999
3  110060     Active   05JAN2012    => 3  110060     Active   05JAN2012 31DEC9999
4  110222     Active   30JUN2012       4  110222     Active   30JUN2012 31DEC9999
5  110545     Resigned 01JUL2012       5  110545     Resigned 01JUL2012 12FEB2013
6  110545     Active   12FEB2013       6  110545     Active   12FEB2013 31DEC9999

2. Get active employees based on this condition:

WHERE Status = 'Active'
  AND StartDate < @EndDate
  AND EndDate > @StartDate

3. Count distinct EmployeeID values.

This is how you could implement the above:

WITH ranked AS (
  SELECT
    *,
    rn = ROW_NUMBER() OVER (PARTITION BY EmployeeID ORDER BY EffectiveDate)
  FROM EmployeeActivity
),
ranges AS (
  SELECT
    s.EmployeeID,
    s.Status,
    StartDate = s.EffectiveDate,
    EndDate   = ISNULL(e.EffectiveDate, '31DEC9999')
  FROM ranked s
  LEFT JOIN ranked e ON s.EmployeeID = e.EmployeeID AND s.rn = e.rn - 1
)
SELECT
  ActiveCount = COUNT(DISTINCT EmployeeID)
FROM ranges
WHERE Status = 'Active'
  AND StartDate < '01JAN2013'
  AND EndDate   > '01AUG2012'
;

A SQL Fiddle demo for this query: http://sqlfiddle.com/#!3/c3716/3

Upvotes: 4

Nenad Zivkovic
Nenad Zivkovic

Reputation: 18559

This should work fine:

DECLARE @d1 date = '01AUG2012';
DECLARE @d2 date = '01JAN2014';

WITH CTE_Before AS 
(
    --Last status of each employee before period will be RN=1
    SELECT *, ROW_NUMBER() OVER (PARTITION BY EmployeeID ORDER BY EffectiveDate DESC) RN
    FROM dbo.Table1
    WHERE EffectiveDate < @d1
)
, CTE_During AS
(
    --Those who become active during period
    SELECT * FROM dbo.Table1
    WHERE [Status] = 'Active' AND EffectiveDate BETWEEN @d1 AND @d2
)
--Union of those who were active at the beginning of period and those who became active during period
SELECT EmployeeID FROM CTE_Before WHERE RN = 1 AND Status = 'Active'
UNION
SELECT EmployeeID FROM CTE_During

SQLFiddle DEMO

Upvotes: 0

Hassan
Hassan

Reputation: 1433

This should work (not tested)

SELECT COUNT DISTINCT EmployeeID FROM TABLE 
WHERE EffectiveDate > CONVERT(VARCHAR(11), '08-01-2012', 106) AS [DDMONYYYY] 
and EffectiveDate < CONVERT(VARCHAR(11), '01-01-2013', 106) AS [DDMONYYYY]
AND Status = 'Active'

Upvotes: 1

Related Questions