sql query help on multiple count columns and group by - mysql

i have the following table Students:
id | status | school | name
----------------------------
0 | fail | skool1 | dan
1 | fail | skool1 | steve
2 | pass | skool2 | joe
3 | fail | skool2 | aaron
i want a result that gives me
school | fail | pass
---------------------
skool1 | 2 | 0
skool2 | 1 | 1
I have this but it's slow,
SELECT s.school, (
SELECT COUNT( * )
FROM school
WHERE name = s.name
AND status = 'fail'
) AS fail, (
SELECT COUNT( * )
FROM school
WHERE name = s.name
AND status = 'pass'
) AS pass,
FROM Students s
GROUP BY s.school
suggestions?

Something like this should work:
SELECT
school,
SUM(CASE WHEN status = 'fail' THEN 1 ELSE 0 END) as [fail],
SUM(CASE WHEN status = 'pass' THEN 1 ELSE 0 END) as [pass]
FROM Students
GROUP BY school
ORDER BY school
EDIT
Almost forgot, but you could also write the query this way:
SELECT
school,
COUNT(CASE WHEN status = 'fail' THEN 1 END) as [fail],
COUNT(CASE WHEN status = 'pass' THEN 1 END) as [pass]
FROM Students
GROUP BY school
ORDER BY school
I'm not sure if there's any performance benefit with second query. My guess would be if there is it's probably very small. I tend to use the first query because I think it's more clear but both should work. Also, I don't have a MySql instance handy to test with, but according to #Johan the ORDER BY clauses are unnecessary.

SELECT q.school, q.fail, q.failpass-q.fail as pass
FROM
(
SELECT s.school, sum(if(status = 'fail',1,0)) as fail, count(*) as failpass
FROM students s
GROUP BY s.school
) q
This way you save one conditional sum.
In MySQL a GROUP BY already orders the results, so a separate ORDER BY is not needed.

Related

Mysql Group by 3 fields with count

See the attached image to see the data I have.
I am trying to get a result like this:
SessionNumber | Event Date| Critical Care Count | Pulmonary Circulation
G1 | 5/19/2018 | 2 | 3
G1 | 5/20/2018 | 5 | 1
PCC1 | 5/19/2018 | 4 | 5
I'm trying to count the various primaryAssembly, topic, reg per SessionNumber and EventDate.
This is the query I am using:
select SessionNumber, EventDate, count(distinct BadgeID) as CriticalCareCount
from beacon
where primaryAssembly="Critical Care"
group by SessionNumber, EventDate
order by EventDate;
But I would rather not have to use the 'Where' clause. I'd like grouping on the term itself.
Here's a screen shot:
A pivot query can help:
SELECT SessionNumber,Event_Date,
count( case when primaryAssembly = 'Critical Care' then 1 end )
As Critical_Care_Count,
count( case when primaryAssembly = 'Pulmonary Circulation' then 1 end )
As Pulmonary_Circulation_Count,
count( case when primaryAssembly = 'Some other value' then 1 end )
As Some_other_value_Count,
......
......
count( case when some_other_logical_condition then 1 end )
As some_other_condition_count
......
......
SUM( case when primaryAssembly = 'Critical Care' then Duration else 0 end )
As sum_of_duration_for_critical_care
......
......
count(*) As total_count
FROM table
GROUP BY SessionNumber,Event_Date

SQL: count of distinct users with conditions based on many to many table

