COUNT with case statement not working properly - mysql

Suppose I have the following ouput from my query:
{
"player_first_name": "Albano",
"player_last_name": "Aleksi",
"yellow_cards": "14",
"orange_cards": "0",
"red_cards": "1",
"points": "15",
"player_id": "286635"
}
as you can see the player have 14 yellow cards, 1 red cards and 0 orange cards.
I want calculate the "point" earned by this playern counting each card in the following way:
yellow card: 1 point
orange card: 2 point
red card: 3 point
so the final result should be: 17
I tried to count the total of the points in the following way:
$sql = $this->db->prepare("SELECT
p.first_name AS player_first_name,
p.last_name AS player_last_name,
COUNT(CASE
WHEN c.card_id = 1 THEN 1
END) AS yellow_cards,
COUNT(CASE WHEN c.card_id = 2 THEN 1 END) AS orange_cards,
COUNT(CASE
WHEN c.card_id = 3 THEN 1
END) AS red_cards,
COUNT(CASE
WHEN c.card_id = 1 THEN 1
WHEN c.card_id = 2 THEN 2
WHEN c.card_id = 3 THEN 3
END) AS points,
p.id AS player_id
FROM `match` m
INNER JOIN player_cards c ON c.match_id = m.id
INNER JOIN player p ON c.player_id = p.id
WHERE m.round_id = :round_id
GROUP BY p.id
ORDER BY points DESC, player_last_name ASC");
as you can see I have the following statement:
COUNT(CASE
WHEN c.card_id = 1 THEN 1
WHEN c.card_id = 2 THEN 2
WHEN c.card_id = 3 THEN 3
END) AS points,
the card id correspond the id of the color, so:
1: yellow card
2: orange card
3: red card
why the toal is incorrect?
UPDATE
Expected output:
{
"player_first_name": "Albano",
"player_last_name": "Aleksi",
"yellow_cards": "14",
"orange_cards": "0",
"red_cards": "1",
"points": "17",
"player_id": "286635"
}
as you can see points is 17 because we have 14 yellow cards (each yellow card is worth 1 point), and we have 1 red card, each red card is worth 3 point, so: 14 + 3 = 17. But I get 15

Replace
COUNT(CASE
WHEN c.card_id = 1 THEN 1
WHEN c.card_id = 2 THEN 2
WHEN c.card_id = 3 THEN 3
END) AS points,
with
SUM(CASE
WHEN c.card_id = 1 THEN 1
WHEN c.card_id = 2 THEN 2
WHEN c.card_id = 3 THEN 3
END) AS points,
because with count you just count while you want the sum really. If you count the numbers 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3 you get fifteen, if you add them up you get seventeen.
However, there is obviously a card table the card_id refers to. This card table should naturally contain the value of the card. So join with the cards table and use
SUM(card.value)
instead of the above.

Since you just want to sum the numerical value of the card to get the total, then just use SUM(card_id) instead of COUNT:
SELECT
p.first_name AS player_first_name,
p.last_name AS player_last_name,
COUNT(CASE WHEN c.card_id = 1 THEN 1 END) AS yellow_cards,
COUNT(CASE WHEN c.card_id = 2 THEN 1 END) AS orange_cards,
COUNT(CASE WHEN c.card_id = 3 THEN 1 END) AS red_cards,
SUM(c.card_id) AS points,
p.id AS player_id
FROM match m
INNER JOIN player_cards c
ON c.match_id = m.id
INNER JOIN player p
ON c.player_id = p.id
WHERE
m.round_id = :round_id
GROUP BY
p.id
ORDER BY
points DESC, player_last_name;
Your PHP code should then look like this:
$stmt = $connection->prepare();
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
$red = $row["red_cards"];
$yel = $row["yellow_cards"];
$orange = $row["orange_cards"];
$points = $row["points"];
}

