Запрос MySQL для получения общего процентного изменения

Как добавить столбец процентного изменения (не процентных точек) в MySQL?

есть таблица с колонкой изменения процентов:

+---------+
| 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 (-25%) от исходного значения, поскольку с 1.0 до 0.75 это -0.25 (-25%) из 1.0.

Это именно то, что я сделал после изменения 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)
;

person Jimmix    schedule 21.03.2019    source источник
comment
Вам нужен ПЕРВИЧНЫЙ КЛЮЧ   -  person Strawberry    schedule 22.03.2019
comment
Я не понимаю, как вы можете получить от nominal_value, равное 0, до nominal_value, равное -2. Если значение равно 0, любое процентное изменение этого значения даст 0.   -  person Nick    schedule 22.03.2019
comment
В первой и последней строках ваших данных nominal_value равно 0,5. Однако в первом случае total_percent равно -0,5, а в последнем случае +0,5. Как это может быть?   -  person Nick    schedule 22.03.2019
comment
@Nick, например, у вас есть баланс в размере 100 долларов на вашем банковском счете, и вы тратите 100% из них, поэтому в результате баланс = 0 долларов, но поскольку у вашей учетной записи есть возможность ссуды, вы тратите еще 100 долларов на покупки в тот же день, так что в итоге вы получаете - 100 долларов баланс, который отражается -200% от суммы начального баланса и -100%, идущим в долг с $ 0. Другим примером может быть изменение значения синусовой функции, которое идет от -1 через 0 до +1 и обратно до -1, пересекая 0. Если вы остановились на 0, вы не смогли бы выразить его повышение или понижение в процентах. время.   -  person Jimmix    schedule 22.03.2019
comment
@Jimmix хорошо, я получаю часть с отрицательным значением, но как вы объясните разные значения total_percent для одного и того же значения nominal_value? (и 0, и 0,5 - последние две строки) имеют другой общий процент, чем для их первых вхождений   -  person Nick    schedule 22.03.2019
comment
@Nick, ты прав, спасибо, что указали на это. Я скорректировал ожидаемый результат и с другими просчетами.   -  person Jimmix    schedule 22.03.2019
comment
@Jimmix, хорошо, это соответствует тому, что я ожидал от результатов.   -  person Nick    schedule 22.03.2019
comment
@ Strawberry, ты можешь понять, почему? Я проверил ваш ответ с таблицей test без столбца id и даже удалил ORDER BY id и все равно получил тот же результат. Вы имели в виду производительность или в случае неиспользования PRIMARY KEY возникает другая проблема?   -  person Jimmix    schedule 23.03.2019
comment
@jimmix, если у вас нет способа однозначно идентифицировать строки, значит, у вас действительно нет таблицы. Без него вы можете случайно получить результат, соответствующий вашим ожиданиям, но это просто слепая удача.   -  person Strawberry    schedule 23.03.2019


Ответы (3)


Этот запрос даст вам желаемый результат. Он использует два CTE, первый из которых просто добавляет номер строки к данным, а второй, рекурсивный CTE, который генерирует значения nominal_value из текущего percent и предыдущего nominal_value (где предыдущее определяется номером строки). Наконец, total_percent вычисляется из nominal_value.

Примечание

Чтобы этот (и любой аналогичный) запрос работал надежно, должен быть PRIMARY KEY, по которому результаты первого CTE могут быть упорядочены. В демонстрации я добавил для этой цели AUTO_INCREMENT INT столбец id.

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

Демо на dbfiddle

