Subtracting the value from the last row using variable assignment in MySQL

人盡茶涼 提交于 2021-02-08 11:54:18

问题


According to the MySQL documentation:

As a general rule, you should never assign a value to a user variable and read the value within the same statement. You might get the results you expect, but this is not guaranteed.

http://dev.mysql.com/doc/refman/5.6/en/user-variables.html

However, in the book High Perfomance MySQL there are a couple of examples of using this tactic to improve query performance anyway.

Is the following an anti-pattern and if so is there a better way to write the query while maintaining good performance?

set @last = null;
select tick, count-@last as delta, @last:=count from measurement;

For clarification, my goal is to find the difference between this row and the last. My table has a primary key on tick which is a datetime column.

Update:

After trying Shlomi's suggestion, I have reverted back to my original query. It turns out that using a case statement with aggregate functions produces unexpected behavior. See for example:

case when (@delta := (max(measurement.count) - @lastCount)) AND 0 then null
when (@lastCount := measurement.count) AND 0 then null
else @delta end

It appears that mysql evaluates the expressions that don't contain aggregate functions on a first pass through the results, and then evaluates the aggregate expressions on a second (grouping) pass. It appears to evaluate the case expression during or after that second pass and use the precalculated values from the first pass in that evaluation. The result is that the third line @delta is always the initial value of @delta (because assignment didn't happen until the grouping pass). I attempted to incorporate a group function into the line with @delta but couldn't get it to behave as expected. So I ultimately when back to my original query which didn't have this problem.

I would still love to hear any more suggestions about how to better handle a query like this.

Update 2:

Sorry for the lack of response on this question, I didn't have a chance to investigate further until now.

Using Shlomi's solution it looks like I had a problem because I was using a group by function when I read my @last variable but not when I set it. My code looked something like this:

CASE
    WHEN (@delta := count - @last) IS NULL THEN NULL
    WHEN (@last:= count ) IS NULL THEN NULL
    ELSE (CASE WHEN cumulative THEN @delta ELSE avg(count) END)
END AS delta

MySQL appears to process expressions that don't contain aggregate functions in a first pass and ones that do in a second pass. The strange thing in the code above is that even when cumulative evaluates to true MySQL must see the AVG aggregate function in the ELSE clause and decides to evaluate the whole inner CASE expression in the second pass. Since @delta is set in an expression without an aggregate function it seems to be getting set on the first pass and by the time the second pass happens MySQL is done evaluating the lines that set @delta and @last.

Ultimately I seem to have found a fix by including aggregate functions in the first expressions as well. Something like this:

CASE
    WHEN (@delta := max(count) - @last) IS NULL THEN NULL
    WHEN (@last:= max(count) ) IS NULL THEN NULL
    ELSE (CASE WHEN cumulative THEN @delta ELSE avg(count) END)
END AS delta

My understanding of what MySQL is doing is purely based on testing and conjecture since I didn't read the source code, but hopefully this will help others who might run into similar problems.

I am going to accept Shlomi's answer because it really is a good solution. Just be careful how you use aggregate functions.


回答1:


I've researched this issue in depth, and wrote a few improvements on the above.

I offer a solution in this post, which uses functions whose order can be expected. Also consider my talk last year.

Constructs such as CASE and functions such as COALESCE have known underlying behavior (at least until this is changed, right?).

For example, a CASE clause inspects the WHEN conditions one by one, by order of definition.

Consider a rewrite of the original query:

select 
  tick,
  CASE
    WHEN (@delta := count-@last) IS NULL THEN NULL
    WHEN (@last:=count ) IS NULL THEN NULL
    ELSE @delta
  END AS delta
from 
  measurement,
  (select @last := 0) s_init
;

The CASE clause has three WHEN conditions. It executes them by order until it meets the first that succeeds. I've written them such that the first two will always fail. It therefore executes the first, then turns to execute the second, then finally returns the third. Always.

I thus overcome the problem of expecting order of evaluation, which is a real and true problem, mostly evident when you start adding more complex clauses such as GROUP BY, DISTINCT, ORDER BY and such.

As a final note, my solution differs from yours in the first row on the result set -- with yours' it returns NULL, with mine it returns the delta between 0 and count. Had I used NULL I would have needed to change the WHEN conditions in some other way -- making sure they would fail on NULL values.



来源:https://stackoverflow.com/questions/11589439/subtracting-the-value-from-the-last-row-using-variable-assignment-in-mysql

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!