What if you try to count the points by query the cards from the database, save it in variables and then count them all without any inline CASE statements in SQL. Make a simple query where you get each player's red, orange and yellow cards and then count them as in the example.
// The query to get the cards, name etc.
$sql = "SELECT
p.first_name AS player_first_name,
p.last_name AS player_last_name,
c.card_id = 1 THEN 1 AS yellow_cards
c.card_id = 2 THEN 2 AS orange_cards
c.card_id = 3 THEN 3 AS red_cards
p.id AS player_id
FROM `match` m
INNER JOIN player_cards c ON c.match_id = m.id
INNER JOIN player p ON c.player_id = p.id
WHERE m.round_id = round_id
GROUP BY p.id
ORDER BY points DESC, player_last_name ASC";
// Prepared statement to get the cards as variables and then count the points
$stmt = $connection->prepare();
$stmt->execute();
$result = $stmt->get_result();
while($row = $result->fetch_assoc()){
$red = $row["red_cards"];
$yel = $row["yellow_cards"];
$orange = $row["orange_cards"];
$points = $red * 3 + $orange * 2 + $yel;
}
Please note that I used prepared statements in the example and the $connection variable covers the mySQL connection to the server.

Just replace all your count with sum and retry.

As mentioned in other answers, count() counts the number of non-NULL results. Hence, 0 is as non-NULL as any other value.
But, MySQL also offers a convenient shortcut that doesn't use CASE. Boolean expressions are treated as numbers in a numeric context, with "1" for true and "0" for false. So:
SELECT p.first_name AS player_first_name, p.last_name AS player_last_name,
SUM( c.card_id = 1 ) AS yellow_cards,
SUM( c.card_id = 2 ) AS orange_cards,
SUM( c.card_id = 3 ) AS red_cards,
SUM( CASE WHEN c.card_id IN (1, 2, 3) THEN c.card_id ELSE 0 END) AS points,
p.id AS player_id
FROM `match` m INNER JOIN
player_cards c
ON c.match_id = m.id INNER JOIN
player p ON c.player_id = p.id
WHERE m.round_id = :round_id
GROUP BY p.id
ORDER BY points DESC, player_last_name ASC;
If the only allowed card_ids are 0, 1, 2, and 3, then the points calculation can be simplified to:
SUM( c.card_id ) AS points,

The simplest code is:
SUM(c.card_id) AS points,
If you don't want to depend on card_id having certian values, then this is the next simplest:
SUM((c.card_id = 1) + 2 * (c.card_id = 2) + 3 * (c.card_id = 3))
This works because in MySQL true is 1 and false is 0.

Related

Mysql query subselect getting NULL