I have a typical user table in addition to the following feature table
features:
-----------------------
| userId | feature |
-----------------------
| 1 | account |
| 1 | hardware |
| 2 | account |
| 3 | account |
| 3 | hardware |
| 3 | extra |
-----------------------
Basically I am trying to get some counts for reporting purposes. In particular, I am trying to find the number of users with accounts and hardware along with the total number of accounts.
I know I can do the following to get the total number of accounts
SELECT
COUNT(DISTINCT userId) as totalAccounts
FROM features
WHERE feature = "account";
I am unsure as to how to get the number of users with both accounts and hardware though. In this example dataset, the number I am looking for is 2. Users 1 and 3 have both accounts and hardware.
I would prefer to do this in a single query. Possibly using CASE (example for totalAccounts below):
SELECT
COUNT(DISTINCT(CASE WHEN feature = "account" THEN userId END)) as totalAccounts,
COUNT( ? ) as accountsWithHardware
FROM features;
These are two queries - one for the all user count, one for the two-features user count - that you can combine with a cross join:
select
count_all_users.cnt as all_user_count,
count_users_having_both.cnt as two_features_user_count
from
(
select count(distinct userid) as cnt
from features
) count_all_users
cross join
(
select count(*) as cnt
from
(
select userid
from features
where feature in ('account', 'hardware')
group by userid
having count(*) = 2
) users_having_both
) count_users_having_both;
UPDATE: With some thinking, there is a much easier way. Group by user and detect whether feature 1 and feature 2 exists. Then count.
select
count(*) as all_user_count,
count(case when has_account = 1 and has_hardware = 1 then 1 end)
as two_features_user_count
from
(
select
userid,
max(case when feature = 'account' then 1 else 0 end) as has_account,
max(case when feature = 'hardware' then 1 else 0 end) as has_hardware
from features
group by userid
) users;

SQL queries using self-join

I can make simple select, join, update queries. But this seems a bit hard for me (I'm just learning).
Customer has a table like this (using Mysql) (I don't have control on his DB schema, I can't create a Customers table for him. I just need to create some customer reports).
+-----------+--------------+--------------------------+
|Transaction|Customers name|Customers email |Set |
+-----------+--------------+--------------------------+
| 1 | John | jo#gmail.com | blue |
| 2 | Mary | ma#gmail.com | green |
| 3 | Paul | pa#gmail.com | red |
| 4 | JOHN G. | jo#gmail.com | green |
| 5 | Paul Simon | pa#gmail.com | blue |
+-----------+--------------+--------------------------+
As you can see, each transaction the customer enters freely his name. That could lead to apparently more customers, but email field is unique.
I need to make these reports (all of them are driven by what he bought - the 'Set' field):
1) AND searchs (like 'blue' AND 'green')
Customers that have bought 'this' AND 'that' set.
I need to get a result like this:
|John | jo#gmail.com |
or this (as I said, John could enter his name different ways each transaction. If the email is unique, it is fine):
|JOHN G. | jo#gmail.com |
2) OR searchs (like 'blue' or 'red')
Need to get this:
|John | jo#gmail.com |
|Paul | pa#gmail.com |
or this:
|John | jo#gmail.com |
|Paul Simon| pa#gmail.com |
3) Bought one set, but not the other (like 'green' but not 'blue')
|Mary | ma#gmail.com |
Doe anyone know how to do that? I believe this could be accomplished by some kind of 'self join'. But as I'm just a beginner, I couldn't figure out how to solve this.
Obviously a person can buy this or that set and I imagine it is even possible that one person buys the same set again in a later transaction.
So you want information per person. The easiest way is to aggregate you data hence by grouping by person (GROUP BY). Then you check your aggregates in the HAVING clause: Did the customer by set X and / or y?
Query 1:
select email, name
from transactions
group by email
having max(case when set = 'blue' then 1 else 0 end) = 1
and max(case when set = 'green' then 1 else 0 end) = 1;
Query 2:
select email, name
from transactions
group by email
having max(case when set = 'blue' then 1 else 0 end) = 1
or max(case when set = 'red' then 1 else 0 end) = 1;
Query 3:
select email, name
from transactions
group by email
having max(case when set = 'green' then 1 else 0 end) = 1
and max(case when set = 'blue' then 1 else 0 end) = 0;
The name you get with these queries is just one of the matching names arbitrarily chosen. This is something special in MySQL. In standard SQL this would not be allowed. Anyway, whether MySQL or standard SQL, you could also use MIN(name) or MAX(name) to always get the first or last in alphabetical order.
By the way: The CASE WHEN expressions are standard SQL. MySQL, however, features an additional special boolean handling: a true expression evaluates to 1 and a false expression to 0. So in MySQL you can simply write max(set = 'green') = 1 instead of max(case when set = 'green' then 1 else 0 end) = 1.
For first query - I am using sub query here
select name,email from customer c where Set = 'blue' and c.email = (select email from customer where Set = 'green' and email = c.email);
For second query- simple or condition will be enough
select * from customer where Set = 'green' or Set = 'blue' group by email ;
For Third query- (its a work around, as per your requirement it will work,it is based on approach like there should be only 1 record and that record should have Set value as mentioned int the input)
select * from customer group by email having count(pset)= 1 and pset like 'green';

