MySQL subtract two count columns - mysql

I have a table like this:
client msg_type msg_body id
------ -------- -------- ---
123 typeA success abc
123 typeB success abc
456 typeA success abc
456 typeB failure abc
123 typeA success abc
123 typeA success abc
789 typeA success def
789 typeB success def
etc.
I would like output like this:
client diff id
------ ---- ---
123 2 abc
456 1 abc
789 0 def
where diff is the count of typeA:success messages - typeB:success messages. I can get the count of the typeA success using something like:
select client, count(*) from mytable
where msg_type="typeA" and msg_body="success"
However, I can't figure out how to put another count in there (for typeB) and also subtract.
I tried something like:
select client, count(*) from mytable
where msg_type="typeA" and msg_body="success" - count(*)
from mytable where msg_type="typeB" and msg_body="success"
But of course it didn't work, or I wouldn't be asking here. :) Any advice?
Edit: added another column. I tried the two suggestions given, but it only seems to return the results for one of the ids, not both.
Edit #2: I tried wrapping the SELECT query with:
select id, count(*) from (select ...) as anothertable where count_a_minus_count_b = 0;
I was hoping the output would be like:
id count
--- -----
abc 2
def 1
where count is the number of clients where the difference between typeA:success and typeB:success is 0.

COUNT counts non-null values, so you can construct an expression that's non-null when msg_type = 'typeA', and an expression that's non-null when msg_type = 'typeB'. For example:
SELECT client,
COUNT(CASE WHEN msg_type = 'typeA' THEN 1 END) AS count_a,
COUNT(CASE WHEN msg_type = 'typeB' THEN 1 END) AS count_b,
COUNT(CASE WHEN msg_type = 'typeA' THEN 1 END)
- COUNT(CASE WHEN msg_type = 'typeB' THEN 1 END) AS count_a_minus_count_b
FROM mytable
WHERE msg_body = 'success'
GROUP
BY client
;
(Disclaimer: not tested.)

Another way:
SELECT
d.client, COALESCE(a.cnt, 0) - COALESCE(b.cnt, 0) AS diff, d.id
FROM
( SELECT DISTINCT client, id
FROM mytable
) AS d
LEFT JOIN
( SELECT client, COUNT(*) AS cnt, id
FROM mytable
WHERE msg_type = 'typeA'
AND msg_body = 'success'
GROUP BY client, id
) AS a
ON d.client = a.client
AND d.id = a.id
LEFT JOIN
( SELECT client, COUNT(*) AS cnt, id
FROM mytable
WHERE msg_type = 'typeB'
AND msg_body = 'success'
GROUP BY client, id
) AS b
ON d.client = b.client
AND d.id = b.id ;
Tested at SQL-Fiddle

Here you go:
select client,
(sum(case when msg_type='typeA' and msg_body='success' then 1 else 0 end) -
sum(case when msg_type='typeB' and msg_body='success' then 1 else 0 end)) as diff
from your_table
group by client

Here's one way to get the result:
SELECT t.client
, SUM(t.msg_type<=>'typeA' AND t.msg_body<=>'success')
- SUM(t.msg_type<=>'typeB' AND t.msg_body<=>'success') AS diff
FROM mytable t
GROUP BY t.client
(The expressions in this query are MySQL specific; for a more portable query, use a less concise CASE expression to obtain an equivalent result.)
As more terse and obfuscated alternative to return the same result:
SELECT t.client
, SUM((t.msg_body<=>'success')*((t.msg_type<=>'typeA')+(t.msg_type<=>'typeB')*-1)) AS diff
FROM mytable t
GROUP BY t.client

Related

need to merge rows into two colums mysql

