SQL Server : zero values with COUNT(*) - sql

I have a table #MemberAttribute
MemberID AttributeID AttributeValue
1 1 False
1 2 True
2 1 False
2 2 True
3 1 False
3 2 False
I want to group by attributeID and get count of attributes whose value is True. But when attributetype is false for a particular attribute there I want it to display 0. Right now the attributeID with all false vallues just doesn't show up.
Here is the sql query
SELECT MA.AttributeID, GA.Name,
--COUNT(isNull(MA.AttributeID,0)) as AttributCount,
CASE WHEN COUNT(MA.AttributeID) > 0 THEN COUNT(MA.AttributeID) Else 0 END AS 'AttributCount'
--CASE WHEN COUNT(MA.AttributeID) < 0 THEN 0 Else COUNT(MA.AttributeID) END AS 'TOTAL Attributes'
from GroupAttribute GA
inner join #MemberAttribute MA on GA.GroupAttributeID = MA.AttributeID
WHERE MA.AttributeValue = 'True'
GROUP BY MA.AttributeID,GA.Name
FOR AttributeID = 1 all the values are = False... so the result is like this
AttributeID Name AttributeCount <br/>
2 Attr2 2 <br/>
I want
1 Attr1 0 <br/>
too in the result set.

Try this - note that the 1 in ...THEN 1 ELSE ... is an arbitrary non-NULL value - it could be 'fred' or 12345 - that it's not NULL is the important part.
SELECT MA.AttributeID, GA.Name,
COUNT(CASE WHEN MA.AttributeValue = 'True' THEN 1 ELSE NULL END) AS 'AttributeCount'
from GroupAttribute GA
inner join #MemberAttribute MA on GA.GroupAttributeID = MA.AttributeID
GROUP BY MA.AttributeID,GA.Name
...somewhat more intuitively (thanks Ken) - and note that here the 1 and 0 are important...
SELECT MA.AttributeID, GA.Name,
SUM(CASE WHEN MA.AttributeValue = 'True' THEN 1 ELSE 0 END) AS 'AttributeCount'
from GroupAttribute GA
inner join #MemberAttribute MA on GA.GroupAttributeID = MA.AttributeID
GROUP BY MA.AttributeID,GA.Name

After some wriggling, I came up with this beauty containing no CASE expression:
SELECT GA.GroupAttributeID AS AttributeID, GA.Name,
COUNT(MA.AttributeID) AS AttributeCount
FROM GroupAttribute AS GA
LEFT OUTER JOIN #MemberAttribute AS MA
ON GA.GroupAttributeID = MA.AttributeID AND MA.AttributeValue = 'True'
GROUP BY GA.GroupAttributeID, GA.Name
This takes advantage of the fact that if there are no 'True' values for a particular AttributeID, the MA.AttributeID resulting from the LEFT OUTER JOIN will be NULL. The NULL value passed into COUNT() will lead to an AttributeCount of zero. The LEFT OUTER JOIN also ensures that a row will be present in the result set for AttributeID rows with zero counts.
The assumption with this query is that all group attributes are represented in the #MemberAttribute table variable. If not, there will be rows with zero counts representing those group attributes that are absent. If this is undesirable, a WHERE clause can be added to filter them out, complicating this query. Will's solution(s) would be far more practical if this is the case.
The execution plan compares well with Will's first solution, containing one less (Compute Scalar) step. It does use a LEFT OUTER JOIN vs an INNER JOIN, however, making the two methods practically identical for this simple example. It would be interesting to see how the two solutions scale if table variable is converted to a fairly large table, instead.
Will's actual plan for his solution involving COUNT():
My actual plan:

Related

SQL COUNT return multiple rows

