MySQL Query: 3 tests passed, 1 failed - mysql

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

Related

IF condition on a group of record using mysql

Suppose I have a table t1 with two columns, Student_ID and Grade. The data shown like this;
Student_ID, Grade (1,'A'),(1,'C'),(1,'F'),(1,'A'),(2,'B'),(2,'A'),(2,'C'),(2,'A').
Now I have to group the Student_ID with a new column named 'Result'. Student_ID 1 have one grade 'F' so consider failed and Student_ID 2 have no 'F' grade so consider passed. Require result should be like this.
Student_ID Result
1 Failed
2 Passed
Because the table have 500 records so can not manually enter 'Passed" or 'Failed'.
SELECT `Student_ID`, (IF(`Grade`='F',"Failed","Passed")) AS `Result`
FROM t1 group by `Student_ID`;
Above code will not work as the column Grade has 4 values not 1.
So code will be some thing like this.
SELECT `Student_ID`, (IF(`Grade` CONTAINS 'F',"Failed","Passed")) AS `Result` FROM t1 group by `Student_ID`;
There is no such thing CONTAINS in sql but may be HAVING or IN but how to use them here?
You can use CASE statement with EXISTS:
select DISTINCT
t.Student_ID,
case
when exists (
select 1 from t1 where Student_ID = t.Student_ID and Grade = 'F'
) then 'Failed'
else 'Passed'
end Result
from t1 t
See the demo.
Results:
| Student_ID | Result |
| ---------- | ------ |
| 1 | Failed |
| 2 | Passed |
You can also do it by evaluating each Grade to 1 or 0 for 'F' or not 'F' and then grouping by Student_ID:
select
t.Student_ID,
case sum(t.Result)
when 0 then 'Passed'
else 'Failed'
end Result
from (select Student_ID, Grade = 'F' Result from t1) t
group by t.Student_ID
See the demo.

Simple SQL Query Help Required

I have this table
uid | rid
-----------
1 | 4
1 | 13
2 | 4
3 | 13
I want my query to return the uid where it has rid = 13 AND does not have rid = 4 so the result would be 3
So far I have
SELECT distinct uid
FROM test
WHERE
ur.rid <> 4
AND ur.rid = 13
but of course this is returning 1 and 3
What am I missing from my query to get only 3?
Many thanks.
You can add NOT EXISTS:
SELECT DISTINCT uid
FROM test t1
WHERE t1.rid = 13
AND NOT EXISTS (SELECT 1
FROM test t2
WHERE t2.uid = t1.uid
AND t2.rid = 4);
LiveDemo
or using aggregation:
SELECT uid
FROM test
GROUP BY uid
HAVING COUNT(CASE WHEN rid = 13 THEN 1 END) > 0
AND COUNT(CASE WHEN rid = 4 THEN 1 END) = 0;
LiveDemo2
EDIT (by gordon):
The above answer is fine. I'm just adding this as a simplification to the second query (it didn't seem appropriate to put in a separate answer and it is too long for a comment):
SELECT uid
FROM test
GROUP BY uid
HAVING SUM(rid = 13) > 0 AND
SUM(rid = 4) = 0;
It takes advantage of a nice feature of MySQL.
SqlFiddleDemo

Aggregate rows if one row meets a specific condition

I'm trying to select the sum of the values in the isOK column for each Name separated, BUT only if isOK = 1 on Day = 2.
The query for the following example table tablename
Name | Day | isOK
char | int | int
-----------------
Flo | 1 | 1
Seb | 1 | 1
Tim | 1 | 0
Flo | 2 | 1
Seb | 2 | 0
Tim | 2 | 1
should give Flo: 2 and Tim: 1, but not Seb: 1, since his isOK on Day = 2 is 0.
I've tried using SUM(isOK) with IF constructs, but it's just not working. My alternative solution, to select all Name where isOK = 1 first and select the SUM(isOK) for each of the names is slow and seems in need of improvement.
I guess it's not that difficult, but I've been trying for hours now and I just can't combine my two queries into one.
One way to do this is to use a conditional expression together with a having clause like this:
select name, sum(isOk) ok_sum
from your_table
group by name
having sum(case when day = 2 and isOK = 1 then 1 else 0 end) > 0;
With your sample data the result would be:
name ok_sum
Flo 2
Tim 1
As MySQL evaluates boolean expressions as 1 or 0 it should be possible to reduce the condition to this:
having sum(day = 2 and isOK = 1) > 0;
Another way to do it would be to use a correlated subquery that makes sure there exists a row with Day = 2 and isOk = 1 for the Name:
select t1.name, sum(t1.isOk) ok_sum
from your_table t1
where exists (
select 1
from your_table t2
where t2.day = 2 and t2.isOK = 1 and t1.name = t2.name
)
group by t1.name
See this fiddle
TRY this :
SELECT
name, SUM(isok) AS isOk
FROM
table
GROUP BY `name`
HAVING SUM(`day` = 2 AND isok = 1) > 0;
SELECT x.name, SUM(y.isOK) total
FROM my_table x
JOIN my_table y
ON y.name = x.name
WHERE x.day = 2
AND x.isok=1
GROUP
BY x.name;

