Создать вычисленное значение на основе вычисленного значения в предыдущей строке

Я пытаюсь найти способ применять ежемесячные процентные изменения к прогнозируемым ценам. Я поставил свою проблему в Excel, чтобы сделать ее более понятной. Я использую SQL Server 2017.

Примеры данных таблицы цен

Мы скажем, что все месяцы до 1 сентября 18 являются историческими, а 18 января и последующие - прогнозами. Мне нужно рассчитать прогнозную цену (выделенную желтым цветом на выборке данных), используя ...

Forecast Price = (Previous Row Forecast Price * Pct Change) + Previous Row Forecast Price

Для ясности: цены, закрашенные желтым цветом, в моих данных пока отсутствуют. Это то, что я пытаюсь вычислить в моем запросе. Поскольку это изменение в процентах за месяц, каждая строка зависит от предыдущей строки и выходит за рамки одного решения ROW_NUMBER / PARTITION, потому что мы должны использовать предыдущую рассчитанную цену. Ясно, что то, что является простым последовательным вычислением в Excel, здесь немного сложнее. Есть идеи, как создать столбец прогнозируемой цены в SQL?


person jslater41    schedule 12.11.2018    source источник
comment
Откуда первые 150?   -  person Salman A    schedule 12.11.2018
comment
150 = 50% * 100 + 100. Цена с 01.01.18 плюс повышение цены на 50%.   -  person BenM    schedule 12.11.2018
comment
Я считаю, что вам нужно решить это с помощью рекурсии. LAG / SUM OVER здесь не поможет.   -  person Salman A    schedule 13.11.2018


Ответы (2)


Вам нужно использовать рекурсивный CTE. Это один из самых простых способов посмотреть значение вычисленного значения из предыдущей строки:

DECLARE @t TABLE(Date DATE, ID VARCHAR(10), Price DECIMAL(10, 2), PctChange DECIMAL(10, 2));
INSERT INTO @t VALUES
('2018-01-01', 'ABC', 100,    NULL),
('2018-01-02', 'ABC', 150,   50.00),
('2018-01-03', 'ABC', 130,  -13.33),
('2018-01-04', 'ABC', 120,  -07.69),
('2018-01-05', 'ABC', 110,  -08.33),
('2018-01-06', 'ABC', 120,    9.09),
('2018-01-07', 'ABC', 120,    0.00),
('2018-01-08', 'ABC', 100,  -16.67),
('2018-01-09', 'ABC', NULL, -07.21),
('2018-01-10', 'ABC', NULL,   1.31),
('2018-01-11', 'ABC', NULL,   6.38),
('2018-01-12', 'ABC', NULL, -30.00),
('2019-01-01', 'ABC', NULL,  14.29),
('2019-01-02', 'ABC', NULL,   5.27);

WITH ncte AS (
    -- number the rows sequentially without gaps
    SELECT *, ROW_NUMBER() OVER (PARTITION BY ID ORDER BY Date) AS rn
    FROM @t
), rcte AS (
    -- find first row in each group
    SELECT *, Price AS ForecastedPrice
    FROM ncte AS base
    WHERE rn = 1
    UNION ALL
    -- find next row for each group from prev rows
    SELECT curr.*, CAST(prev.ForecastedPrice * (1 + curr.PctChange / 100) AS DECIMAL(10, 2))
    FROM ncte AS curr
    INNER JOIN rcte AS prev ON curr.ID = prev.ID AND curr.rn = prev.rn + 1
)
SELECT *
FROM rcte
ORDER BY ID, rn

Результат:

| Date       | ID  |  Price | PctChange | rn | ForecastedPrice |
|------------|-----|--------|-----------|----|-----------------|
| 2018-01-01 | ABC | 100.00 |      NULL |  1 |          100.00 |
| 2018-01-02 | ABC | 150.00 |     50.00 |  2 |          150.00 |
| 2018-01-03 | ABC | 130.00 |    -13.33 |  3 |          130.01 |
| 2018-01-04 | ABC | 120.00 |     -7.69 |  4 |          120.01 |
| 2018-01-05 | ABC | 110.00 |     -8.33 |  5 |          110.01 |
| 2018-01-06 | ABC | 120.00 |      9.09 |  6 |          120.01 |
| 2018-01-07 | ABC | 120.00 |      0.00 |  7 |          120.01 |
| 2018-01-08 | ABC | 100.00 |    -16.67 |  8 |          100.00 |
| 2018-01-09 | ABC |   NULL |     -7.21 |  9 |           92.79 |
| 2018-01-10 | ABC |   NULL |      1.31 | 10 |           94.01 |
| 2018-01-11 | ABC |   NULL |      6.38 | 11 |          100.01 |
| 2018-01-12 | ABC |   NULL |    -30.00 | 12 |           70.01 |
| 2019-01-01 | ABC |   NULL |     14.29 | 13 |           80.01 |
| 2019-01-02 | ABC |   NULL |      5.27 | 14 |           84.23 |

Демонстрация DB Fiddle

person Salman A    schedule 13.11.2018
comment
Да, это хорошо работает. Впервые с помощью рекурсии. Однако, если мы применим это к паре миллионов строк, я думаю, что обработка будет очень медленной, верно? - person jslater41; 13.11.2018
comment
Да, это итеративно, и его сложно или, возможно, невозможно оптимизировать. Помимо медленности, меня беспокоит ограничение на рекурсию ... SQL-сервер останавливается после максимум 32 768 итераций. - person Salman A; 13.11.2018

В SQL Server вы можете получить доступ к значениям предыдущих / следующих строк с помощью оконных функций LAG и LEAD. Вам необходимо определить порядок строк, указав его в предложение OVER. Вам может потребоваться обернуть запрос выбора, который возвращает предыдущие / следующие значения в производной таблице или CTE, а затем выбрать из него и рассчитать свои прогнозы.

with cte as (SELECT [Date], Price, LAG(Price, 1) over(order by [Date]) as PrevPrice from TABLE)
select [Date], Price, Price - PrevPrice as PriceChange from cte
person Andrey Nikolov    schedule 12.11.2018
comment
Потрясающие. Попробую позже. Одна вещь, которую я не добавил в образцы данных, - это несколько идентификаторов за один и тот же промежуток времени. Я предполагаю, что это будет своего рода раздел в операторе over или просто группа по. Я сообщу, если не смогу понять эту часть. - person jslater41; 12.11.2018
comment
Если я правильно понял, вам нужно добавить раздел в предложении over LAG(Price, 1) over(partition by ElementId order by [Date]) as PrevPriceForTheSameElementType - person Andrey Nikolov; 12.11.2018