I am writing a sql where I am trying to pull out information of the status of the courses the user has enrolled. I am trying to return single record for each user. Two fields in the select list would derive the value in the following manner
CourseResultStatusId -
If the status of all the courses is passed then return the status as passed otherwise.
If any status is fail, the overall status is fail.
If any of the status is expired, then overall status is expired.
If any of the status is in-progress then overall status is in-progress
ExpiryDateTime - Training expiring (nearest date)
I need to apply the following logic on courses he has been assigned.
cr.ExpiryDateTime > GetDate() and cr.ExpiryDateTime <= dateadd(dd,30,getdate()) )
If you see below , the query I have written so far pulls the courses that each user has been enrolled but it is not a cumulative result. Do I need to group, if yes would need help.
DECLARE #Rep1 INT;
SET #Rep1 = 13119;
SELECT
cr.[CourseID]
,cr.[UserID]
,u.[Code]
,u.[DisplayName]
,t.[Name]
,cr.[CourseResultStatusID] AS [CourseResultStatusID]
,crs.[Description] AS [CourseResultStatusDescription]
,c.[PointsRequired]
,cr.[ExpiryDateTime]
FROM [training].[CourseResult] cr
INNER JOIN [training].[Course] c
ON cr.[CourseID] = c.[ID] and c.[IsOptional] = 0 -- and cr.ExpiryDateTime > GetDate() and cr.ExpiryDateTime <= dateadd(dd,30,getdate())
INNER JOIN [training].[CourseResultStatus] crs
ON cr.[CourseResultStatusID] = crs.[ID]
INNER JOIN org.RepresentativeTierHistory rth on rth.RepresentativeID = cr.[UserID] and GetDate() between rth.StartDate and rth.EndDate
INNER JOIN org.tier t on t.ID = rth.TierID
LEFT JOIN [org].[User] u
ON u.[ID] = cr.[UserID]
WHERE cr.[UserID] IN (
SELECT hd.DescendantId FROM org.HierarchyDescendant hd WHERE hd.RepresentativeId = #Rep1 UNION ALL SELECT #Rep1 -- for management exchange info
)
order by UserID
The result of the query is as follows. I have circled to show you records that belong to a particular user and the columns that I am interested in . I need help in getting single record for each user based on the of logic that I mentioned above.
If I followed you correctly, you can implement the priorization rules on the overall result of each user using conditional aggregation.
Starting from your existing query, the logic would be:
select
cr.[UserID],
case
when min(case when crs.[Description] = 'Complete' then 1 else 0 end) = 1
then 'Complete'
when max(case when crs.[Description] = 'Fail' then 1 else 0 end) = 1
then 'Fail'
when max(case when crs.[Description] = 'Expired' then 1 else 0 end) = 1
then 'Expired'
when max(case when crs.[Description] = 'In Progress' then 1 else 0 end) = 1
then 'In Progress'
end as ResultStatus
from ...
where ...
group by cr.[UserID]
As for the date filtering logic, you should be able to implement it directly in the where clause.
It is possible that other parts of your query can be optimized - you might want to ask a new question for this, providing proper sample data and desired results.
Related
I'm working on SMS-Gateway that holds multiple charged SMS-services with different numbers,
each SMS sent to the customer has 4 status as below (forwarded, delivered, expired,delivery failed)
Now I have the below first_table for the charging-system with the below details (TABLE-A)
and below (TABLE-B) which contain the status of each sent SMS with its ID
Below is my expected final result to forecast the details for each sms-service :
At first I thought it was easy all I need is just to use COUNT(Case when ...)
but in my case I have thousands of SMS-numbers(services) so if I use this approach it will be like that:-
COUNT(CASE WHEN a.SMS_SHORT_CODE='1111' AND B.STATUS='forwarded' )
COUNT(CASE WHEN a.SMS_SHORT_CODE='1111' AND B.STATUS='delivered' )
COUNT(CASE WHEN a.SMS_SHORT_CODE='1111' AND B.STATUS='expired' )
COUNT(CASE WHEN a.SMS_SHORT_CODE='1111' AND B.STATUS='delivery failed' )
COUNT(CASE WHEN a.SMS_SHORT_CODE='5000' AND B.STATUS='forwarded' )
COUNT(CASE WHEN a.SMS_SHORT_CODE='5000' AND B.STATUS='delivered' )
COUNT(CASE WHEN a.SMS_SHORT_CODE='5000' AND B.STATUS='expired' )
COUNT(CASE WHEN a.SMS_SHORT_CODE='5000' AND B.STATUS='delivery failed' )
...
...
...
...
...
...
...
The above approach not practical when you have many services also noting that CASE can handle only 250 conditions?
So what is the best approach to do left outer join for (Table A) on (Table B) using the SMS-ID and count each SMS-status and forecast it as below?
I would suggest conditional aggregation:
select b.SMS_SHORT_CODE,
sum(case when status = 'forwaded' then 1 else 0 end) as count_of_forwaded,
sum(case when status = 'delivered' then 1 else 0 end) as count_of_status,
sum(case when status = 'expired' then 1 else 0 end) as count_of_expired,
sum(case when status = 'delivery failed' then 1 else 0 end) as count_of_delivery_failed
from TABLEB b
group by b.SMS_SHORT_CODE ;
Note that no JOIN is necessary. All the data you want to aggregate is in TABLEB.
Please use below query,
select
A.SMS_SHORT_CODE,
case when status = 'forwaded' then count(status ) end as count_of_forwaded,
case when status = 'delivered' then count(status ) end as count_of_status,
case when status = 'expired' then count(status ) end as count_of_expired,
case when status = 'delivery failed' then count(status ) end as count_of_delivery_failed
from TABLEA A
inner join TABLEB B
on (A.SMS_ID = B.SMS_ID)
group by A.SMS_SHORT_CODE, status ;
You can use PIVOT clause (introduced in Oracle 11g version) for those status columns :
SELECT sms_short_code,
COUNT_OF_forwarded,
COUNT_OF_delivered,
COUNT_OF_expired,
COUNT_OF_delivery_failed
FROM tableB
PIVOT
(
COUNT(*) FOR status IN ( 'forwarded' AS COUNT_OF_forwarded,
'delivered' AS COUNT_OF_delivered,
'expired' AS COUNT_OF_expired,
'delivery failed' AS COUNT_OF_delivery_failed )
)
e.g. only using TableB is enough.
Demo
This question already has an answer here:
How to use an Alias in a Calculation for Another Field
(1 answer)
Closed 3 years ago.
In my query below I am counting occurrences in a table based on the Status column. I also want to perform calculations based on the counts I am returning. For example, let's say I want to add 100 to the Snoozed value... how do I do this? Below is what I thought would do it:
SELECT
pu.ID Id, pu.Name Name,
COUNT(*) LeadCount,
SUM(CASE WHEN Status = 'Working' THEN 1 ELSE 0 END) AS Working,
SUM(CASE WHEN Status = 'Uninterested' THEN 1 ELSE 0 END) AS Uninterested,
SUM(CASE WHEN Status = 'Converted' THEN 1 ELSE 0 END) AS Converted,
SUM(CASE WHEN SnoozedId > 0 THEN 1 ELSE 0 END) AS Snoozed,
Snoozed + 100 AS Test
FROM
Prospects p
INNER JOIN
ProspectsUsers pu on p.OwnerId = pu.SalesForceId
WHERE
p.Store = '108'
GROUP BY
pu.Name, pu.Id
ORDER BY
Name
I get this error:
Invalid column name 'Snoozed'.
How can I take the value of the previous SUM statement, add 100 to it, and return it as another column? What I was aiming for is an additional column labeled Test that has the Snooze count + 100.
You can't use one column to create another column in the same way that you are attempting. You have 2 options:
Do the full calculation (as #forpas has mentioned in the comments above)
Use a temp table or table variable to store the data, this way you can get the first 5 columns, and then you can add the last column or you can select from the temp table and do the last column calculations from there.
You can not use an alias as a column reference in the same query. The correct script is:
SELECT
pu.ID Id, pu.Name Name,
COUNT(*) LeadCount,
SUM(CASE WHEN Status = 'Working' THEN 1 ELSE 0 END) AS Working,
SUM(CASE WHEN Status = 'Uninterested' THEN 1 ELSE 0 END) AS Uninterested,
SUM(CASE WHEN Status = 'Converted' THEN 1 ELSE 0 END) AS Converted,
SUM(CASE WHEN SnoozedId > 0 THEN 1 ELSE 0 END)+100 AS Snoozed
FROM
Prospects p
INNER JOIN
ProspectsUsers pu on p.OwnerId = pu.SalesForceId
WHERE
p.Store = '108'
GROUP BY
pu.Name, pu.Id
ORDER BY
Name
MSSQL does not allow you to reference fields (or aliases) in the SELECT statement from within the same SELECT statement.
To work around this:
Use a CTE. Define the columns you want to select from in the CTE, and then select from them outside the CTE.
;WITH OurCte AS (
SELECT
5 + 5 - 3 AS OurInitialValue
)
SELECT
OurInitialValue / 2 AS OurFinalValue
FROM OurCte
Use a temp table. This is very similar in functionality to using a CTE, however, it does have different performance implications.
SELECT
5 + 5 - 3 AS OurInitialValue
INTO #OurTempTable
SELECT
OurInitialValue / 2 AS OurFinalValue
FROM #OurTempTable
Use a subquery. This tends to be more difficult to read than the above. I'm not certain what the advantage is to this - maybe someone in the comments can enlighten me.
SELECT
5 + 5 - 3 AS OurInitialValue
FROM (
SELECT
OurInitialValue / 2 AS OurFinalValue
) OurSubquery
Embed your calculations. opinion warning This is really sloppy, and not a great approach as you end up having to duplicate code, and can easily throw columns out-of-sync if you update the calculation in one location and not the other.
SELECT
5 + 5 - 3 AS OurInitialValue
, (5 + 5 - 3) / 2 AS OurFinalValue
You can't use a column alias in the same select. The column alias do not precedence / sequence; they are all created after the eval of the select result, just before group by and order by.
You must repeat code :
SELECT
pu.ID Id,pu.Name Name,
COUNT(*) LeadCount,
SUM(CASE WHEN Status = 'Working' THEN 1 ELSE 0 END) AS Working,
SUM(CASE WHEN Status = 'Uninterested' THEN 1 ELSE 0 END) AS Uninterested,
SUM(CASE WHEN Status = 'Converted' THEN 1 ELSE 0 END) AS Converted,
SUM(CASE WHEN SnoozedId > 0 THEN 1 ELSE 0 END) AS Snoozed,
SUM(CASE WHEN SnoozedId > 0 THEN 1 ELSE 0 END)+ 100 AS Test
FROM
Prospects p
INNER JOIN
ProspectsUsers pu on p.OwnerId = pu.SalesForceId
WHERE
p.Store = '108'
GROUP BY
pu.Name, pu.Id
ORDER BY
Name
If you don't want to repeat the code, use a subquery
SELECT
ID, Name, LeadCount, Working, Uninterested,Converted, Snoozed, Snoozed +100 AS test
FROM
(SELECT
pu.ID Id,pu.Name Name,
COUNT(*) LeadCount,
SUM(CASE WHEN Status = 'Working' THEN 1 ELSE 0 END) AS Working,
SUM(CASE WHEN Status = 'Uninterested' THEN 1 ELSE 0 END) AS Uninterested,
SUM(CASE WHEN Status = 'Converted' THEN 1 ELSE 0 END) AS Converted,
SUM(CASE WHEN SnoozedId > 0 THEN 1 ELSE 0 END) AS Snoozed
FROM Prospects p
INNER JOIN ProspectsUsers pu on p.OwnerId = pu.SalesForceId
WHERE p.Store = '108'
GROUP BY pu.Name, pu.Id) t
ORDER BY Name
or a view
I actually have a query joining 3 tables to collect informations so I can calculate some KPIs, but my logic is flawed, here is my actual query :
SELECT t.idCustomer, t.nameCustomer
COUNT(DISTINCT t.idTrip),
SUM(
CASE
WHEN t.tripDone <> 1
THEN 1
ELSE 0
END),
SUM(CASE
WHEN t.codeIncident = 'CANCEL'
THEN 1
ELSE 0
END)
FROM
(SELECT customer.idCustomer, customer.nameCustomer, trip.tripDone, incident.codeIncident
FROM CUSTOMER customer
JOIN TRIP trip ON customer.idCustomer = trip.idCustomer
JOIN INCIDENT incident ON trip.idTrip = incident.idTrip) t
GROUP BY t.idCustomer, t.nameCustomer
So, I want to know for each Customer :
COUNT(DISTINCT t.idTrip) -> The number of trips by this customer
Sum when t.tripDone <> 1 -> The number of trips that are done by this customer ( not ingoing )
Sum when t.codeIncident = 'CANCEL' -> The number of trips by this customer where there was a cancellation.
The big mistake I made here, is that a trip can have multiple codeIncidents (example : one record for an idTrip with the codeIncident 'CANCEL' and another record with same idTrip with the codeIncident 'DELAYED'), so when I calculate the Sum when t.tripDone <> 1 I get a result of : '2' instead of '1' (because there are 2 records in my from Clause that have the t.tripDone <> 1 for the same idTrip).
Would you have any idea on how I should process this query so I can do the Sum when tripDone <> 1 only once for each tripId ?
Thanks a lot for the help !
If you need some more infos I'm available, and sorry for my lack of english skills !
It sounds like you want to do the same count(distinct ...) pattern for the columns you're currently summing, but with some logic. You can use case within a count instead in the same way:
...
COUNT(
DISTINCT CASE
WHEN t.tripDone <> 1
THEN t.idTrip
ELSE null
END),
COUNT(
DISTINCT CASE
WHEN t.codeIncident = 'CANCEL'
THEN t.idTrip
ELSE null
END)
The else null is a bit redundant as that's the default. As count() ignores nulls, if the when isn't matched then that trip ID isn't counted.
First Select idTrip field in your inner query that means table "t"
I need to extract some data to analyse exceptions/logs, and I'm stuck at a point.
I have a table with a column called CallType, and a status which can be Success or Failure. This table also has a column called SessionId.
I need to do this:
Select all the SessionId's where all the CallType = 'A' are marked as Success, but there is at least one CallType = 'B' having a Failure for that session.
There will be a where clause to filter out some stuff.
I'm thinking something like:
select top 10 *
from Log nolock
where ProviderId=48 -- add more conditions here
group by SessionId
having --? what should go over here?
I would do this with conditional aggregation in the having clause:
select top 10 *
from Log nolock
where ProviderId=48 -- add more conditions here
group by SessionId
having sum(case when CallType = 'A' and Status = 'Failure' then 1 else 0 end) = 0 and
sum(case when CallType = 'B' and Status = 'Failure' then 1 else 0 end) > 0 and
sum(case when CallType = 'A' and Status = 'Success' then 1 else 0 end) > 0;
The having clause checks for three conditions by counting the number of rows that meet each one. If = 0, then no records are allowed. If > 0 then records are required.
That CallType A has no failures.
That CallType B has at least one failure.
That at least one CallType A success exists.
The third condition is ambiguous -- if is not clear if you actually need CallType As to be in the data, based on the question.
SELECT *
FROM Log L WITH(NOLOCK)
WHERE L.CallType='A'
AND L.[Status] = 'Success'
AND L.ProviderId = 48
AND EXISTS (SELECT 1
FROM Log
WHERE L.SessionID = SessionID
AND CallType='B'
AND [Status] = 'Failure')
Having clause can only operate on aggregates within the group so this isn't the correct way to go about it since you are filtering out other rows you want to check against. I'd use EXISTS for this e.g.
edit: corrected the query
SELECT *
FROM Log L WITH(NOLOCK)
WHERE ProviderId = 48
AND CallType = 'A'
AND Status = 'Success'
AND EXISTS(SELECT * FROM Log WHERE L.SessionId = SessionId AND CallType = 'B' AND Status = 'Failure')
You can essentially filter out rows in the EXISTS part of the query using the aliased Log table (aliased L), matching all rows with the same session ID and seeing if any match the filters you required (failed with call type B)
Are there way/s for me to extract data that only contain a certain values.
Ex:
Contact Asset Status
AB 1 Cancelled
AB 2 Cancelled
AB 3 Cancelled
AB 4 Cancelled
CD 5 Cancelled
CD 6 Active
CD 7 Cancelled
CD 8 Active
What I want to get are only those contacts that does contain cancelled assets ONLY (like Contact AB). And not those with both cancelled and active assets (like Contact CD).
You can do this with group by and a having clause:
select contact
from table t
group by contact
having min(status) = 'Cancelled' and max(status) = 'Cancelled';
This works for the data in your example. If status could be NULL and you want to count that as a different value, then the logic would be slightly more complicated.
The pure relational logic way is easier to understand, but will perform less well, requiring some kind of join to work. Let's satisfy the conditions
There is at least one status Cancelled for each Contact
But there are 0 statuses for that same Contact that aren't Cancelled
in a query like so:
SELECT DISTINCT
CS.Contact
FROM
ContactStatus CS
WHERE
CS.Status = 'Cancelled' -- at least one cancelled
AND NOT EXISTS ( -- but there are none of...
SELECT *
FROM ContactStatus CS2 -- contacts in the same table
WHERE
CS.Contact = CS2.Contact -- for that same contact
AND CS.Status <> 'Cancelled' -- that aren't cancelled
)
;
But we can do this with an aggregate, that will take only a single scan of the table, by using a little thought:
SELECT
Contact
FROM
ContactStatus
GROUP BY
Contact
HAVING
Count(*) = Count(CASE WHEN Status = 'Cancelled' THEN 1 END)
;
Other aggregate expressions in the HAVING clause are possible, such as:
Count(CASE WHEN Status <> 'Cancelled' THEN 1 END) = 0 -- or Min()
Min(Status) = 'Cancelled' AND Max(Status) = 'Cancelled'
Max(CASE WHEN Status = 'Cancelled' THEN 0 ELSE 1 END) = 0
Sum(SELECT 1 WHERE Status <> 'Cancelled') = 0
All of these would do the trick in this case; pick the one that makes the most sense to you.
Select contact from contacts a
where a.status = 'Canceled' AND contact = 'AB'