please advice how to make SQL query in order to get from this table
ID|Number|Type|
----------------
1 |AA1 |IN |
2 |AA2 |OUT |
3 |AA3 |IN |
4 |AA4 |OUT |
into this result
ID| IN | OUT |
-------------------
1 | AA1 | AA2 |
2 | AA3 | AA4 |
Thanks
This Will work using Implicit join.
It will use mysql session variables. for reference, you can read http://www.mysqltutorial.org/mysql-variables/ for session variables.
SET #row_number = 0;
SET #row_number2 = 0;
SELECT
out_table.OUTs AS outs, in_table.Ins as INs FROM
(SELECT
(#row_number2:=#row_number2 + 1) AS num2, Number as OUTs FROM your_table WHERE your_table.Type = 'OUT') as out_table ,
(SELECT
(#row_number:=#row_number + 1) AS num1, Number as Ins FROM your_table WHERE your_table.Type = 'IN') as in_table
WHERE num2 = num1
You can emulate row_number like functionality, using session variables. We get all INs and OUTs separately in two derived tables and do a LEFT JOIN on them, to get the desired output.
This will work even for the cases where IN and OUT are not consecutive. It will also handle the cases where there is an IN without OUT.
It would not work for the case when there is an OUT without IN.
Try the following query:
SET #row_no_1 = 0;
SET #row_no_2 = 0;
SELECT
t1.row_no AS ID, t1.Number AS `IN`, t2.Number AS `OUT`
FROM
(SELECT
#row_no_1:=#row_no_1 + 1 AS row_no, Number
FROM
`your_table`
WHERE
Type = 'IN'
ORDER BY id ASC) AS t1
LEFT JOIN
(SELECT
#row_no_2:=#row_no_2 + 1 AS row_no, Number
FROM
`your_table`
WHERE
Type = 'OUT'
ORDER BY id ASC) AS t2 ON t2.row_no = t1.row_no
answering myself...
SELECT a.ID
MAX(CASE WHEN a.type = "IN" THEN a.Number ELSE "" END) AS IN_Type,
MAX(CASE WHEN b.type = "IN" THEN b.Number ELSE "" END) AS Out_Type
FROM table1 a Left join table1 b on a.ID = b.ID
Group by a.ID

MySQL Query: 3 tests passed, 1 failed

I'm trying to write a query that will pull students that have passed tests 1 thru 3 AND failed test 4.
Students can retake tests so there may be failed records, followed by passed records for some tests, such is the case with student_id = 2 below.
Table setup like this -
test_id | student_id | status | completed_on
--------+------------+---------+------------
1 | 1 | passed | 2018-03-24
2 | 1 | passed | 2018-03-25
3 | 1 | passed | 2018-03-26
4 | 1 | failed | 2018-03-27
1 | 2 | failed | 2018-03-24
1 | 2 | passed | 2018-03-25
2 | 2 | passed | 2018-03-26
3 | 2 | passed | 2018-03-27
4 | 2 | failed | 2018-03-27
In this case the query should pull both student_id 1 and 2
I tried this but it obviously didn't work -
select *
from table
where (test_id = 1 and status = 'passed')
and (test_id = 2 and status = 'passed')
and (test_id = 3 and status = 'passed')
and (test_id = 4 and status = 'failed')
Demo
SELECT count(Z.Test_ID), Z.student_ID
FROM (SELECT distinct student_ID, test_ID, Status
FROM table) Z
WHERE (Z.Status = 'Passed' and Z.test_ID in (1,2,3,4))
OR (Z.status = 'Failed' and Z.test_ID = 4)
GROUP BY Z.Student_ID
HAVING count(Z.Test_ID) = 4;
This works by first ensuring we only have distinct records for each student, status, and test_ID. (derived table Z)
We then evaluate how many passes in tests 1,2,3,4 exist and existence of fails we have for test 4. if the count is anything other than 4 then we know either they didn't pass the tests 1-3 and fail 4 or they've passed test 4 as well.
I'm not claiming this is fast or most efficient, but it will do the job. Make sure you have the right indices on your table,
SELECT s1.student_id
FROM mytable s1
JOIN mytable s2 on s1.student_id=s2.student_id and s2.test_id=2 and s2.status='passed'
JOIN mytable s3 on s1.student_id=s3.student_id and s3.test_id=3 and s3.status='passed'
WHERE s1.test_id=1
AND s1.status='passed'
AND NOT EXISTS (
SELECT 1
FROM mytable s4
WHERE s4.student_id=s1.student_id
AND s4.test_id=4
AND s4.status='passed'
)
Another approach:
select distinct t1.student_id
from mytable t1
inner join
-- students passed all the 3 tests
(select student_id from mytable where test_id in (1, 2, 3)
and status = 'passed' group by student_id having count(distinct test_id) = 3 ) t2
on t1.test_id = 4 and t1.status = 'failed' and t1.student_id = t2.student_id
where not exists
(select 1 from mytable where student_id = t1.student_id and
status = 'passed' and test_id = 4)
PS. If a student passed a test (e.g. test 1) but later took it again and failed, the student would be considered as passed. Not sure if that is acceptable.
One approach you can use to meet your requirement is to basically make a pivot table. Any way you slice it you will probable need to use a subquery, here using the SUM function and CASE statements you can determine how many times each student passed a test.
Then, in your outer WHERE clause you can select only the rows where you got 1 or more 'passed' results for tests 1 through 3, and 0 'passed' results for test 4.
SQL Fiddle
SELECT student_id
FROM (SELECT student_id,
SUM(
CASE WHEN(test_id = 1 AND result = 'passed')
THEN 1
ELSE 0
END) AS "Test1",
SUM(
CASE WHEN(test_id = 2 AND result = 'passed')
THEN 1
ELSE 0
END) AS "Test2",
SUM(
CASE WHEN(test_id = 3 AND result = 'passed')
THEN 1
ELSE 0
END) AS "Test3",
SUM(
CASE WHEN(test_id = 4 AND result = 'passed')
THEN 1
ELSE 0
END) AS "Test4"
FROM TestResults
GROUP BY student_id) tr
WHERE Test1 > 0 AND Test2 > 0 AND Test3 > 0 AND Test4 = 0
Using this technique you can also determine how many times a student passed or failed a test. So for example, you can change the CASE statements to 'failed' and return the test columns in the outer query to see the number of times a student failed the test. For example:
SELECT student_id, Test1Fails
FROM (SELECT student_id,
SUM(
CASE WHEN(test_id = 1 AND result = 'failed')
THEN 1
ELSE 0
END) AS "Test1Fails"
FROM TestResults
GROUP BY student_id) tr

Show count 0 when condition is not met in MySQL group by

Sample Records:
examid setcode answer
------- -------- --------
10 A A
10 A B
10 A X
10 B A
10 B B
10 B C
I am trying to find count group by setcode where answer is X.
I have tried the following query:
SELECT setCode,COUNT(answer) FROM mcq_answer WHERE examid=10 AND answer='X' GROUP BY setCode
This one is returning the following result:
setcode count
------- --------
A 1
But I am looking for the following:
setcode count
------- --------
A 1
B 0
Setcode is dynamic here. I have mentioned A and B only. There may be more setocdes as C,D,E,F etc. How can I do it. I am using MySQL
you can use below query
SELECT C.setCode ,
SUM(CASE WHEN B.setcode IS NULL THEN 0
ELSE 1
END) AS answer
FROM ( SELECT A.setCode ,
COUNT(1) AS cnt
FROM mcq_answer A
GROUP BY A.setCode
) C
LEFT JOIN mcq_answer b ON C.setcode = B.setcode
AND B.answer = 'X'
GROUP BY C.setCode
You can use SUM + IF to have this result:
SELECT `setCode`, SUM(if (`answer`='X', 1,0)) as answer
FROM `mcq_answer`
WHERE examid=10
GROUP BY `setcode`
Try:
SELECT A.setcode,
COUNT(CASE WHEN B.answer ='X' THEN B.answer ELSE NULL END) as count_x
FROM
(SELECT DISTINCT setcode FROM YOUR_TABLE) A
LEFT JOIN
YOUR_TABLE B
ON A.setcode = B.setcode
GROUP BY A.setcode;
It also works for dynamic set of values for setcode.

Return Yes if only one record is found

I have a table that has this structure:
table
id | site_id
-------------------
240 | 1
240 | 2
240 | 3
320 | 1
320 | 2
421 | 1
520 | 2
-------------------
300k records
Now i am trying to write a query to return a yes or a no for each record (id).
For example if the records with id 240 only have a site_id 1 then return 'Yes', if it has 2, 3 and so on return 'No'
I am not sure how to approach it but here is a result sample:
result_table
.-----------------------.
| id | result |
|-----------------------|
| 240 | No | -- has a site_id 1, 2 and 3
| 320 | No | -- has a site_id 1 and 2
| 421 | Yes | -- has a site_id 1 only
| 520 | No | -- has a site_id 2
'-----------------------'
Here is the query i have so far, but it seems to be incorrect
SELECT CASE WHEN count(id) > 1 THEN 'N' ELSE 'Y' END as Result
FROM table sm
WHERE sm.site_id IN (1)
AND sm.site_id NOT IN (2,3,4,5,6,7,8,9)
AND id = 240
UPDATE
SO Here is my full query, i added the answer from #gordon
SELECT
m.merchant_name,
m.merchant_id,
ut.realname,
a.affiliate_name,
(select (case when count(site_id) = 1 then 'Yes' else 'No' end) as result
from site_merchant sm
WHERE sm.merchant_id = m.merchant_id
group by merchant_id) as isTjoos, -- y or no
(select (case when count(site_id) = 2 then 'Yes' else 'No' end) as result
from site_merchant sm
WHERE sm.merchant_id = m.merchant_id
group by merchant_id) as isUCOnly
-- isdlkonly -- y or no
FROM merchant m
LEFT JOIN merchant_editor_assignment mea ON mea.merchant_id = m.merchant_id
LEFT JOIN user_table ut ON ut.user_id = mea.user_id
LEFT JOIN affiliate a ON a.affiliate_id = m.affiliate_id_default
I interpreted this question as you want ids that have only one value for site_id. I took the example in the question to be an example, with site_id = 1. To do this:
You want to use count(distinct):
select id, (case when count(distinct site_id) = 1 then 'Yes' else 'No' end) as result
from site_merchant sm
group by id
A slightly more efficient version is to use min() and max(), assuming that the site_id is never NULL:
select id, (case when min(site_id) = max(site_id) then 'Yes' else 'No' end) as result
from site_merchant sm
group by id
This is because min and max generally require a bit less processing than count(distinct).
If you want to check that the site_id is "1" and never anything else, then add the condition and min(site_id) = 1 to the when clause.
If you want to check that the site_id is 1 and there is exactly one row, then you can do:
select id, (case when count(site_id) = 1 and min(site_id) = 1 then 'Yes' else 'No' end) as result
from site_merchant sm
group by id
And, if you want to check that there is exactly one row:
select id, (case when count(site_id) = 1 then 'Yes' else 'No' end) as result
from site_merchant sm
group by id
SELECT
it,
CASE WHEN COUNT(CASE WHEN site_id THEN 1 END)=1
AND COUNT(CASE WHEN site_id!=1 THEN 1 END)=0 THEN 'Yes'
ELSE 'No'
END
FROM sm
GROUP BY it
Please see fiddle here.
SELECT ID, CASE WHEN EXTRA > 1 THEN 'No' ELSE 'Yes' END AS Result
FROM
(SELECT ID, Sum(site_id) AS Extra
from myTable
GROUP BY ID
) AS Test
EDIT: I suppose this should work in MySQL. I haven't worked on it though.
The idea is to SUM up the site_id. For records with only site_id = 1, the sum will be 1.
Your query seems overcomplicated. Just to start, why the IN(1) and NOT IN(2,3...9)? And why limit to a single ID (AND id = 240) when your "result sample" clearly doesn't want that? It does not make any sense. How about this?
SELECT CASE WHEN count(merchant_id) > 1 THEN 'N' ELSE 'Y' END as isTjoos
FROM site_merchant
GROUP BY site_id;
I would use a Having Count Statement. Something like that:
SELECT site_id
FROM site_merchant
HAVING (count(merchant_id) > 1)
GROUP BY site_id;
Here is the solution i found. I used #Gordons query to get started and what was missing was the site_id, and the group by was not needed:
SELECT
m.merchant_name,
m.merchant_id,
ut.realname,
a.affiliate_name,
(select (case when count(*)>0 then 'No' else 'Yes' end) as result
from site_merchant sm
WHERE sm.merchant_id = m.merchant_id
AND site_id != 1) as isTjoos,
(select (case when count(*)> 0 then 'No' else 'Yes' end) as result
from site_merchant sm
WHERE sm.merchant_id = m.merchant_id
AND site_id != 2) as isUCOnly,
(select (case when count(*)> 0 then 'No' else 'Yes' end) as result
from site_merchant sm
WHERE sm.merchant_id = m.merchant_id
AND site_id != 3) as isDLKonly
FROM merchant m
LEFT JOIN merchant_editor_assignment mea ON mea.merchant_id = m.merchant_id
LEFT JOIN user_table ut ON ut.user_id = mea.user_id
LEFT JOIN affiliate a ON a.affiliate_id = m.affiliate_id_default
Thank you for the help.

Slow running query, is there a better way?

I've got a query that produces the right result, its just very slow. I feel like there must be a better way (perhaps without subqueries).
Table, result and query are below. I've anonymized the data and I have 8 subqueries rather than 2, but the format is the same.
Table "a":
id userId type amount
------------------------------------
1 1 a 400
2 1 b 300
3 1 c 230
4 2 a 600
5 2 b 500
6 2 c 430
I've got an index on each column and one additional one that encompasses the userId and type columns. I can also guarantee you that userId and type are unique (i.e. there would't be two type 'a' for user 1).
Desired Result:
userId typeAtotal typeBtotal
--------------------------------
1 400 300
2 600 500
My Query:
SELECT userId,
(SELECT amount
FROM a AS a2
WHERE a2.userId = a1.userId
AND a2.type = 'a') AS aAmt,
(SELECT amount
FROM a AS a3
WHERE a3.userId = a1.userId
AND a3.type = 'b') AS bAmt
FROM a AS a1
WHERE type IN ('a','b')
GROUP BY userId
Use:
SELECT t.userid,
MAX(CASE WHEN t.type = 'a' THEN amount ELSE NULL END) AS typeAtotal,
MAX(CASE WHEN t.type = 'b' THEN amount ELSE NULL END) AS typeBtotal
FROM YOUR_TABLE t
GROUP BY t.userid
If there can be more than one amount for either type - this will return the highest. If you want such situations added, use SUM:
SELECT t.userid,
SUM(CASE WHEN t.type = 'a' THEN amount ELSE NULL END) AS typeAtotal,
SUM(CASE WHEN t.type = 'b' THEN amount ELSE NULL END) AS typeBtotal
FROM YOUR_TABLE t
GROUP BY t.userid
Looks like cross-tabulation to me. You might try something like this:
SELECT userId,
SUM(IF(a.type = 'a'), a.amount, 0) AS aAmount,
SUM(IF(a.type = 'b'), a.amount, 0) AS bAmount
FROM a
WHERE type IN ('a', 'b')
GROUP BY a.userId
You might want to read this rather well-written tutorial: http://dev.mysql.com/tech-resources/articles/wizard/index.html
Edit: fixed the ELSE condition.