Adding a "total sum" row with a select statement in Mysql

Query:
SELECT (CASE
WHEN a.account_number = '123456' THEN 'Savings'
WHEN a.account_number = '123321' THEN 'Credit'
END) AS "Account"
FROM accounts a
GROUP BY (CASE
WHEN a.account_number = '123456' THEN 'Savings'
WHEN a.account_number = '123321' THEN 'Credit'
END);
Output:
+---------+
| Account |
+---------+
| Savings |
| Credit |
+---------+
Desired Output:
+---------+
| Account |
+---------+
| Savings |
| Credit |
| Total |
+---------+
Given this query, how can I add a new row with the totals on the bottom?
Later I'm going to be doing more transactions...
Presumably Savings == Debit?
It might well be easier to do this in the programming language rather than the query, but could you try something like:
SELECT IFNULL((CASE
WHEN a.account_number = '123456' THEN 'Savings'
WHEN a.account_number = '123321' THEN 'Credit'
END), "Total") AS "Account"
FROM accounts a
GROUP BY a.account_number WITH ROLLUP;
I've not tried this yet so not 100% certain it will work, or how to make it named "Total" (it will be under NULL).
See WITH ROLLUP.
Looking at your diagram is it a total row that you're after? I'm a little confused.
However, if it is a total row adding all credits and debits together then i would look into the UNION statement.
basic example:
SELECT a.AccountCreditsDebits From Table
UNION
SELECT SUM(a.AccountCreditsDebits) From Table

MySQL - Complex COUNT Query

