Cannot exclude previous non-duplicate rows

Viewed 62

In a nutshell, here it is:

  • I have 1000(ish) employees who have multiple recurrent annual training requirements
  • I need to be able to sort the employees by County, Facility, Employee, and Type of Training (and also allow for sorted lists at each level)
  • I want to display only the most recent date the Employee took the training

What I've tried so far:

  • I've been successful when dealing with only one Employee's record:

    DECLARE @Skill int
    SET @Skill = 81
    
    SELECT TOP 1
        P.lastname+', '+P.firstname AS Employee,    
        P.external_id,
        PC.job_title,
        SD.name,
        SV.schedule_days as ExpireInterval,
        PO.course_startdate,
        DATEADD(DD,SV.schedule_days,PO.course_startdate) as ExpireDate
    
    FROM portfolio PO
        INNER JOIN person P ON PO.person_id=P.person_id
        INNER JOIN e_component EC ON PO.component_id=EC.component_id
        JOIN skill_value SV ON EC.component_id=SV.object_id
        JOIN skill_description SD ON SV.skill_id=SD.skill_id
        JOIN person_custom PC ON P.person_id=PC.person_id
    
    GROUP BY 
        PO.person_id, 
        PO.course_startdate, 
        SV.skill_id, 
        P.lastname, 
        P.firstname, 
        P.external_id, 
        PC.job_title, 
        SD.name, 
        SV.schedule_days, 
        SD.language_id
    
    HAVING SD.language_id=26
        AND PO.person_id=123456
        AND SV.skill_id= @Skill
    
    ORDER BY Employee, PO.course_startdate DESC
    

NOTE: The excessive JOINS are due to the lack of FK relationships in the host database. Our vendor designed it to rely mostly on code built into their front end so I'm working with what I've got.

The previously listed code returns the following result:

Most Recent Record for Employee #123456

  • When I try to pull the most recent record from a list of employees however:

    DECLARE @Skill int
    SET @Skill = 81
    
    SELECT
        P.lastname+', '+P.firstname AS Employee,    
        P.external_id,
        PC.job_title,
        SD.name,
        SV.schedule_days as ExpireInterval,
        PO.course_startdate,
        DATEADD(DD,SV.schedule_days,PO.course_startdate) as ExpireDate
    
    FROM portfolio PO
        INNER JOIN person P ON PO.person_id=P.person_id
        INNER JOIN e_component EC ON PO.component_id=EC.component_id
        JOIN skill_value SV ON EC.component_id=SV.object_id
        JOIN skill_description SD ON SV.skill_id=SD.skill_id
        JOIN person_custom PC ON P.person_id=PC.person_id
    
    GROUP BY 
        PO.person_id, 
        PO.course_startdate, 
        SV.skill_id, 
        P.lastname, 
        P.firstname, 
        P.external_id, 
        PC.job_title, 
        SD.name, 
        SV.schedule_days, 
        SD.language_id
    
    HAVING SD.language_id=26
        AND PO.person_id IN (SELECT DISTINCT person_id FROM portfolio)
        AND SV.skill_id= @Skill
    
    ORDER BY Employee, PO.course_startdate DESC
    

I get multiple entries for the same Employee (e.g.different times that employee has taken the training with the same skill_id).

What I want to do is something like this:

IF count(SV.skill_id)>1
    THEN SELECT TOP 1 component_id --for each individual
        FROM portfolio

I just can't figure out where to put the condition to have it give me one record per person. I've tried assigning local variables, moving the SELECT subquery around to various columns, adding and removing constraints... etc. Nothing has worked so far.