Count value variation from 0 to 1 in mysql table

I have a column with two columns. one is TIMESTAMP and the other DIGITAL_BIT.
The value digital bit can be either 0 or 1 and changes a few times during the day. Every minute of the day is stored in this table. I would need to read somehow how many times a day this value changed from 0 to 1.
Is it possible to make a query that returns the count of this changes? What I have in mind is something like this:
select * from mytable where digital_bit = 1 and digital_bit (of previous row) = 0 order by timestamp
Can this be done with a query or do i have to process all data in my program?
Thanks
SAMPLE
timestamp | digital_bit
100000 | 0
100001 | 0
100002 | 1
100003 | 1
100004 | 0
100005 | 1
100006 | 0
100007 | 0
100008 | 1
the above should return 3 because for 3 times the value digital passed from 0 to 1. i need to count how often the value digital CHANGES from 0 to 1.
Here you go. This will get you a count of how many times digital_bit switched from 0 to 1 (in your example, this will return 3).
SELECT COUNT(*)
FROM mytable curr
WHERE curr.digital_bit = 1
AND (
SELECT digital_bit
FROM mytable prev
WHERE prev.timestamp < curr.timestamp
ORDER BY timestamp DESC
LIMIT 1
) = 0
SQLFiddle link
(Original answer relied on the timestamps being sequential: e.g. no jumps from 100001 to 100003. Answer has now been updated not to have that restriction.)
IF you have a result once per minte, you can simple join the table with itself, and
use timestamp+1 as well as leftbit != rightbit as join condition.
http://sqlfiddle.com/#!8/791c0/6
ALL Changes:
SELECT
COUNT(*)
FROM
test a
INNER JOIN
test b
ON
a.digital_bit != b.digital_bit
AND b.timestamp = a.timestamp+1;
Changes from 0 to 1
SELECT
COUNT(*)
FROM
test a
INNER JOIN
test b
ON
a.digital_bit = 0 AND
a.digital_bit != b.digital_bit
AND b.timestamp = a.timestamp+1;
Changes from 1 to 0
SELECT
COUNT(*)
FROM
test a
INNER JOIN
test b
ON
a.digital_bit = 1 AND
a.digital_bit != b.digital_bit
AND b.timestamp = a.timestamp+1;
Adapted from: How do I query distinct values within multiple sub record sets
select count(*)
from (select t1.*,
(select digital_bit
from table t2
where t2.timestamp < t1.timestamp
order by timestamp desc LIMIT 1
) as prevvalue
from table t1
) t1
where prevvalue <> digital_bit and digital_bit = 1;
This isn't likely to be efficient with a lot of data, but you can get all the rows and calculate a sequence number for them, then do the same again but with the sequence number offset by 1. Then join the 2 lots together where those calculated sequence numbers match but the first one has a digital bit of 0 and the other a digital bit of 1:-
SELECT COUNT(*)
FROM
(
SELECT mytable.timestamp, mytable.digital_bit, #aCount1:=#aCount1+1 AS SeqCount
FROM mytable
CROSS JOIN (SELECT #aCount1:=1) sub1
ORDER BY timestamp
) a
INNER JOIN
(
SELECT mytable.timestamp, mytable.digital_bit, #aCount2:=#aCount2+1 AS SeqCount
FROM mytable
CROSS JOIN (SELECT #aCount2:=0) sub1
ORDER BY timestamp
) b
ON a.SeqCount = b.SeqCount
AND a.digital_bit = 0
AND b.digital_bit = 1
EDIT - alternative solution and I would be interested to see how this performs. It avoids the need for adding a sequence number and also avoids a correlated sub query:-
SELECT COUNT(*)
FROM
(
SELECT curr.timestamp, MAX(curr2.timestamp) AS MaxTimeStamp
FROM mytable curr
INNER JOIN mytable curr2
ON curr.timestamp > curr2.timestamp
AND curr.digital_bit = 1
GROUP BY curr.timestamp
) Sub1
INNER JOIN mytable curr
ON Sub1.MaxTimeStamp = curr.timestamp
AND curr.digital_bit = 0
As I understood you have one query every minute. So you have no problem with performance.
You can add flag:
timestamp | digital_bit | changed
100000 | 0 | 0
100001 | 0 | 0
100002 | 1 | 1
100003 | 1 | 0
100004 | 0 | 1
100005 | 1 | 1
100006 | 0 | 1
100007 | 0 | 0
100008 | 1 | 1
And make check before insert:
SELECT digital_bit
FROM table
ORDER BY timestamp DESC
LIMIT 1
and if digital_bit is different insert new row with flag.
And then you just can take COUNT of flags:
SELECT COUNT(*)
FROM table
WHERE DATE BETWEEN (start, end)
AND changed = 1
Hope will see in answers better solution.

MySQL subtract two count columns

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