MySQL 数学查询问题未产生预期输出

MySQL Query Issues with Math not resulting in expected output

概览:

我在本地 运行 构建了一个应用程序,它允许我跟踪孩子们每天的家务和行为。该系统具有我可以分配给它们的消极和积极行为,这些行为对应于 100 点量表上的点值。

逻辑:

场景:

  1. 没有任何收视率的新一天意味着 child 从 100 点开始。他们收到具有 -3 值的负面行为。这将 return 他们的 totalPoints 作为 97.
  2. 以上 child 获得正面评价,价值 2 分,这使他们的 totalPoints 达到 99
  3. 他们又获得了 5 分的好评。由于我们的最大值为 100,因此我们会 return 他们的 totalPoints100,而不管它超出了多少 100

问题:

我构建了查询并认为一切正常,但它似乎存在一个轻微的数学问题。当 child 获得 -3 点评级时,他们达到了预期的 97。然后我给了他们一个积极的 4,这使他们的分数达到 99 而不是我预期的 100

查询:

 SELECT c.id,
       c.NAME,
       Date_format(From_days(Datediff(CURRENT_DATE, c.age)),
       '%y Years %m Months %d Days')                                 AS age,
       c.photoname,
       c.photonamesmall,
       (SELECT CASE
                 WHEN ( Ifnull(Sum(t.points), (SELECT settingvalue
                                               FROM   settings
                                               WHERE  settingname = 'MaxPoints')
                        ) >= (
                        SELECT
                               settingvalue
                        FROM
                        settings
                        WHERE
                                 settingname = 'MaxPoints') ) THEN 100
                 WHEN ( Sum(t.points) <= 0 ) THEN ( (SELECT settingvalue
                                                     FROM   settings
                                                     WHERE  settingname =
                                                            'MaxPoints')
                                                    + Sum(t.points) )
                 ELSE ( (SELECT settingvalue
                         FROM   settings
                         WHERE  settingname = 'MaxPoints') -
                        Ifnull(Sum(t.points), (SELECT
                        settingvalue
                                               FROM   settings
                                               WHERE
                        settingname = 'MaxPoints')) )
               END
        FROM   behaviorratings AS r
               JOIN behaviortypes AS t
                 ON r.behaviorid = t.behaviortypeid
        WHERE  r.childid = c.id
               AND Date_format(r.timestamp, '%Y-%m-%d') = Curdate()) AS
       totalPoints,
       (SELECT definitionname
        FROM   behaviordefinitions AS d
        WHERE  totalpoints BETWEEN d.min AND d.max)                  AS
       behaviorRating
FROM   children AS c  

Fiddle:

这里是 link 到 SQL fiddle: http://sqlfiddle.com/#!9/fa06c/1/0

我希望看到 Child 2 (Brynlee) 的结果是 100 而不是 99