person Nick    schedule 22.03.2019
comment
он хорошо работал с данными, предоставленными с вопросом, однако при тестировании на более крупном наборе данных возникла эта ошибка ERROR 3636 (HY000): Recursive query aborted after 1001 iterations. Try increasing @@cte_max_recursion_depth to a larger value. Использование рекурсии в запросе неизбежно. Возможно, из-за отсутствия индекса? - person Jimmix; 22.03.2019
comment
@Jimmix да, вам нужно использовать рекурсивный запрос из-за итеративного процесса создания значений nominal_value. Но нет ничего плохого в том, чтобы установить эту переменную достаточно большой, чтобы покрыть все строки в вашей таблице. - person Nick; 23.03.2019
comment
Я бы не хотел устанавливать для этой переменной достаточно высокое значение, потому что, если я правильно понимаю причину этого, это защита от почти бесконечного запуска плохо написанных запросов. Я пытался переписать ваш запрос, но не смог. Может ли решение использовать комбинацию LAG () / временной таблицы / переменных? - person Jimmix; 23.03.2019
comment
@Jimmix вы всегда можете установить лимит времени выполнения. См. dev.mysql.com /doc/refman/8.0/en/. Проблема с использованием переменных заключается в том, что они устарели в MySQL 8.0 и будут удалены в более поздней версии. - person Nick; 23.03.2019
comment
Если бы в таблице test был дополнительный столбец с уникальными датами, которые являются ПЕРВИЧНЫМ КЛЮЧОМ, то можно ли было бы написать запрос, который дает тот же результат, но не использует переменные или повторение? - person Jimmix; 23.03.2019
comment
@ Strawberry интересно - почему наличие индекса все меняет? Честно говоря, ваш запрос работает только потому, что вы добавили к нему первичный ключ auto_increment, и этот запрос тоже будет работать с ним ... - person Nick; 23.03.2019
comment
Это ИМЕННО моя точка зрения !! Без ПЕРВИЧНОГО КЛЮЧА это решение (любое решение) полностью зависит от прихоти сервера. - person Strawberry; 23.03.2019
comment
@Strawberry обновили мой ответ, чтобы подчеркнуть это и добавить первичный ключ в демонстрацию. Ранее в тот же день страдал от нехватки кофе, поэтому потребовалось так много времени, чтобы понять свою точку зрения. Спасибо. - person Nick; 23.03.2019
comment
@Nick изменил --cte-max-recursion -depth до максимального значения (4300M строк), должно быть достаточно на некоторое время :) Просто интересно, есть ли какой-либо переключатель, чтобы отключить это ограничение вообще, потому что я его не нашел. - person Jimmix; 23.03.2019
comment
@Jimmix Я не верю, что есть способ его выключить. Установка его в 0 исключает любую рекурсию вообще, а отрицательные значения не допускаются. Тем не менее, ограничение в 4300M должно поддерживать вас ... - person Nick; 24.03.2019
comment
@Jimmix просто для того, чтобы ваши варианты оставались открытыми, я опубликовал альтернативное решение с использованием хранимой процедуры. - person Nick; 24.03.2019

Альтернативный способ вычисления этих данных - использование хранимой процедуры. Преимущество этого подхода состоит в том, что он не требует рекурсивного CTE или переменных, но недостаток в том, что может быть сложно использовать результаты (например, в JOIN). Эта процедура создает временную таблицу для хранения результатов перед их возвратом; эту таблицу можно было бы сохранить, а не DROP в конце процедуры, если потребуется дальнейшая обработка. Как и в случае с другими ответами, этот подход требует, чтобы данные имели 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

Демо на dbfiddle

person Nick    schedule 24.03.2019
comment
Приятно видеть :) Я проверю, как это работает, забавно, что вы упомянули проблемы с JOIN, потому что именно сейчас я работаю с вашими ответами, используя CTE, и выполняю JOINs с другими таблицами :) Что такого сложного в использовании JOIN с такой процедурой? - person Jimmix; 24.03.2019
comment
@Jimmix Если вы хотите использовать хранимую процедуру, дайте временной таблице более удобное имя и удалите операторы SELECT * и DROP с конца. Затем вы можете использовать временную таблицу в других запросах. - person Nick; 24.03.2019
comment
@Jimmix Для JOINing с CTE, оберните этот последний SELECT как другой CTE, то есть , cte3 AS (SELECT percent, nominal_value, (nominal_value - 1) AS total_percent FROM cte2 JOIN cte ON cte.rn = cte2.rn), и его будет достаточно легко использовать в операторе JOIN позже в вашем запросе. - person Nick; 24.03.2019

Это небольшой вариант принятого ответа из-за того, что 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. В некоторых случаях это может быть правильным подходом. Для других вот тот, который продолжает вычисление через ноль или ответ @Nick, если вы используете MySQL 8.

person Jimmix    schedule 23.03.2019