I'm trying to join over three tables and get the active plan of a vendor. It is possible, that the vendor had a lot of plans in the past, but the active on is that counts.
The whole query is bigger (counting items he has aso) and because of that i did it with a subselect, but for this example it should be enough.
I always get plantitle and planstatus of NULL. How can i fix this?
Query
SELECT v.title
, plans.title AS plantitle
, uplans.status AS planstatus
, uplans.uid
, COUNT(DISTINCT obs.id) AS obj_count
, sum(case when obs.published = -1 then 1 else 0 end) trash
, sum(case when obs.published = 1 then 1 else 0 end) published
, sum(case when obs.published = 0 then 1 else 0 end) unpublished
FROM `vendors` AS v
LEFT JOIN objects AS obs ON obs.vid = v.id
LEFT JOIN `userplans` AS uplans ON uplans.uid = (
SELECT up.id
FROM `userplans`AS up
WHERE up.uid=v.uid AND status = "ACTIVE" LIMIT 1)
LEFT JOIN `plans` AS plans ON plans.id=uplans.pid
GROUP BY v.id
ORDER BY v.id asc
Tables
Vendors
id, uid, title
10, 1, Name 1
20, 4, Name 2
30, 5, Name 3
Plans
id, title
40, Plan 1
50, Plan 2
Userplans
id, uid, pid, status
1, 1, 40, CANCELED
2, 1, 50, CANCELED
3, 1, 40, CANCELED
4, 4, 50, CANCELED
5, 4, 50, CANCELED
6, 4, 50, ACTIVE
7, 1, 40, ACTIVE
Lets get the object counts 1st as the associations to other tables may be 1-M which would result in larger counts. then join to the other needed information.
This still assumes that a the combination of a user and plan in userPlan can only have 1 active record. If it can have more than 1 I still need to know which active userPlan to select.
Also why the left joins? are you after all vendors regardless of plans and objects and userplans? Is it possible that a vendor HAS no active plans in which case the title would be null?
SELECT v.title
, P.title AS plantitle
, UP.status AS planstatus
, up.uid
, O.obj_count
, O.trash
, O.published
, O.unpublished
FROM vendors v
LEFT JOIN userplans UP
ON V.uid = UP.UID
AND UP.status = 'ACTIVE'
LEFT JOIN (SELECT obs.VID
,COUNT(DISTINCT obs.id) AS obj_count
,sum(case when obs.published = -1 then 1 else 0 end) trash
,sum(case when obs.published = 1 then 1 else 0 end) published
,sum(case when obs.published = 0 then 1 else 0 end) unpublished
FROM OBJECTS obs
GROUP BY obs.VID) O
ON O.vid = v.id
LEFT JOIN `plans` P
ON P.id=UP.pid
ORDER BY v.id asc
And to address the comment to get the "Latest" Plan regardless of status (assuming latest would have the highest ID in the userPlans table.
SELECT v.title
, P.title AS plantitle
, UP.status AS planstatus
, up.uid
, O.obj_count
, O.trash
, O.published
, O.unpublished
FROM vendors v
LEFT JOIN (SELECT * -- though really we should just pull in the columns needed.
FROM USERPLANS U1
INNER JOIN (SELECT max(ID) ID, PID, UID
FROM UserPlans
GROUP BY PID, UID) U2
on U1.ID = U2.ID) UP
ON V.uid = UP.UID
LEFT JOIN (SELECT obs.VID
,COUNT(DISTINCT obs.id) AS obj_count
,sum(case when obs.published = -1 then 1 else 0 end) trash
,sum(case when obs.published = 1 then 1 else 0 end) published
,sum(case when obs.published = 0 then 1 else 0 end) unpublished
FROM OBJECTS obs
GROUP BY obs.VID) O
ON O.vid = v.id
LEFT JOIN `plans` P
ON P.id=UP.pid
ORDER BY v.id asc
In your join of userplans - you are joining uplans.uid with the selected id from the same table - you need to join on the same column - change the line to :
LEFT JOIN `userplans` AS uplans ON uplans.id = (
Something like this might work:
SELECT Vendors.title, Plans.title, Userplans.status, Userplans.uid FROM Vendors, Plans, Userplans
WHERE Vendors.uid = Userplans.uid AND Plans.id = Userplans.pid AND Userplans.status = 'Active'
This assumes that you can only ever have one Active per user

Display MySQL Pricing Chart in Spreadsheet-like Columns

I'm trying to output a friendly price table in MySQL for export/import into a spreadsheet. Let's use fruits and their price breaks as an example.
Here's a fiddle for the schema I'm referring to:
http://sqlfiddle.com/#!9/c526e3/4
Simply:
Table: fruit
id
name
Table: fruit_pricing
id
fruit_id
min_quantity
max_quantity
price
When executing the query:
SELECT
F.name,
IF(FP.min_quantity = 1, FP.price, '0') as qty_1,
IF(FP.min_quantity = 10, FP.price, '0') as qty_10,
IF(FP.min_quantity = 25, FP.price, '0') as qty_25,
IF(FP.min_quantity = 50, FP.price, '0') as qty_50,
IF(FP.min_quantity = 100, FP.price, '0') as qty_100
FROM Fruit F
LEFT JOIN FruitPricing FP ON FP.fruit_id = F.id
It displays the results like this:
What I'd like to do is group the fruit names so there are only three rows: Apple, Grape, and Orange. Then, I'd like all the 0 values to be replaced with the appropriate quantities. I'm trying to get the same output as the spreadsheet in this screenshot:
Are there any nice tricks for accomplishing this? I'm unsure of the sql-tech-speak for this particular question, making it difficult to search for an answer. I'd be happy to update my question subject if I can and somebody has a better suggestion for it.
SELECT f.name
, SUM(CASE WHEN fp.min_quantity = 1 THEN fp.price ELSE 0 END) qty_1
, SUM(CASE WHEN fp.min_quantity = 10 THEN fp.price ELSE 0 END) qty_10
, SUM(CASE WHEN fp.min_quantity = 25 THEN fp.price ELSE 0 END) qty_25
, SUM(CASE WHEN fp.min_quantity = 50 THEN fp.price ELSE 0 END) qty_50
, SUM(CASE WHEN fp.min_quantity = 100 THEN fp.price ELSE 0 END) qty_100
FROM fruit f
LEFT
JOIN fruitpricing fp
ON fp.fruit_id = f.id
GROUP
BY name;
Although, if it was me, I'd probably just do the following, and handle any remaining display issues in the presentation layer...
SELECT f.name
, fp.min_quantity
, SUM(fp.price) qty
FROM fruit f
LEFT
JOIN fruitpricing fp
ON fp.fruit_id = f.id
GROUP
BY name
, min_quantity;

MySQL query slow in Where MONTH(datetime)

I am trying to add index in datetime, but the result still same.
SELECT s.id, s.player,
COUNT(case when dg.winner = 1 AND dp.colour <= 5 then 1 when dg.winner = 2 AND dp.colour > 5 then 1 else null end) as totalwin,
COUNT(case when dg.winner = 2 AND dp.colour <= 5 then 1 when dg.winner = 1 AND dp.colour > 5 then 1 else null end) as totallose,
COUNT(dg.winner) as totalgames
FROM dotaplayers AS dp
LEFT JOIN gameplayers AS gp ON gp.gameid = dp.gameid and dp.colour = gp.colour
LEFT JOIN stats AS s ON s.player_lower = gp.name
LEFT JOIN dotagames AS dg ON dg.gameid = dp.gameid
LEFT JOIN games AS g ON g.id = dp.gameid
LEFT JOIN bans as b ON b.name=gp.name
WHERE MONTH(g.datetime) = 4
GROUP by gp.name
ORDER BY totalwin DESC LIMIT 0,10
Showing rows 0 - 9 (10 total, Query took 7.7552 seconds.)
I want order the most winner in 4th month (April). Then it shows id, username, totalwins, totallose, totaldraw, totalgames. The case in my query is the how to get that. The result is correct, but slow.
Assuming g.datetime is indexed, try this instead:
WHERE g.`datetime` BETWEEN 20150401 AND 20150430`
Using the MONTH function, or any other function, on the field data in the WHERE eliminates the benefits of any indexes you might have on those fields; this results in the query requiring a full scan of the values in the table.
Rearranging the order of JOINs will probably help as well:
SELECT s.id, s.player
, SUM(case
when dg.winner = 1 AND dp.colour <= 5 then 1
when dg.winner = 2 AND dp.colour > 5 then 1
else 0
end
) as totalwin
, SUM(case
when dg.winner = 2 AND dp.colour <= 5 then 1
when dg.winner = 1 AND dp.colour > 5 then 1
else 0
end
) as totallose
, COUNT(dg.winner) as totalgames -- Not, sure of the nature of dg.`winner`, a SUM might be more appropriate here as well.
FROM games AS g
INNER JOIN dotaplayers AS dp ON g.id = dp.gameid
LEFT JOIN gameplayers AS gp ON gp.gameid = dp.gameid and dp.colour = gp.colour
LEFT JOIN stats AS s ON s.player_lower = gp.name
LEFT JOIN dotagames AS dg ON dg.gameid = dp.gameid
LEFT JOIN bans as b ON b.name=gp.name
WHERE g.`datetime` BETWEEN 20150401000000 AND 20150430235959
GROUP by gp.name
ORDER BY totalwin DESC
LIMIT 0,10
;
Another thing to note: Depending on the relationship between tables, some of the intermediate joins may result in effectively multiplying the resulting totals; this can be resolved by doing the sums in subqueries and joining those instead.

Complex querying on table with multiple userids

I have a table like this:
score
id week status
1 1 0
2 1 1
3 1 0
4 1 0
1 2 0
2 2 1
3 2 0
4 2 0
1 3 1
2 3 1
3 3 1
4 3 0
I want to get all the id's of people who have a status of zero for all weeks except for week 3. something like this:
Result:
result:
id w1.status w2.status w3.status
1 0 0 1
3 0 0 1
I have this query, but it is terribly inefficient on larger datasets.
SELECT w1.id, w1.status, w2.status, w3.status
FROM
(SELECT s.id, s.status
FROM score s
WHERE s.week = 1) w1
LEFT JOIN
(SELECT s.id, s.status
FROM score s
WHERE s.week = 2) w2 ON w1.id=w2.id
LEFT JOIN
(SELECT s.id, s.status
FROM score s
WHERE s.week = 3) w3 ON w1.id=w3.id
WHERE w1.status=0 AND w2.status=0 AND w3.status=1
I am looking for a more efficient way to calculate the above.
select id
from score
where week in (1, 2, 3)
group by id
having sum(
case
when week in (1, 2) and status = 0 then 1
when week = 3 and status = 1 then 1
else 0
end
) = 3
Or more generically...
select id
from score
group by id
having
sum(case when status = 0 then 1 else 0 end) = count(*) - 1
and min(case when status = 1 then week else null end) = max(week)
You can do using not exists as
select
t1.id,
'0' as `w1_status` ,
'0' as `w2_status`,
'1' as `w3_status`
from score t1
where
t1.week = 3
and t1.status = 1
and not exists(
select 1 from score t2
where t1.id = t2.id and t1.week <> t2.week and t2.status = 1
);
For better performance you can add index in the table as
alter table score add index week_status_idx (week,status);
In case of static number of weeks (1-3), group_concat may be used as a hack..
Concept:
SELECT
id,
group_concat(status) as totalStatus
/*(w1,w2=0,w3=1 always!)*/
FROM
tableName
WHERE
totalStatus = '(0,0,1)' /* w1=0,w2=1,w3=1 */
GROUP BY
id
ORDER BY
week ASC
(Written on the go. Not tested)
SELECT p1.id, p1.status, p2.status, p3.status
FROM score p1
JOIN score p2 ON p1.id = p2.id
JOIN score p3 ON p2.id = p3.id
WHERE p1.week = 1
AND p1.status = 0
AND p2.week = 2
AND p2.status = 0
AND p3.week = 3
AND p3.status = 1
Try this, should work

mysql sort group by total and name not working

I have Php program that outputs names with the corresponding events attended and the number of times each event was attended over a period of time. As an example of the output
Name | Run | Swim | Bike | Total
John 3 2 5 10
MySQL query look something like this:
$sql = 'SELECT
e.name as Leader,
SUM(CASE WHEN c.catid = 26 THEN 1 ELSE null END) as "Swim",
SUM(CASE WHEN c.catid = 25 THEN 1 ELSE null END) as "Bike",
SUM(CASE WHEN c.catid = 24 THEN 1 ELSE null END) as "Run",
COUNT("Swim"+"Bike"+"Run") as total
FROM
events as e
LEFT JOIN event_categories as c ON c.uid = e.uid
WHERE
(DATE(e.event_start) BETWEEN "'.$from_date.'" and "'.$to_date.'")
GROUP BY Leader WITH ROLLUP;';
This works well, however, if I want to sort my data by "total" in descending order I get no output if I replace the last GROUP BY line with the following:
GROUP BY total DESC, Leader WITH ROLLUP;';
so that I get a listing with names who have the highest totals to the lowest, and people with the same totals get listed in alphabetical order. What am I doing wrong?
As mentioned in the comments, the ORDER BY and ROLLUP can not be used together. It states this here (http://dev.mysql.com/doc/refman/5.0/en/group-by-modifiers.html) about half way down the page. To get around this, you'll have to do the ORDER BY in another query where your original query acts as the subquery:
SELECT *
FROM
(
SELECT
e.name as Leader,
SUM(CASE WHEN c.catid = 26 THEN 1 ELSE null END) as "Swim",
SUM(CASE WHEN c.catid = 25 THEN 1 ELSE null END) as "Bike",
SUM(CASE WHEN c.catid = 24 THEN 1 ELSE null END) as "Run",
COUNT("Swim"+"Bike"+"Run") as total
FROM
events as e
LEFT JOIN event_categories as c ON c.uid = e.uid
WHERE
(DATE(e.event_start) BETWEEN "'.$from_date.'" and "'.$to_date.'")
GROUP BY Leader WITH ROLLUP
) as rolldup
ORDER BY Total DESC
ORIGINAL (WRONG) ANSWER:
You do not put Sorts in a GROUP BY clause. You put them in your ORDER BY clause:
$sql = 'SELECT
e.name as Leader,
SUM(CASE WHEN c.catid = 26 THEN 1 ELSE null END) as "Swim",
SUM(CASE WHEN c.catid = 25 THEN 1 ELSE null END) as "Bike",
SUM(CASE WHEN c.catid = 24 THEN 1 ELSE null END) as "Run",
COUNT("Swim"+"Bike"+"Run") as total
FROM
events as e
LEFT JOIN event_categories as c ON c.uid = e.uid
WHERE
(DATE(e.event_start) BETWEEN "'.$from_date.'" and "'.$to_date.'")
GROUP BY Leader WITH ROLLUP
ORDER BY total DESC;';
You don't want to GROUP BY Total you just want to ORDER BY total.
So the two last lines of your query should be
GROUP BY Leader WITH ROLLUP
ORDER BY total DESC