她从 100 开始,收到 -3 并收到 +4。虽然我知道这个操作顺序的数学是正确的,但我需要对其进行调整以反映我期望的反映方式。 100 - 3 = 97 然后是 97 + 4 = 101(我们在 100 处达到最大值,所以 100 将是 totalPoints

试试这个

SELECT c.id,
   c.name,
   DATE_FORMAT(
    FROM_DAYS(
        DATEDIFF(CURRENT_DATE, c.age)
    ),
    '%y Years %m Months %d Days'
  ) AS age,
   c.photoName,
   c.photoNameSmall,
   (SELECT CASE
             WHEN ( Ifnull(Sum(t.points), 0
                    ) + (SELECT settingValue
                                           FROM   settings
                                           WHERE  settingName = 'MaxPoints') >= (
                    SELECT
                           settingValue
                    FROM
                    settings
                    WHERE
                             settingName = 'MaxPoints') ) THEN 100
             WHEN ( Sum(t.points) <= 0 ) THEN ( (SELECT settingValue
                                                FROM   settings
                                                WHERE  settingName =
                                                       'MaxPoints')
                                               + Sum(t.points) )
             ELSE ( (SELECT settingValue
                     FROM   settings
                     WHERE  settingName = 'MaxPoints') -
                    Ifnull(Sum(t.points), (SELECT
                    settingvalue
                                           FROM   settings
                                           WHERE
                    settingName = 'MaxPoints')) )
           END
    FROM   behaviorRatings AS r
           JOIN behaviorTypes AS t
             ON r.behaviorID = t.behaviorTypeID
    WHERE  r.childid = c.id
           AND Date_format(r.timestamp, '%Y-%m-%d') = Curdate()) AS
   totalPoints,
   (SELECT definitionName
    FROM   behaviorDefinitions AS d
    WHERE  totalPoints BETWEEN d.min AND d.max)                  AS
   behaviorRating
   FROM   children AS c

基本上,使用

WHEN ( Ifnull(Sum(t.points), (SELECT settingvalue
                              FROM   settings
                              WHERE  settingname = 'MaxPoints')
)
当 sum(t.points) 为 null 时,

只会给你 100。要获得总分,您需要做

Ifnull(Sum(t.points), 0) + (SELECT settingvalue
                              FROM   settings
                              WHERE  settingname = 'MaxPoints')

这个sql可能更容易看

SET @maxPoints := (SELECT settingValue
              FROM   settings
              WHERE  settingName = 'MaxPoints');

SELECT c.id,
   c.name,
   DATE_FORMAT(
    FROM_DAYS(
        DATEDIFF(CURRENT_DATE, c.age)
    ),
    '%y Years %m Months %d Days'
  ) AS age,
   c.photoName,
   c.photoNameSmall,
   (SELECT CASE
             WHEN ( Ifnull(Sum(t.points), 0) + @maxPoints > @maxPoints ) THEN 100
             ELSE ( Ifnull(Sum(t.points), 0) + @maxPoints )
           END
    FROM   behaviorRatings AS r
           JOIN behaviorTypes AS t
             ON r.behaviorID = t.behaviorTypeID
    WHERE  r.childid = c.id
           AND Date_format(r.timestamp, '%Y-%m-%d') = Curdate()) AS
   totalPoints,
   (SELECT definitionName
    FROM   behaviorDefinitions AS d
    WHERE  totalPoints BETWEEN d.min AND d.max)                  AS
   behaviorRating
   FROM   children AS c

以 50 为起点:

SET @maxPoints := (SELECT settingValue
          FROM   settings
          WHERE  settingName = 'MaxPoints');

SET @startingPoint := 50;

SELECT c.id,
c.name,
DATE_FORMAT(
FROM_DAYS(
    DATEDIFF(CURRENT_DATE, c.age)
),
'%y Years %m Months %d Days'
) AS age,
c.photoName,
c.photoNameSmall,
(SELECT CASE
         WHEN ( Ifnull(Sum(t.points), 0) + @startingPoint > @maxPoints ) THEN 100
         ELSE ( Ifnull(Sum(t.points), 0) + @startingPoint )
       END
FROM   behaviorRatings AS r
       JOIN behaviorTypes AS t
         ON r.behaviorID = t.behaviorTypeID
WHERE  r.childid = c.id
       AND Date_format(r.timestamp, '%Y-%m-%d') = Curdate()) AS
totalPoints,
(SELECT definitionName
FROM   behaviorDefinitions AS d
WHERE  totalPoints BETWEEN d.min AND d.max)                  AS
behaviorRating
FROM   children AS c

Sql 总点数超过限制后申请封顶

SET @maxPoints := (SELECT settingValue
            FROM   settings
            WHERE  settingName = 'MaxPoints');

SET @startingPoint := 50;

SELECT 
    c.id,
    c.name,
    DATE_FORMAT(
    FROM_DAYS(DATEDIFF(CURRENT_DATE, c.age)), '%y Years %m Months %d Days') AS age,
    c.photoName,
    c.photoNameSmall,
    (
        select x.tp 
        from 
        (
            SELECT t.childid,
                @rn:=CASE WHEN @cid <> t.childid THEN 0 ELSE @rn+1 END AS rn,
                @startingPoint + @tp:= CASE 
                    WHEN @cid <> t.childid 
                    THEN ifnull(t.points, 0)
                    ELSE (
                        case when @startingPoint + t.points + @tp > @maxPoints 
                        then @maxPoints - @startingPoint
                        else t.points + @tp end)
                    END AS tp,
                @cid:=t.childid AS clset,
                t.timestamp
            FROM
                (SELECT @tp:= -1) p,
                (SELECT @rn:= -1) n,
                (SELECT @cid:= -1) cd,
                (
                    SELECT r.childid, t.points, r.timestamp
                    FROM behaviorRatings AS r
                    JOIN behaviorTypes AS t ON r.behaviorID = t.behaviorTypeID
                    ORDER BY r.childid, r.timestamp
                ) t
        ) x
        where x.childid = c.id AND Date_format(x.timestamp, '%Y-%m-%d') = Curdate()
        order by x.childid, x.rn desc
        limit 1
    ) AS totalPoints,
    (
        SELECT definitionName
        FROM   behaviorDefinitions AS d
        WHERE  totalPoints BETWEEN d.min AND d.max
    ) AS behaviorRating
FROM   children AS c

不要让事情变得更复杂。为您的任务选择正确的语言。在你的情况下是 PHP:

$query = "select settingValue from settings where settingName = 'MaxPoints'";
$result = $this->db->query($query);
$row = $result->fetchAssoc();
$maxPoints = $row['settingValue'];

$query = "select * from children";
$result = $this->db->query($query);
$children = array();
while ($row = $result->fetchAssoc()) {
    $row['totalPoints'] = $maxPoints;
    $children[$row['id']] = $row;
}

$query = "
    select c.id, coalesce(bt.points, 0) as points
    from children c
    join behaviorRatings br on  br.childID = c.id
    join behaviorTypes bt on bt.behaviorTypeID = br.behaviorID
    where date(br.timestamp) = current_date()
    order by c.id, br.timestamp
";

$result = $this->db->query($query);
while ($row = $result->fetchAssoc()) {
    $childId = $row['id'];
    $totalPoints = $children[$row['id']]['totalPoints'];
    $totalPoints = $totalPoints + $row['points'];
    $totalPoints = min($totalPoints, $maxPoints);
    $children[$row['id']]['totalPoints'] = $totalPoints;
}

var_dump($children);

所有获取总分的逻辑都在最后一个循环中。现在将其与您的查询进行比较。

但是 - 如果您更改规则,允许在一天中超过限制并仅在一天结束时削减点数,这可以在单个查询中完成:

select c.*, sub.totalPoints, bd.definitionName
from (
  select c.id, least(100+coalesce(sum(bt.points), 0), mp.settingValue) as totalPoints
  from children c
  join settings mp on settingName = 'MaxPoints'
  left join behaviorRatings br
    on  br.childID = c.id
    and date(br.timestamp) = current_date()
  left join behaviorTypes bt on bt.behaviorTypeID = br.behaviorID
  group by c.id
) sub
join children c on c.id = sub.id
join behaviorDefinitions bd on sub.totalPoints between bd.min and bd.max

http://sqlfiddle.com/#!9/fa06c/71

虽然这不是一个简单的查询,但它远没有您尝试的那么复杂。公认的解决方案是忽略规则,即每次获得点数时总点数必须减少到 100 (Sum(t.points))。

正如我在评论中所写:要遵循该规则,您需要进行某种迭代。 MySQL 中有一个使用用户变量的技巧:

select c.id, c.name, sub.totalPoints, bd.definitionName
from (
  select sub.childId, sum(sub.cuttedPoints) + sp.settingValue as totalPoints
  from (
    select 
      @points := coalesce(bt.points,0) as points,
      @lastTotalPoints := case when (c.id = @childId)
        then @totalPoints
        else sp.settingValue
      end lastTotalPoints,
      @totalPoints := least(@lastTotalPoints + @points, mp.settingValue) as totalPoints,
      @totalPoints - @lastTotalPoints as cuttedPoints,
      @childId := c.id as childId
    from children c
    join settings sp on sp.settingName = 'StartPoints'
    join settings mp on mp.settingName = 'MaxPoints'
    left join behaviorRatings br 
      on  br.childID = c.id
      and date(br.timestamp) = current_date()
    left join behaviorTypes bt on bt.behaviorTypeID = br.behaviorID
    cross join (select @childId := null) init_var
    order by c.id, br.timestamp
  ) sub
  join settings sp on sp.settingName = 'StartPoints'
  group by sub.childId
) sub
join children c on c.id = sub.childId
join behaviorDefinitions bd on sub.totalPoints between bd.min and bd.max

结果(Brynlee 行为:+4 -3 +4 -3):

| id |    name | totalPoints |         definitionName |
|----|---------|-------------|------------------------|
|  2 | Brynlee |          97 | Having an amazing day! |
|  1 |    Maya |         100 | Having an amazing day! |

Brynlee 获得 97 分(+4 => 100,-3 => 97,+4 => 100,-3 => 97)

http://sqlfiddle.com/#!9/751c51/28

如果您将新设置 "StartPoints" 更改为 50,您将获得:

| id |    name | totalPoints |   definitionName |
|----|---------|-------------|------------------|
|  2 | Brynlee |          52 | Not looking good |
|  1 |    Maya |          50 | Not looking good |

此处 Brynlee 得到 52 分,因为从未达到 100 的限制(+4 => 54,-3 => 51,+4 => 55,-3 => 52)。

http://sqlfiddle.com/#!9/db020/13

这是因为 MySQL 的处理顺序。但是这个顺序取决于 MySQL 的内部实现。此实现可能会在未来版本中更改,而不会发出任何警告。事实上 - MySQL 开发人员明确警告以这种方式使用用户变量。

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 example, to increment a variable, this is okay:

SET @a = @a + 1;

For other statements, such as SELECT, you might get the results you expect, but this is not guaranteed. In the following statement, you might think that MySQL will evaluate @a first and then do an assignment second:

SELECT @a, @a:=@a+1, ...;

However, the order of evaluation for expressions involving user variables is undefined.

(User-Defined Variables)

我只将 "tricks" 用于单向报告 - 但从不用于生产代码。

所以我的建议是:改变规则或使用程序语言(PHP)。