I have those two Tables:
tblCommentReactions
id
Liked
CommentID
1
0
1
2
1
1
3
1
1
4
0
2
1 is Like and 0 is dislike.
tblComments:
id
userID
message
1
1
message 1
2
1
message 2
3
2
message 1
I tried to select all comments and Count the dislikes and likes and give the result in the same Row.
SELECT c.ID as CommentID, c.message,
COUNT(case when Liked = 1 AND r.CommentID = c.ID then 1 else null end) as likes,
COUNT(case when Liked = 0 AND r.CommentID = c.ID then 1 else null end) as dislikes
FROM tblcomments as c LEFT JOIN tblcommentreactions as r ON c.ID = r.CommentID
WHERE c.userID = 1;
Expected Output should be:
CommentID
message
likes
dislikes
1
message 1
2
1
2
message 2
0
1
On my Return it counts everything and only returns the first message. Could you tell me what i need to change in my request, to get my expected output?
There are two issues in your query:
you have no GROUP BY clause in presence of non-aggregated fields inside the SELECT clause, which will bring you have an error fired by the DBMS in the best case scenario, no error but random/subtle semantic errors in the worst one.
you are attempting to filter your rows (the WHERE condition) before the aggregation is applied.
In order to solve:
the first problem, you need to add the GROUP BY clause with the two missing selected fields, namely "c.ID" and "c.message"
the second problem, you need to transform your current WHERE clause into an HAVING one (as long as this one applies after the aggregation has been carried out) and add the checked field, namely "c.userID", inside the GROUP BY clause, as long as it is a field that was selected along with the fields in the SELECT clause.
SELECT c.ID as CommentID,
c.message,
COUNT(CASE WHEN Liked = 1 THEN 1 END) AS likes,
COUNT(CASE WHEN Liked = 0 THEN 1 END) AS dislikes
FROM tblComments AS c
LEFT JOIN tblCommentReactions AS r
ON c.ID = r.CommentID
GROUP BY c.ID,
c.message,
c.userID
HAVING c.userID = 1
Minor fixes on the CASE construct that doesn't require "AND r.CommentID = c.ID" as already pointed in the comments section, but also the non-required "ELSE NULL" condition, that is considered by PostgreSQL as default for this construct.
Here's a demo in MySQL, though this should work in the most common DBMS' more or less.
Try using group by clause e.g
select cr.CommentID ,c.message,
COUNT(case when Liked = 1 AND cr.CommentID = c.ID then 1 else null end) as likes ,
COUNT(case when Liked = 0 AND cr.CommentID = c.ID then 1 else null end) as dislikes
from [tblCommentReactions] cr inner join tblComments c on cr.CommentID = c.id
group by cr.CommentID , c.message
SELECT c.Id, COUNT(cr1.Liked), COUNT(cr2.Liked)
FROM Comments c
LEFT JOIN CommentReactions cr1 ON cr1.CommentId = c.Id AND cr1.Liked = 0
LEFT JOIN CommentReactions cr2 ON cr2.CommentId = c.Id AND cr2.Liked = 1
GROUP BY c.Id

Tricky (MS)SQL query with aggregated functions

