【问题标题】:MySQL query to get total percentage changeMySQL查询以获取总百分比变化
【发布时间】:2019-08-12 19:37:30
【问题描述】:

如何在 MySQL 中添加百分比变化列(不是percentage points)?

有一个表格,其中列有百分比变化:

+---------+
| percent |
+---------+
|   -0.50 |
|    0.50 |
|    1.00 |
|   -0.20 |
|    0.50 |
|   -1.00 |
|   -2.00 |
|    0.75 |
|    1.00 |
|    0.50 |
+---------+

如何编写一个查询来计算每行值的总百分比变化,以便计算出的行表示其百分比变化和所有先前的百分比变化行?

预期结果:

+---------+---------------+---------------+
| percent | nominal_value | total_percent |
+---------+---------------+---------------+
|   -0.50 |          0.50 |         -0.50 |
|    0.50 |          0.75 |         -0.25 |
|    1.00 |          1.50 |          0.50 |
|   -0.20 |          1.20 |          0.20 |
|    0.50 |          1.80 |          0.80 |
|   -1.00 |          0.00 |         -1.00 |
|   -2.00 |         -2.00 |         -3.00 |
|    0.75 |         -0.50 |         -1.50 |
|    1.00 |          0.00 |         -1.00 |
|    0.50 |          0.50 |         -0.50 |
+---------+---------------+---------------+

nominal_value 是由percent 更改的任意值,因此对于第一行,如果标称值为 1.0 (100%) 但被-0.50 (-50%) 更改,则导致标称值0.5

然后在第二行 percent 变化是 +0.50 (+50%) 所以名义价值增加了​​一半 0.5 => 0.75 但也可以说它只是降低了 -0.25 (@ 987654334@) 从其原始值从1.00.751.0-0.25 (-25%)。

这正是我在更改total_percent 之后的内容,nominal_value 仅用于说明目的,不需要。

我使用的是 MySQL 8,因此查询可能会使用窗口函数/范围等。

这里是要复制的测试表:

CREATE TABLE IF NOT EXISTS test
(
    percent DECIMAL(5,2) NOT NULL
)
ENGINE = InnoDB
;

INSERT INTO test (percent) VALUES 
(-0.50)
,(0.50)
,(1.00)
,(-0.20)
,(0.50)
,(-1.0)
,(-2.0)
,(0.75)
,(1.0)
,(0.50)
;

【问题讨论】:

  • 你需要一个主键
  • 我看不出你如何能从 0 的标称值到 -2 的标称值。一旦某物为 0,该值的任何百分比变化都会给出 0。
  • 在数据的第一行和最后一行,nominal_value 是 0.5。但是在第一种情况下total_percent 是-0.5,在最后一种情况下是+0.5。这怎么可能?
  • @Nick 例如,您的银行账户有 100 美元的余额并花费了 100%,所以结果是余额 = 0 美元,但是因为您的账户有贷款能力,所以您再花 100 美元购买相同的东西天,所以你最终得到 -$100 的余额,这反映在初始余额的 -200% 和 -100% 从 $0 进入部门。另一个例子是 sinus 函数值的变化,它从 -1 到 0 到 +1 并回到 -1 穿过 0。如果你停在 0 处,那么你将无法以百分比表示它的上升或下降时间。
  • @jimmix 如果您没有某种唯一标识行的方法,那么您实际上就没有表格。没有一个,您可能会意外地得出符合您期望的结果,但这只是盲目的运气。

标签: mysql percentage calculation


【解决方案1】:
DROP TABLE IF EXISTS test;

CREATE TABLE test
( id SERIAL PRIMARY KEY
, percent DECIMAL(5,2) NOT NULL
);

INSERT INTO test (percent) VALUES 
(-0.5)
,(0.5)
,(1)
,(-0.2)
,(0.5)
,(-1)
;

SELECT ROUND(@i:=(@i+(@i*percent)),2)n 
  FROM test
     , (SELECT @i:=1) vars 
 ORDER 
    BY id;