I have a table called user_scores as below:
id | af_id | uid | level | record_date
----------------------------------------
1 | 1.1 | 1 | 3 | 2012-01-01
2 | 1.1 | 1 | 4 | 2012-02-01
3 | 1.2 | 1 | 3 | 2012-01-01
4 | 1.2 | 1 | 5 | 2012-03-01
...
I have another table call user_info as below:
uid | forename | surname | gender
-----------------------------------
1 | Homer | Simpson | M
2 | Marge | Simpson | F
3 | Bart | Simpson | M
4 | Lisa | Simpson | F
...
In user scores uid is the user id of a registered user on the system, af_id identifies a particular test a user submits. A user scores a level between 1 - 5 for each test, which can be submitted every month.
My problem is I need to produce an analysis at the end of the year to COUNT the number of users that have achieved each level for a particular test. The analysis is to show a gender split for male and female.
So for example an administrator would select test 1.1 and the system would generate stats based that would COUNT of the total MAX level achieved by each user in the year, with a gender split.
Any help is much appreciated. Thank you in advance.
-
I think I need to clarify myself a bit. Because a user can complete the test multiple times throughout the year, there will be multiple scores for the same test. The query should take the highest level achieved and include this in the count. An example result would be:
Male Results:
level1 | level2 | level3 | level4 | level5
------------------------------------------
2 | 5 | 10 | 8 | 1
I am not certain I get exactly what you mean, but as always I'll have a go. As I understand it you want to know how many people from each gender reached each level in a certain year.
SELECT MaxLevel,
COUNT(CASE WHEN ui.Gender = 'M' THEN 1 END) AS Males,
COUNT(CASE WHEN ui.Gender = 'F' THEN 1 END) AS Females
FROM User_Info ui
INNER JOIN
( SELECT MAX(Level) AS MaxLevel,
UID
FROM User_Scores us
WHERE af_ID = '1.1'
AND YEAR(Record_Date) = 2012
GROUP BY UID
) AS MaxUs
ON MaxUs.uid = ui.UID
GROUP BY MaxLevel
I've put some sample data on SQL Fiddle so you see if it is what you were after.
EDIT
To transpose the data so levels are along the top and Gender in the rows the following will work:
SELECT Gender,
COUNT(CASE WHEN MaxLevel = 1 THEN 1 END) AS Level1,
COUNT(CASE WHEN MaxLevel = 2 THEN 1 END) AS Level2,
COUNT(CASE WHEN MaxLevel = 3 THEN 1 END) AS Level3,
COUNT(CASE WHEN MaxLevel = 4 THEN 1 END) AS Level4,
COUNT(CASE WHEN MaxLevel = 5 THEN 1 END) AS Level5
FROM User_Info ui
INNER JOIN
( SELECT MAX(Level) AS MaxLevel,
UID
FROM User_Scores us
WHERE af_ID = '1.1'
AND YEAR(Record_Date) = 2012
GROUP BY UID
) AS MaxUs
ON MaxUs.uid = ui.UID
GROUP BY Gender
Note, that if there are ever more than 5 levels you will need to add more to the select statement, or start building dynamic SQL.
Assuming record_date holds only dates (without time parts):
SELECT
s.maxlevel,
COUNT(NULLIF(gender, 'F')) AS M,
COUNT(NULLIF(gender, 'M')) AS F
FROM user_info u
INNER JOIN (
SELECT
uid,
MAX(level) AS maxlevel
FROM user_scores
WHERE record_date > DATE_SUB(CURDATE(), INTERVAL DAYOFYEAR(CURDATE()) DAY)
AND af_id = '1.1'
GROUP BY
uid
) s ON s.uid = u.uid
GROUP BY
s.maxlevel
That will show you only the maximum levels found in the user_scores table. If you have a Levels table where all possible levels (1 to 5) are listed, you could use that table to get a complete list of levels. If some levels are not present in the requested subset of data, the corresponding rows will show 0s in both columns.
Here's the above script with minor changes to show the complete chart of levels:
SELECT
l.level AS maxlevel,
COUNT(NULLIF(gender, 'F')) AS M,
COUNT(NULLIF(gender, 'M')) AS F
FROM user_info u
INNER JOIN (
SELECT
uid, MAX(level) AS maxlevel
FROM user_scores
WHERE record_date > DATE_SUB(CURDATE(), INTERVAL DAYOFYEAR(CURDATE()) DAY)
AND af_id = '1.1'
GROUP BY
uid
) s ON s.uid = u.uid
RIGHT JOIN Levels l ON s.maxlevel = l.level
GROUP BY
l.level
Hope this is what your looking for!
Show number of records group by userid and gender of the max score for af_id '1.1'.
select count(*), info.uid, info.gender, max(score.level)
from user_info as info
join user_scores as score
on info.uid = score.uid
where score.af_id = '1.1'
group by info.uid, info.gender;
EDITED based on your edit.
select sum(if(a.gender="M",1,0)) Male_users, sum(if(a.gender="F",1,0)) Female_users
from myTable a where
a.level = (select max(b.level) from myTable b where a.uid=b.uid)
group by af_id.
I typed this in a rush. But it should work or at least get you where you need to go. E.G. if you need to specify time frame, add that.
You need something like
SELECT
uid,
MAX(level)
WHERE
record_date BETWEEN '2012-01-01' AND '2012-12-31'
AND af_id='1.1'
GROUP BY uid
If you need the gender splits then depending on what stat you need per gender you can either add a JOIN on the user_info table into this query (to get the MAX per gender) to wrap this as a sub-query and JOIN on the whole thing.