I have these three tables:
table_things: [id]
table_location: [id]
[location]
[quantity]
table_reservation: [id]
[quantity]
[location]
[list_id]
Example data:
table_things:
id
1
2
3
table_location
id location quantity
1 100 10
1 101 4
2 100 1
table_reservation
id quantity location list_id
1 2 100 500
1 1 100 0
2 1 100 0
They are connected by [id] being the same in all three tables and [location] being the same in table_loation and table_reservation.
[quantity] in table_location shows how many ([quantity]) things ([id]) are in a certain place ([location]).
[quantity] in table_reservation shows how many ([quantity]) things ([id]) are reserved in a certain place ([location]).
There can be 0 or many rows in table_reservation that correspond to table_location.id = table_reservation_id, so I probably need to use an outer join for that.
I want to create a query that answers the question: How many things ([id]) are in this specific place (WHERE table_location=123), how many of of those things are reserved (table_reservation.[quantity]) and how many of those that are reserved are on a table_reservation.list_id where table_reservation.list_id > 0.
I can't get the aggregate functions right to where the answer contains only the number of lines that are in table_location with the given WHERE clause and at the same time I get the correct number of table_reservation.quantity.
If I do this I get the correct number of lines in the answer:
SELECT table_things.[id],
table_location.[quantity],
SUM(table_reservation.[quantity]
FROM table_location
INNER JOIN table_things ON table_location.[id] = table_things.[id]
RIGHT OUTER JOIN table_reservation ON table_things.location = table_reservation.location
WHERE table_location.location = 100
GROUP BY table_things.[id], table_location[quantity]
But the problem with that query is that I (of course) get an incorrect value for SUM(table_reservation.[quantity]) since it sums up all the corresponding rows in table_reservation and posts the same value on each of the rows in the result.
The second part is trying to get the correct value for the number of table_reservation.[quantity] whose list_id > 0. I tried something like this for that, in the SELECT list:
(SELECT SUM(CASE WHEN table_reservation.list_id > 0 THEN table_reservation.[quantity] ELSE 0 END)) AS test
But that doesn't even parse... I'm just showing it to show my thinking.
Probably an easy SQL problem, but it's been too long since I was doing these kinds of complicated queries.
For your first two questions:
How many things ([id]) are in this specific place (WHERE table_location=123), how many of of those things are reserved (table_reservation.[quantity])
I think you simply need a LEFT OUTER JOIN instead of RIGHT, and an additional join predicate for table_reservation
SELECT l.id,
l.quantity,
Reserved = SUM(ISNULL(r.quantity, 0))
FROM table_location AS l
INNER JOIN table_things AS t
ON t.id = l.ID
LEFT JOIN table_reservation r
ON r.id = t.id
AND r.location = l.location
WHERE l.location = 100
GROUP BY l.id, l.quantity;
N.B I have added ISNULL so that when nothing is reserved you get a result of 0 rather than NULL. You also don't actually need to reference table_things at all, but I am guessing this is a simplified example and you may need other fields from there so have left it in. I have also used aliases to make the query (in my opinion) easier to read.
For your 3rd question:
and how many of those that are reserved are on a table_reservation.list_id where table_reservation.list_id > 0.
Then you can use a conditional aggregate (CASE expression inside your SUM):
SELECT l.id,
l.quantity,
Reserved = SUM(r.quantity),
ReservedWithListOver0 = SUM(CASE WHEN r.list_id > 0 THEN r.[quantity] ELSE 0 END)
FROM table_location AS l
INNER JOIN table_things AS t
ON t.id = l.ID
LEFT JOIN table_reservation r
ON r.id = t.id
AND r.location = l.location
WHERE l.location = 100
GROUP BY l.id, l.quantity;
As a couple of side notes, unless you are doing it for the right reasons (so that different tables are queried depending on who is executing the query), then it is a good idea to always use the schema prefix, i.e. dbo.table_reservation rather than just table_reservation. It is also quite antiquated to prefix your object names with the object type (i.e. dbo.table_things rather than just dbo.things). It is somewhat subject, but this page gives a good example of why it might not be the best idea.
You can use a query like the following:
SELECT tt.[id],
tl.[quantity],
tr.[total_quantity],
tr.[partial_quantity]
FROM table_location AS tl
INNER JOIN table_things AS tt ON tl.[id] = tt.[id]
LEFT JOIN (
SELECT id, location,
SUM(quantity) AS total_quantity,
SUM(CASE WHEN list_id > 0 THEN quantity ELSE 0 END) AS partial_quantity
FROM table_reservation
GROUP BY id, location
) AS tr ON tl.id = tr.id AND tl.location = tr.location
WHERE tl.location = 100
The trick here is to do a LEFT JOIN to an already aggregated version of table table_reservation, so that you get one row per id, location. The derived table uses conditional aggregation to calculate field partial_quantity that contains the quantity where list_id > 0.
Output:
id quantity total_quantity partial_quantity
-----------------------------------------------
1 10 3 2
2 1 1 0
This was a classic case of sitting with a problem for a few hours and getting nowhere and then when you post to stackoverflow, you suddenly come up with the answer. Here's the query that gets me what I want:
SELECT table_things.[id],
table_location.[quantity],
SUM(table_reservation.[quantity],
(SELECT SUM(CASE WHEN table_reservation.list_id > 0 THEN ISNULL(table_reservation.[quantity], 0) ELSE 0 END)) AS test
FROM table_location
INNER JOIN table_things ON table_location.[id] = table_things.[id]
RIGHT OUTER JOIN table_reservation ON table_things.location = table_reservation.location AND table_things.[id] = table_reservation.[id]
WHERE table_location.location = 100
GROUP BY table_things.[id], table_location[quantity]
Edit: After having read GarethD's reply below, I did the changes he suggested (to my real code, not to the query above) which makes the (real) query correct.

SQL Server Return Null if exists

In SQL Server 2008 I am looking to create a query that will return a NULL in an aggregate if one exists, otherwise I'm looking for the maximum. This is a simplified example...
I have the following data:
CO Loc Term_Dt
1 A 7/15/2013
1 B
1 C 10/30/2000
2 A 8/10/2008
2 B 6/1/2015
2 C 4/30/2010
The result I'm looking for is:
CO Term_Dt
1 NULL
2 6/1/2015
because technically the Company is still open if at least one location has not yet been terminated.
Thanks
Just use aggregation and a case statement:
select co,
(case when count(term_dt) = count(*) then max(term_dt)
end) as term_dt
from table t
group by co;
count(<column>) counts the number of non-NULL values. If this doesn't match all the rows, then at least one is NULL. No else is needed for the case, because the default is NULL.
Generate a sub set of data with companies having null term dates and left join your super set to it. Any records in 2nd table which are not null you want to display as null so use a case statement.
This works because our outer table (A) returns
CO TERM_DT
1 7/15/2013
2 6/1/2015
But then our LEFT join on our inline view also adds B.Co...
CO TERM_DT B.CO
1 7/15/2013 1
2 6/1/2015 NULL
So you can see by saying we want to display NULL when B.CO is not null instead of the max(TERM_DT) will yield the desired results. This is accomplished using a case statement.
SELECT A.Co,
Case when B.CO is not null then Max(A.Term_DT) else NULL end as Term_DT
FROM tableName A
LEFT JOIN (SELECT Distinct CO from tableName where Term_dt is null) B
on A.Co = B.CO
GROUP BY CO

Link tables based on column value

Is it possible to pull values from 2 different tables based on the value of a column? For example, I have a table with a boolean column that either returns 0 or 1 depending on what the end user selects in our program. 0 means that I should pull in the default values. 1 means to use the user's data.
If my table Table1 looked like this:
Case ID Boolean
====================
1 0
2 1
3 1
4 0
5 0
Then I would need to pull Case IDs 1,4,and 5's corresponding data from table Default and Case IDs 3 and 4's corresponding data from table UserDef. Then I would have to take these values, combine them, and reorder them by Case ID so I can preserve the order in the resulting table.
I am fairly inexperienced with SQL but I am trying to learn. Any help or suggestions are greatly appreciated. Thank you in advance for your help.
Something like this:
SELECT
t1.CaseID
,CASE WHEN t1.Boolean = 1 THEN dt.Col1 ELSE ut.Col1 END AS Col1
,CASE WHEN t1.Boolean = 1 THEN dt.Col2 ELSE ut.Col2 END AS Col2
FROM Table1 t1
LEFT JOIN DefaultTable dt ON dt.CaseID = t1.CaseID
LEFT JOIN UserDefTable ut ON ut.CaseID = t1.CaseID
ORDER BY t1.CaseID
You join on both tables and then use CASE in SELECT to choose from which one to display data.
Option B:
WITH CTE_Combo AS
(
SELECT 0 as Boolean, * FROM Default --replace * with needed columns
UNION ALL
SELECT 1 AS Boolean, * FROM UserDef --replace * with needed columns
)
SELECT * FROM Table1 t
LEFT JOIN CTE_Combo c ON t.CaseID = c.CaseID AND t.Boolean = c.Boolean
ORDER BY t.CaseID
This might be even simpler - using CTE make a union of both tables adding artificial column, and then join CTE and your Table using both ID and flag column.
SELECT t1.CaseID,
ISNULL(td.data, tu.data) userData -- pick data from table_default
-- if not null else from table_user
FROM table1 t1
LEFT JOIN table_default td ON t1.CaseID = td.CaseID -- left join with table_default
AND t1.Boolean = 0 -- when boolean = 0
LEFT JOIN table_user tu ON t1.CaseID = tu.CaseID -- left join with table_user
AND t1.Boolean = 1 -- when boolean = 1
ORDER BY t1.CaseID

SQL join to table with 3 possible cases: table can have no records, match 1 or more, records or require all records found to match

I have a 2 tables:
Questions table with Question ID
Part Table:
Question ID
Part ID
BAllPartsRequired
The user will select some parts (or may select none) and depending on what was selected certain questions will be displayed.
I want to join the 2 tables for 3 scenerios but do them all in 1 query. I can write each scenerio individually (EDIT I thought I could but scenario 3 I can not get to work where it requires all found in part table to be selected) but can not figure out how to get them all in 1 query (I have done something similar before but cant remember how).
If no parts exist in part table for that question retruen the question
If any part selected exists in part table return question (i.e. user selects 1 part and 5 parts are associated to that question then the question will match and be returned). BAllPartsRequired = false
If user selects parts and ALL of the parts are associated to the question the question is returend but if NOT all parts are selected by user the question is not returend (i.e. user selects 3 parts and there are 4 parts in table then the user will not see the question, but if the user selectes all 4 parts the question will be shown). BAllPartsRequired = true
I am an advanced SQL programmer but this is eluding me and I know I have done this before but how do I get it to work in 1 query, do I do a nested join, a left join, a case on the where statement or something else.
Sample Data:
Question Form Association:
NFormAssociationID NQuestionID FormType
1 1 PAEdit
2 2 PAEdit
3 3 PAEdit
4 4 PAEdit
Question Part Form Association Table:
ID NFormAssociationID PartNumber BAllPartsRequired
1 1 1 0
2 2 2 1
3 2 3 1
Query without the new parts table added:
Select ROW_NUMBER() OVER(ORDER BY QL.NOrderBy) AS RowNumber,
QL.NQuestionID, QL.FieldName, QL.Question, QL.BRequired, QFL.FormFieldType, QFL.SingleMultipleSM,
QFL.CSSStyle
FROM dbo.QuestionFormAssociation QA WITH (NOLOCK)
INNER JOIN dbo.QuestionLookup QL WITH (NOLOCK) ON QA.NQuestionID = QL.NQuestionID
INNER JOIN dbo.QuestionFieldTypeLookup QFL WITH (NOLOCK) ON QL.NFieldTypeID = QFL.NFieldTypeID
WHERE QA.BActive = 1 AND QL.BActive = 1 AND QFL.BActive=1
AND QA.FormType = 'PAEdit'
ORDER BY QL.NOrderBy
Simple query using new table
Select ID
FROM dbo.QuestionPartFormAssociation
WHERE BAllPartsRequired = 1
AND PartNumber IN ('1', '2') --'1', '2', '3')
It sounds like you are trying to find the eligible questions, based on some criteria.
In this sitatuion, it is best to summarize first at the question level, and then apply logic to those summaries. Here is an example:
select q.questionid
from (select q.questionid,
max(case when qp.questionid is null then 1 else 0 end) as HasNoParts,
sum(case when qp.partid in (<user parts>) then 1 else 0 end) as NumUserParts,
count(qp.questionid) as NumParts,
max(qp.AllPartsRequired) as AreAllPartsRequired
from question q left outer join
questionpart qp
on q.questionid = qp.questionid
group by q.questionid
) q
where HasNoParts = 1 or -- condition 1
AreAllPartsRequired = 0 and NumUserParts > 0 or -- condition 2
AreAllPartsRequired = 1 and NmUserParts = NumParts -- condition 3
I've simplified the table and column names to make the logic clearer.
updated with full answer from OP:
Select ROW_NUMBER() OVER(ORDER BY QL.NOrderBy) AS RowNumber,
QL.NQuestionID, QL.FieldName, QL.Question, QL.BRequired, QFL.FormFieldType, QFL.SingleMultipleSM,
QFL.CSSStyle
FROM dbo.QuestionFormAssociation QA WITH (NOLOCK)
INNER JOIN dbo.QuestionLookup QL WITH (NOLOCK) ON QA.NQuestionID = QL.NQuestionID
INNER JOIN dbo.QuestionFieldTypeLookup QFL WITH (NOLOCK) ON QL.NFieldTypeID = QFL.NFieldTypeID
INNER JOIN (
select q.NFormAssociationID,
max(case when qp.NFormAssociationID is null then 1 else 0 end) as HasNoParts,
sum(case when qp.PartNumber in ('1','2','3') then 1 else 0 end) as NumUserParts,
count(qp.NFormAssociationID) as NumParts,
qp.BAllPartsRequired
from QuestionFormAssociation q
left outer join QuestionPartFormAssociation qp on q.NFormAssociationID = qp.NFormAssociationID
AND QP.BActive = 1
WHERE Q.FormType = 'PAEdit'
AND Q.BActive = 1
group by q.NFormAssociationID, qp.BAllPartsRequired
) QSUB ON QA.NFormAssociationID = QSUB.NFormAssociationID
WHERE QA.BActive = 1 AND QL.BActive = 1 AND QFL.BActive=1
AND (
QSUB.HasNoParts = 1 -- condition 1
OR (QSUB.BAllPartsRequired = 0 and QSUB.NumUserParts > 0) -- condition 2
OR (QSUB.BAllPartsRequired = 1 and QSUB.NumUserParts = QSUB.NumParts) -- condition 3
)
ORDER BY QL.NOrderBy