MSSQL Count multiple conditions in subquery or outer apply? - sql

I have a query which currently has a few counts in the field selection part.
SELECT userid,
(SELECT Count(*) AS Expr1
FROM dbo.relationships
WHERE ( authorised IS NOT NULL
AND expired IS NULL
AND relationshipended IS NULL )
AND ( mentorid = u.userid )) AS MenteeCount,
(SELECT Count(*) AS Expr1
FROM dbo.relationships AS Relationships_3
WHERE ( authorised IS NULL )
AND (rejecteddate IS NULL)
AND (mentorid = u.userid)) AS UnansweredRequests
FROM users
Would it be better (more streamlined) to do this using an outer apply as I have two counts coming from the same external table?
e.g. Using CASE WHEN?

You don't need to use apply, you can just use sum with case:
SELECT
u.userId,
MenteeCount = SUM(CASE WHEN authorised IS NOT NULL AND expired IS NULL AND relationshipended IS NULL THEN 1 ELSE 0 END),
UnansweredRequests =SUM(CASE WHEN authorised IS NULL AND rejecteddate IS NULL THEN 1 ELSE 0 END)
FROM
users u
INNER JOIN
dbo.relationships r
ON u.userid = mentorid

Yes. You should do the calculations in a single subquery:
SELECT u.userid, r.MenteeCount, r.UnansweredRequests
FROM users u OUTER APPLY
(SELECT SUM(CASE WHEN authorised IS NOT NULL AND expired IS NULL AND relationshipended IS NULL
THEN 1 ELSE 0
END) as MenteeCount
SUM(CASE WHEN authorised IS NULL AND rejecteddate IS NULL
THEN 1 ELSE 0
END) as UnansweredRequests
FROM dbo.relationships r
WHERE r.mentorid = u.userid
) r ;
That way, the processing for summarizing the relationships table is done only once for each mentorid. Note that when specifying correlation clauses (for either subqueries or apply, you should always use fully qualified column names. This helps avoid problems in the future.

Related

How to merge two rows having null values into one row replacing null values?

I'm having the following results from my sql query:
id
sp_firstname
sp_lastname
member_firstname
member_lastname
1
NULL
NULL
John
Smith
2
Dejuan
McLaughlin
NULL
NULL
2
NULL
NULL
Jack
Sparrow
3
John
Walker
NULL
NULL
3
NULL
NULL
Sherlock
Holmes
4
Mellie
Durgan
NULL
NULL
4
NULL
NULL
John
Waston
5
Lucy
Snider
NULL
NULL
Whereas what I need to achieve is this:
id
sp_firstname
sp_lastname
member_firstname
member_lastname
1
NULL
NULL
John
Smith
2
Dejuan
McLaughlin
Jack
Sparrow
3
John
Walker
Sherlock
Holmes
4
Mellie
Durgan
John
Waston
5
Lucy
Snider
NULL
NULL
Basically, I need to somehow merge pairs of rows that sort of have nulls crosswise.
After looking through SO answers, I could only find variants of this problem when NULL values needed to be substituted by numbers, and in that case people used max function combined with group by.
However I have several joins in my table and my NULL values need to be substituted with strings, not numbers, so max wouldn't really work here (as I thought).
Here's my sql code:
select
meeting.id,
(case when salesprofile.userid = "user".id then "user".firstname end) as sp_firstname,
(case when salesprofile.userid = "user".id then "user".lastname end) as sp_lastname,
(case when business.userid = "user".id then "user".firstname end) as member_firstname,
(case when business.userid = "user".id then "user".lastname end) as member_lastname
from
meeting
join project on project.id = meeting.projectid
left join business on business.id = project.businessid
left join salesprofile on salesprofile.id = meeting.salesprofileid
join "user" on "user".id = business.userid or "user".id = salesprofile.userid
group by
"user".id,
business.userid,
meeting.id,
salesprofile.userid;
These firstnames and lastnames come from the exact same user table, but they are taken based on different relations found in the same meeting table.
Basically, one meeting has two users: member and sp. And I needed a way to get meetings along with its member and sp users in one row.
How can I modify my sql query so that it would merge these pairs of rows with crosswise nulls into one row with data and without nulls?
Just use aggregation:
select meeting.id,
max(case when salesprofile.userid = "user".id then "user".firstname end) as sp_firstname,
max(case when salesprofile.userid = "user".id then "user".lastname end) as sp_lastname,
max(case when business.userid = "user".id then "user".firstname end) as member_firstname,
max(case when business.userid = "user".id then "user".lastname end) as member_lastname
from meeting join
project
on project.id = meeting.projectid left join
business
on business.id = project.businessid left join
salesprofile
on salesprofile.id = meeting.salesprofileid left join
"user"
on "user".id = business.userid or "user".id = salesprofile.userid
group by meeting.id;
Note the change to the group by as well.
You can wrap your results with an outer query to aggregate the columns using max and group by the id
select id, Max(sp_firstname) as sp_firstname, Max(sp_lastname) as sp_lastname...
from (
<inner query>
)x
group by id

How can I get a distinct count of a named inner join?

I've built a query for a summary table of information, and it's almost there, with one small bug. The confirmed_class_count variable comes back too high if there's multiple users on a class, leading me to believe that the number isn't distinct
Here's my current code:
SELECT "staffs".*,
count(distinct subclasses) as class_count,
sum(case when users.confirmed_at is not null then 1 else 0 end) confirmed_class_count
FROM
staffs
INNER JOIN classes as subclasses on staffs.staff_id = ANY(subclasses.staff)
INNER JOIN "classes_users" ON "classes_users"."class_id" = "subclasses"."id"
INNER JOIN "users" ON "users"."id" = "classes_users"."user_id"
INNER JOIN class_types ON class_types.code = subclasses.class_type_code
WHERE
(subclasses.closed_date is NULL OR subclasses.closed_date > '2019-09-06')
GROUP BY
staffs.id ORDER BY "staffs"."full_name" ASC
I want to replace the sum with something like (select distinct count(*) from subcases where users.confirmed_at is not null) as confirmed_case_count but I get relation "subclasses" does not exist.
How do I get what I'm intending here?
You can use count distinct with conditional aggregation. Replace
sum(class when users.confirmed_at is not null then 1 else 0 end) confirmed_class_count
^ looks like a typo, this should be case not class
with
count(distinct case when users.confirmed_at is not null then classes_users.class_id end) confirmed_class_count

Rewrite a query with GROUP BY ALL

Microsoft has deprecated GROUP BY ALL and while the query might work now, I'd like to future-proof this query for future SQL upgrades.
Currently, my query is:
SELECT qt.QueueName AS [Queue] ,
COUNT ( qt.QueueName ) AS [#ofUnprocessedEnvelopes] ,
COUNT ( CASE WHEN dq.AssignedToUserID = 0 THEN 1
ELSE NULL
END
) AS [#ofUnassignedEnvelopes] ,
MIN ( dq.DocumentDate ) AS [OldestEnvelope]
FROM dbo.VehicleReg_Documents_QueueTypes AS [qt]
LEFT OUTER JOIN dbo.VehicleReg_Documents_Queue AS [dq] ON dq.QueueID = qt.QueueTypeID
WHERE dq.IsProcessed = 0
AND dq.PageNumber = 1
GROUP BY ALL qt.QueueName
ORDER BY qt.QueueName ASC;
And the resulting data set:
<table><tbody><tr><td>Queue</td><td>#ofUnprocessedEnvelopes</td><td>#ofUnassignedEnvelopes</td><td>OldestEnvelope</td></tr><tr><td>Cancellations</td><td>0</td><td>0</td><td>NULL</td></tr><tr><td>Dealer</td><td>26</td><td>17</td><td>2018-04-06</td></tr><tr><td>Matched to Registration</td><td>93</td><td>82</td><td>2018-04-04</td></tr><tr><td>New Registration</td><td>166</td><td>140</td><td>2018-03-21</td></tr><tr><td>Remaining Documents</td><td>2</td><td>2</td><td>2018-04-04</td></tr><tr><td>Renewals</td><td>217</td><td>0</td><td>2018-04-03</td></tr><tr><td>Transfers</td><td>296</td><td>245</td><td>2018-03-30</td></tr><tr><td>Writebacks</td><td>53</td><td>46</td><td>2018-04-09</td></tr></tbody></table>
I've tried various versions using CTE's and UNION's but I cannot get result set to generate correctly - the records that have no counts will not display or I will have duplicate records displayed.
Any suggestions on how to make this work without the GROUP BY ALL?
Below is one attempt where I tried a CTE with a UNION:
;WITH QueueTypes ( QueueTypeID, QueueName )
AS ( SELECT QueueTypeID ,
QueueName
FROM dbo.VehicleReg_Documents_QueueTypes )
SELECT qt.QueueName AS [Queue] ,
COUNT ( qt.QueueName ) AS [#ofUnprocessedEnvelopes] ,
COUNT ( CASE WHEN dq.AssignedToUserID = 0 THEN 1
ELSE NULL
END
) AS [#ofUnassignedEnvelopes] ,
CONVERT ( VARCHAR (8), MIN ( dq.DocumentDate ), 1 ) AS [OldestEnvelope]
FROM QueueTypes AS qt
LEFT OUTER JOIN dbo.VehicleReg_Documents_Queue AS dq ON dq.QueueID = qt.QueueTypeID
WHERE dq.IsProcessed = 0
AND dq.PageNumber = 1
GROUP BY qt.QueueName
UNION ALL
SELECT qt.QueueName AS [Queue] ,
COUNT ( qt.QueueName ) AS [#ofUnprocessedEnvelopes] ,
COUNT ( CASE WHEN dq.AssignedToUserID = 0 THEN 1
ELSE NULL
END
) AS [#ofUnassignedEnvelopes] ,
CONVERT ( VARCHAR (8), MIN ( dq.DocumentDate ), 1 ) AS [OldestEnvelope]
FROM QueueTypes AS qt
LEFT OUTER JOIN dbo.VehicleReg_Documents_Queue AS dq ON dq.QueueID = qt.QueueTypeID
GROUP BY qt.QueueName
But the results are not close to being correct:
Your current query doesn't work as it seems to work, because while you outer join table VehicleReg_Documents_Queue you dismiss all outer joined rows in the WHERE clause, so you are where you would have been with a mere inner join. You may want to consider either moving your criteria to the ON clause or make this an inner join right away.
It is also weird that you join queue type and queue not on the queue ID or the queue type ID, but on dq.QueueID = qt.QueueTypeID. That's like joining employees and addresses on employee number matching the house number. At least that's what it looks like.
(Then why does your queue type table have a queue name? Shouldn't the queue table contain the queue name instead? But this is not about your query, but about your data model.)
GROUP BY ALL means: "Please give us all QueueNames, even when the WHERE clause dismisses them. I see two possibilities for your query:
You do want an outer join actually. Then there is no WHERE clause and you can simply make this GROUP BY qt.QueueName.
You don't want an outer join. Then we want a row per QueueName in the table, which we might not get with simply changing GROUP BY ALL qt.QueueName to GROUP BY qt.QueueName.
In that second case we want all QueueNames first and outer join your query:
select
qn.QueueName AS [Queue],
q.[#ofUnassignedEnvelopes],
q.[OldestEnvelope]
FROM (select distinct QueueName from VehicleReg_Documents_QueueTypes) qn
LEFT JOIN
(
SELECT qt.QueueName,
COUNT ( qt.QueueName ) AS [#ofUnprocessedEnvelopes] ,
COUNT ( CASE WHEN dq.AssignedToUserID = 0 THEN 1
ELSE NULL
END
) AS [#ofUnassignedEnvelopes] ,
MIN ( dq.DocumentDate ) AS [OldestEnvelope]
FROM dbo.VehicleReg_Documents_QueueTypes AS [qt]
JOIN dbo.VehicleReg_Documents_Queue AS [dq] ON dq.QueueID = qt.QueueTypeID
WHERE dq.IsProcessed = 0
AND dq.PageNumber = 1
) q ON q.QueueName = qn.QueueName
GROUP BY ALL qn.QueueName
ORDER BY qn.QueueName ASC;
I think the best corollary here for a 'GROUP BY ALL' into something more ANSI compliant would be a CASE statement. Without knowing your data, it's hard to say for sure if this is 1:1, but I'm betting it's in the ballpark.
SELECT qt.QueueName AS [Queue]
,COUNT(CASE
WHEN dq.IsProcessed = 0
AND dq.PageNumber = 1
THEN qt.QueueName
END) AS [#ofUnprocessedEnvelopes]
,COUNT(CASE
WHEN dq.AssignedToUserID = 0
AND dq.IsProcessed = 0
AND dq.PageNumber = 1
THEN 1
ELSE NULL
END) AS [#ofUnassignedEnvelopes]
,MIN(CASE
WHEN dq.IsProcessed = 0
AND dq.PageNumber = 1
THEN dq.DocumentDate
END) AS [OldestEnvelope]
FROM dbo.VehicleReg_Documents_QueueTypes AS [qt]
LEFT OUTER JOIN dbo.VehicleReg_Documents_Queue AS [dq] ON dq.QueueID = qt.QueueTypeID
GROUP BY qt.QueueName
ORDER BY qt.QueueName ASC;
That's a bit uglier because every aggregate has to have the WHERE conditions inside a case statement, but at least you are future proof.

Create overview table with true false values based on rows exist in child table

I have a table with cases, and another table with notifications.
For simplicity let's say the case table contains
id int
name nvarchar(100)
The notification table contains:
id int
caseid int
notificationtype string
Notification types can be either 'standard' or 'critical'.
I'd like an sql that can give me an overview for each case, and if they have any critical or standard notifications.
So a result like this:
CaseId CaseName StdNotification CriticalNotification
1 Test case yes no
I tried this SQL:
select distinct case.id as CaseId,
case.name as CaseName,
notifications.notificationtype,
case notifications when 'standard' then 'yes' else 'no' end as StdNotification,
case notifications when 'critical' then 'yes' else 'no' end as CriticalNotification
from cases
inner join notifications on Notifications.caseid = case.id
But this gives me duplicate rows for each combination
CaseId CaseName StdNotification CriticalNotification
1 Test case yes no
1 Test case no yes
So, how do I construct a sql that wil make some kind of "sum" and only return one row for each case?
You don't want distinct. You want group by. Your data structure suggests that a given case could have more than one notification, so I would go with counts using conditional aggregation:
select c.id as CaseId, c.name as CaseName,
sum(case when n.notificationtype = 'Standard' then 1 else 0 end) as NumStandard,
sum(case when n.notificationtype = 'Critical' then 1 else 0 end) as numCritical
from cases c left join
notifications n
on n.caseid = c.id
group by c.id, c.name;
You can convert these to "yes" and "no" using another case.
Also, note that I changed the inner join to a left join, so you'll get cases that have no notifications at all.
SELECT C.id as CaseId,
C.name as CaseName,
IIF(n1.notificationtype IS null, 'no', 'yes') as StdNotification,
IIF(n2.notificationtype IS null, 'no', 'yes') as CriticalNotification
FROM [case] C
left join notification n1
on C.id = n1.caseid and n1.notificationtype = 'standard'
left join notification n2
on C.id = n2.caseid and n2.notificationtype = 'critical'

incorrect joins - 1 row per result set

I trying to determine which people in my databases have either unsubscribed from my news letters, which people have bad email addresses and which dont have either. I have activities activities for both iUnsub' and 'iBadEmail'.
the code i wrote was
select distinct
n.id,
'Unsubscribe' =
case
when a.activity_type = 'iUnsub' then '1'
end,
'Bad Email' =
case
when a.activity_type = 'iBadEmail' then '1'
end
from name n
left join activity a on n.id = a.id
where n.id in
(
'1002421',
'1005587',
'1009073',
'1001102'
)
the results i receive creates 2 results for each id
id Unsubscribe Bad Email
1001102 NULL NULL
1002421 NULL NULL
1002421 1 NULL
1005587 NULL NULL
1005587 1 NULL
1009073 NULL 1
1009073 NULL NULL
i would like to the code to only give me one row for each id like below
id Unsubscribe Bad Email
1001102 NULL NULL
1002421 1 NULL
1005587 1 NULL
1009073 NULL 1
The problem is that you have multiple activity rows 3 or your names, and you are returning a row in the result for each activity. Name 1001102 either has no or only one activity which is neither Unsub or BadEmail.
select n.Id,
sum(case when a.activity_id = 'iUnSub' then 1 else 0 end) UnSub,
sum(case when a.activity_id = 'iBadEmail' then 1 else 0 end) BadEmail
from name
left outer join activity a on n.id = a.id
where a.activity
and n.id in
(
'1002421',
'1005587',
'1009073',
'1001102'
)
group by n.Id
This will give you a non-zero figure if UnSubbed or BadEmail, and if both are 0, then it's presumably OK.
The left outer join is for cases whene a name has no activity rows. If you don't include that then they will not be included in the output. If that's fine, change it to an inner join.