+------+
| n    |
+------+
| 0.50 |
| 0.75 |
| 1.50 |
| 1.20 |
| 1.80 |
| 0.00 |
+------+
6 rows in set (0.00 sec)

mysql>

【讨论】:

    【解决方案2】:

    此查询将为您提供所需的结果。它使用两个 CTE,第一个简单地将行号添加到数据中,第二个递归 CTE 从当前 percent 和前面的 nominal_value 生成 nominal_value 值(其中前面由行号定义)。最后total_percent 是从nominal_value 计算出来的。

    注意

    为了使这个(和任何类似的)查询可靠地工作,必须有一个PRIMARY KEY,第一个 CTE 可以对其结果进行排序。在演示中,我为此添加了一个AUTO_INCREMENT INTid

    WITH RECURSIVE cte AS (
      SELECT percent, ROW_NUMBER() OVER () AS rn
      FROM test
      ORDER BY id),
    cte2 AS (
      SELECT 1 + percent AS nominal_value, rn
      FROM cte
      WHERE rn = 1
      UNION ALL
      SELECT CASE WHEN nominal_value = 0 THEN percent
                  ELSE nominal_value + percent * ABS(nominal_value)
                  END,
             cte.rn
      FROM cte
      JOIN cte2 ON cte2.rn = cte.rn - 1
      )
    SELECT percent, nominal_value, (nominal_value - 1) AS total_percent
    FROM cte2
    JOIN cte ON cte.rn = cte2.rn
    

    输出:

    percent nominal_value   total_percent
    -0.5    0.5             -0.5
    0.5     0.75            -0.25
    1       1.5             0.5
    -0.2    1.2             0.2
    0.5     1.8             0.8
    -1      0               -1
    -2      -2              -3
    0.75    -0.5            -1.5
    1       0               -1
    0.5     0.5             -0.5
    

    Demo on dbfiddle

    【讨论】:

    • 它在问题提供的数据上运行良好,但是在更大的数据集上进行测试时出现此错误ERROR 3636 (HY000): Recursive query aborted after 1001 iterations. Try increasing @@cte_max_recursion_depth to a larger value. 在查询中使用递归是不可避免的。可能是因为缺少索引?
    • @Jimmix 是的,由于创建 nominal_value 值的迭代过程,您需要使用递归查询。但是,将该变量设置得足够大以覆盖表中的所有行并没有什么坏处。
    • 我想避免将此变量设置得足够高,因为如果我正确理解其背后的原因,它可以防止近乎无限运行的写得不好的查询。我试图重写您的查询,但失败了。使用 LAG() / 临时表 / 变量的组合可以解决吗?
    • @Jimmix 您可以随时设置执行时间限制。见dev.mysql.com/doc/refman/8.0/en/…。使用变量的问题是它们在 MySQL 8.0 中已被弃用,并将在以后的版本中删除。
    • 如果test 表有一个附加列,其唯一日期是主键,那么是否可以编写一个给出相同结果但不使用变量也不重复的查询?
    【解决方案3】:

    这是接受答案的一个小变化,因为 OP 编辑​​了帖子并在接受答案后添加了额外的数据行和想要的结果。已发布并接受:

    查询:

    DROP TABLE IF EXISTS test;
    
    CREATE TABLE test
    ( 
     id SERIAL PRIMARY KEY
     , percent DECIMAL(5,2) NOT NULL
    );
    
    INSERT INTO test (percent) VALUES 
    (-0.50)
    ,(0.50)
    ,(1.00)
    ,(-0.20)
    ,(0.50)
    ,(-1.0)
    ,(-2.0)
    ,(0.75)
    ,(1.0)
    ,(0.50)
    ;
    
    SELECT 
        percent,
    
        CASE @i 
            WHEN 0 THEN ROUND(@i:=(@i+(percent * 1)),2) -1
            ELSE ROUND(@i:=(@i+(percent * ABS(@i))) ,2) -1
        END total_percent
    
    FROM 
        test
        , (SELECT @i:=1) vars         
    ORDER 
        BY id; 
    

    结果:

    +---------+---------------+
    | percent | total_percent |
    +---------+---------------+
    |   -0.50 |         -0.50 |
    |    0.50 |         -0.25 |
    |    1.00 |          0.50 |
    |   -0.20 |          0.20 |
    |    0.50 |          0.80 |
    |   -1.00 |         -1.00 |
    |   -2.00 |         -3.00 |
    |    0.75 |         -1.50 |
    |    1.00 |         -1.00 |
    |    0.50 |         -0.50 |
    +---------+---------------+
    10 rows in set, 3 warnings (0.00 sec)
    

    请注意,接受的答案在达到零标称值后停止计算,然后无论百分比变化都没有区别,标称值相同 = 0。在某些情况下,这可能是正确的方法。对于其他人来说,如果您使用 MySQL 8,则这里会继续计算为零或@Nick 答案。

    【讨论】:

      【解决方案4】:

      计算此数据的另一种方法是使用存储过程。这种方法的优点是它不需要递归 CTE 或变量,但缺点是使用结果可能很棘手(例如在 JOIN 中)。此过程在返回结果之前创建一个临时表来存储结果;如果需要进一步处理,可以保留该表,而不是在过程结束时保留DROPped。与其他答案一样,此方法要求数据具有PRIMARY KEY 以保证结果一致。

      DELIMITER //
      CREATE PROCEDURE total_percent()
      BEGIN
        DECLARE nominal_value DECIMAL(10,2) DEFAULT 1;
        DECLARE this_percent DECIMAL(5,2);
        DECLARE done INT DEFAULT 0;
        DECLARE p_cursor CURSOR FOR SELECT percent FROM test ORDER BY id;
        DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
        CREATE TEMPORARY TABLE p (percent DECIMAL(5, 2),
                                  nominal_value DECIMAL(10, 2),
                                  total_percent DECIMAL(10, 2));
        OPEN p_cursor;
        compute: LOOP
          FETCH p_cursor INTO this_percent;
          IF done THEN
            LEAVE compute;
          END IF;
          IF nominal_value = 0 THEN
            SET nominal_value = this_percent;
          ELSE
            SET nominal_value = nominal_value + this_percent * ABS(nominal_value);
          END IF;
          INSERT INTO p VALUES (this_percent, nominal_value, nominal_value -1);
        END loop;
        SELECT * FROM p;
        DROP TABLE p;
      END //
      DELIMITER ;
      
      CALL total_percent();
      

      输出:

      percent  nominal_value   total_percent
      -0.5     0.5             -0.5
      0.5      0.75            -0.25
      1        1.5             0.5
      -0.2     1.2             0.2
      0.5      1.8             0.8
      -1       0               -1
      -2       -2              -3
      0.75     -0.5            -1.5
      1        0               -1
      0.5      0.5             -0.5
      

      Demo on dbfiddle

      【讨论】:

      • 很高兴看到 :) 我会检查它是如何工作的,有趣的是你提到了 JOIN 的问题,因为现在我正在使用 CTE 与你的 ans 一起工作,并与其他表一起做 JOIN :) 有什么难的使用 JOIN 和这样的过程?
      • @Jimmix 如果要使用存储过程,请给临时表一个更有用的名称,并从末尾删除SELECT *DROP 语句。然后就可以在其他查​​询中使用临时表了。
      • @Jimmix 对于带有 CTE 的 JOINing,将最后一个 SELECT 包装为另一个 CTE,即 , cte3 AS (SELECT percent, nominal_value, (nominal_value - 1) AS total_percent FROM cte2 JOIN cte ON cte.rn = cte2.rn),稍后在您的 JOIN 语句中使用它会相当容易查询。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-01-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-25
      • 2015-10-27
      相关资源
      最近更新 更多