Here is what should be an equivalent query using EXISTS and NOT EXISTS:
SELECT a.UserId, COUNT(DISTINCT a.CustomerId) AS TotalUniqueContact FROM [UserActivityLog] a WITH(NOLOCK) WHERE CAST(a.ActivityDatetime AS DATE) BETWEEN '2015-09-28' AND '2015-09-30' AND EXISTS (SELECT * FROM [User] b WHERE b.Id = a.UserId AND b.UserType = 'EpicUser' AND b.IsEpicEmployee = 1 AND b.IsActive = 1) AND NOT EXISTS (SELECT * FROM [CustomerNoteInteractions] b WITH(NOLOCK) JOIN [User] c ON c.Id = b.UserId AND c.UserType = 'EpicUser' AND c.IsEpicEmployee = 1 AND c.IsActive = 1 WHERE b.activitylogid = a.ID AND b.reason IN ('20', '36') AND CAST(b.datecreated AS DATE) BETWEEN '2015-09-28' AND '2015-09-30' ) GROUP BY a.UserId
Obviously, it's hard to understand what will truly help your performance without understanding your data. But here is what I expect:
- I think the
EXISTS/NOT EXISTS version of the query will help. - I think your conditions on
UserActivityLog.ActivityDateTime and CustomerNoteInteractions.datecreated are a problem. Why are you casting? Is it not a date type? If not, why not? You would probably get big gains if you could take advantage of an index on those columns. But with the cast, I don't think you can use an index there. Can you do something about it? - You'll also probably benefit from indexes on
User.Id (probably the PK anyways), and CustomerNoteInteractions.ActivityLogId.
Also, not a big fan of using with (nolock) to improve performance (Bad habits : Putting NOLOCK everywhere).
EDIT
If your date columns are of type DateTime as you mention in the comments, and so you are using the CAST to eliminate the time portion, a much better alternative for performance is to not cast, but instead modify the way you filter the column. Doing this will allow you to take advantage of any index on the date column. It could make a very big difference.
The query could then be further improved like this:
SELECT a.UserId, COUNT(DISTINCT a.CustomerId) AS TotalUniqueContact FROM [UserActivityLog] a WITH(NOLOCK) WHERE a.ActivityDatetime >= '2015-09-28' AND a.ActivityDatetime < dateadd(day, 1, '2015-09-30') AND EXISTS (SELECT * FROM [User] b WHERE b.Id = a.UserId AND b.UserType = 'EpicUser' AND b.IsEpicEmployee = 1 AND b.IsActive = 1) AND NOT EXISTS (SELECT * FROM [CustomerNoteInteractions] b WITH(NOLOCK) JOIN [User] c ON c.Id = b.UserId AND c.UserType = 'EpicUser' AND c.IsEpicEmployee = 1 AND c.IsActive = 1 WHERE b.activitylogid = a.ID AND b.reason IN ('20', '36') AND b.datecreated >= '2015-09-28' AND b.datecreated < dateadd(day, 1, '2015-09-30')) GROUP BY a.UserId
NOT IN, why ? , because there are a SUB QUERY within SUB QUERY under your NOT IN condition, meaning to say, you have 3 level of query to execute only inNOT INand that may cause too much time.INNER JOINandLEFT JOINto replace your sub query inNOT IN.