I'm using the following software:

  • SQL Server Management Studio 2014 & 2017 (the live DB is on 2014 and I have a static one on 2017 for development purposes)
  • Report Builder 3.0 (my company hasn't upgraded to the latest and greatest yet)

P.S. If there is a method of sorting the records on the report form itself using Regular Expressions, please let me know!

3 Answers

A couple of observations, then an answer.

In SQL Server, INNER JOIN and JOIN mean the same thing.

As @DaleBurrell notes, unless you're filtering by an aggregated value, use a WHERE clause rather than a HAVING clause. The WHERE is applied earlier in the query processing and you should see modestly better performance putting your filtering there. Also, it's more "standard", if you will.

Finally, I removed your filtering sub-query for person_id because it's a self-join to portfolio that I couldn't see a good reason for. If there are additional criteria in there that make it useful, go ahead and put it back.

With that said, your second attempt was really close. If you RANK your results using your existing ORDER BY clause, then apply TOP (1) WITH TIES, it will return the #1 ranked result for each employee, ordered by date.

DECLARE @Skill int
SET @Skill = 81

SELECT TOP (1) WITH TIES
    P.lastname+', '+P.firstname AS Employee,    
    P.external_id,
    PC.job_title,
    SD.name,
    SV.schedule_days as ExpireInterval,
    PO.course_startdate,
    DATEADD(DD,SV.schedule_days,PO.course_startdate) as ExpireDate
FROM portfolio PO
    JOIN person P ON PO.person_id=P.person_id
    JOIN e_component EC ON PO.component_id=EC.component_id
    JOIN skill_value SV ON EC.component_id=SV.object_id
    JOIN skill_description SD ON SV.skill_id=SD.skill_id
    JOIN person_custom PC ON P.person_id=PC.person_id
    JOIN portfolio PF ON PO.person_id = PF.person_id

WHERE SD.language_id=26
    AND SV.skill_id= @Skill

GROUP BY 
    PO.person_id, 
    PO.course_startdate, 
    SV.skill_id, 
    P.lastname, 
    P.firstname, 
    P.external_id, 
    PC.job_title, 
    SD.name, 
    SV.schedule_days, 
    SD.language_id

ORDER BY RANK() OVER (PARTITION BY Employee ORDER BY PO.course_startdate DESC)

If you group by the course name, and select max(course_date) you'll get it e.g.

DECLARE @Skill int
SET @Skill = 81

SELECT TOP 1
    P.lastname+', '+P.firstname AS Employee,    
    P.external_id,
    PC.job_title,
    SD.name,
    SV.schedule_days as ExpireInterval,
    max(PO.course_startdate) most_recent_course_startdate,
    max(DATEADD(DD,SV.schedule_days,PO.course_startdate)) as ExpireDate

FROM portfolio PO
    INNER JOIN person P ON PO.person_id=P.person_id
    INNER JOIN e_component EC ON PO.component_id=EC.component_id
    JOIN skill_value SV ON EC.component_id=SV.object_id
    JOIN skill_description SD ON SV.skill_id=SD.skill_id
    JOIN person_custom PC ON P.person_id=PC.person_id

where SD.language_id=26
    AND PO.person_id=123456
    AND SV.skill_id= @Skill

GROUP BY 
    PO.person_id, 
    --PO.course_startdate, 
    SV.skill_id, 
    P.lastname, 
    P.firstname, 
    P.external_id, 
    PC.job_title, 
    SD.name, 
    SV.schedule_days, 
    SD.language_id

ORDER BY Employee, most_recent_course_startdate DESC

Also HAVING is for using aggregate conditions, otherwise just stick to WHERE.

You pretty much found the issue with your "what I want to do" snippet, and that is you can't use TOP 1 + ORDER BY to get the most recent record when you have more than 1 user (ie want more than 1 row returned).

ROW_NUMBER() is a good way to handle this. It assigns a number to each row based on conditions.

For instance, ROW_NUMBER() OVER (PARTITION BY PO.person_id ORDER BY PO.course_startdate DESC) as RN will assign a 1 to each row with the most recent PO.course_startdate for each PO.person_id. If you do this within a derived table or CTE, then you simply need to filter to RN = 1 in your final/outer select in order to find the most recent row for each user.

CTE example:

DECLARE @Skill int
SET @Skill = 81

;WITH yourCTE as (
SELECT
    P.lastname+', '+P.firstname AS Employee,    
    P.external_id,
    PC.job_title,
    SD.name,
    SV.schedule_days as ExpireInterval,
    PO.course_startdate,
    DATEADD(DD,SV.schedule_days,PO.course_startdate) as ExpireDate,
    ROW_NUMBER() OVER (PARTITION BY PO.person_id ORDER BY PO.course_startdate DESC) as RN

FROM portfolio PO
JOIN person P ON PO.person_id=P.person_id
JOIN e_component EC ON PO.component_id=EC.component_id
JOIN skill_value SV ON EC.component_id=SV.object_id
JOIN skill_description SD ON SV.skill_id=SD.skill_id
JOIN person_custom PC ON P.person_id=PC.person_id

WHERE SD.language_id=26
AND SV.skill_id= @Skill
)

SELECT employee, extenal_id, job_title, name, 
       ExpireInterval, course_startdate, ExpireDate
FROM yourCTE
WHERE RN = 1

I also moved your HAVING conditions to WHERE (and removed one redundant one), shorthanded the INNER JOINs to JOINs (just to be consistent), and removed your GROUP BY and ORDER BY. I didn't see a point to the grouping, but you can add the ORDER BY to the final select if you still want it.

Related