Reputation: 79
I am building a report and using user defined variables to keep the query as compact as possible. What I am noticing in MySQL Workbench and in PHP is that they don't always work properly.
For example:
SELECT
@NewSales := SUM(CASE WHEN `v`.`new_used`='N' THEN 1 ELSE 0 END) AS `NewSales`,
@UsedSales := SUM(CASE WHEN `v`.`new_used`='U' THEN 1 ELSE 0 END) AS `UsedSales`,
@UsedSales + @NewSales AS `TotalSales`
FROM `fi_sales` `s`
LEFT JOIN `vehicles` `v` ON `v`.`stock_number`=`s`.`stock_number`
If I run the above query in Workbench, the first run outputs TotalSales=NULL:
NewSales, UsedSales, TotalSales
3418, 2889, NULL
If I refresh the query, the output produces expected result for TotalSales:
NewSales, UsedSales, TotalSales
3418, 2889, 6307.000000000000000000000000000000
A bit strange; almost as if the variable is not usable in the same query that sets it. I usually work around it by reproducing the calculation without using variables.
My next problem is that if I copy the same query from Workbench into my application (PHP) the TotalSales output will produce "0" zero.
I am sure there is a perfectly good explanation what is going on here but I am having trouble finding it. Any answers are greatly appreciated.
Upvotes: 1
Views: 436
Reputation: 179194
You're in non-deterministic territory using user-defined variables in a query that changes them, and the explanation is straightforward: the answer you get is actually from the previous run of the same query.
UDVs are scoped to your individual database connection. Their values persist between queries, but not across connections. This query is giving you the value of @UsedSales + @NewSales
from before the query is run, not after. (Why? Because it just is. There is not a reason... it could go either way. See below.)
SET @UsedSales = 1, @NewSales = 2;
and run your query again. Total
will be 3 in the next run, clearly the wrong answer (compared to what you expected) but not wrong in the sense that the server is free to resolve these in any order it likes, because they look like constants.
As a general rule, other than in SET statements, you should never assign a value to a user variable and read the value within the same statement.
...
For other statements, such as SELECT, you might get the results you expect, but this is not guaranteed.
...
The order of evaluation for expressions involving user variables is undefined.
You're trying to solve a problem that isn't really a problem.
Instead, either do this:
SELECT
SUM(CASE WHEN `v`.`new_used`='N' THEN 1 ELSE 0 END) AS `NewSales`,
SUM(CASE WHEN `v`.`new_used`='U' THEN 1 ELSE 0 END) AS `UsedSales`,
SUM(CASE WHEN `v`.`new_used` IN ('N','U') THEN 1 ELSE 0 END) AS `TotalSales`
FROM `fi_sales` `s`
LEFT JOIN `vehicles` `v` ON `v`.`stock_number`=`s`.`stock_number`;
Or if you insist, make a derived table (here, named x
) and do the extra addition of the resulting columns.
SELECT
x.NewSales,
x.UsedSales,
x.NewSales + x.UsedSales AS TotalSales
FROM (
SELECT
SUM(CASE WHEN `v`.`new_used`='N' THEN 1 ELSE 0 END) AS `NewSales`,
SUM(CASE WHEN `v`.`new_used`='U' THEN 1 ELSE 0 END) AS `UsedSales`
FROM `fi_sales` `s`
LEFT JOIN `vehicles` `v` ON `v`.`stock_number`=`s`.`stock_number`
) x;
This materializes the inner result into a temporary table that is discarded as soon as the query finishes executing.
Or, if you really want clever and short, go with this:
SELECT
COUNT(`v`.`new_used`='N' OR NULL) AS `NewSales`,
COUNT(`v`.`new_used`='U' OR NULL) AS `UsedSales`,
COUNT(`v`.`new_used` IN ('N','U') OR NULL) AS `TotalSales`
FROM `fi_sales` `s`
LEFT JOIN `vehicles` `v` ON `v`.`stock_number`=`s`.`stock_number`;
This works, because COUNT()
counts only rows with non-null arguments, and any expression expr OR NULL
coerces expr
to be evaluated as a boolean expression, and thus is logically equivalent to CASE WHEN expr [IS TRUE] THEN 1 ELSE NULL END
and thus can only evaluate to 1
(if expr
is truthy)... or NULL
(if expr
is either false or null)... which an exact match for how COUNT()
works.
Upvotes: 2