С каждой версией SQL Server появляются новые возможности, за которые мы благодарны — наконец-то появляется доступ к полезным функциям, которые уже были в других системах.
В SQL Server 2025 CTP 1.3 была представлена функция PRODUCT(). Она ведёт себя похоже на SUM()
, но умножает значения вместо того, чтобы их складывать. Это агрегатная функция в SQL Server, следовательно, она действует на набор данных, а не на скалярные значения.
Вычисление произведения без PRODUCT()
До появления этой функции написать T-SQL, умножающий ряд значений из набора, было возможно, но делалось это не изящно. Представим ситуацию, где нужно перемножать значения за период, чтобы посчитать накопительный мультипликативный показатель — например, процент по вкладу или эффект инфляции.
Рассмотрим следующий набор данных, моделирующий банковский счёт, где процентная ставка меняется по месяцам:
CREATE TABLE #BankAccount
( RowId INT NOT NULL IDENTITY(1,1) PRIMARY KEY CLUSTERED,
StartDate DATE NOT NULL,
EndDate DATE NOT NULL,
StartBalance DECIMAL(18,2) NOT NULL,
EndBalance DECIMAL(18,2) NOT NULL,
AnnualInterestRate DECIMAL(7,4) NOT NULL);
INSERT INTO #BankAccount
(StartDate, EndDate, StartBalance, EndBalance, AnnualInterestRate)
VALUES
('1/1/2024', '1/31/2024', 100.00, 100.44, 0.0523),
('2/1/2024', '2/29/2024', 100.44, 100.86, 0.0501),
('3/1/2024', '3/31/2024', 100.86, 101.27, 0.0490),
('4/1/2024', '4/30/2024', 101.27, 101.66, 0.0463),
('5/1/2024', '5/31/2024', 101.66, 102.02, 0.0424),
('6/1/2024', '6/30/2024', 102.02, 102.38, 0.0426),
('7/1/2024', '7/31/2024', 102.38, 102.74, 0.0407),
('8/1/2024', '8/31/2024', 102.74, 103.09, 0.0395),
('9/1/2024', '9/30/2024', 103.09, 103.43, 0.0382),
('10/1/2024', '10/31/2024', 103.43, 103.76, 0.0381),
('11/1/2024', '11/30/2024', 103.76, 104.08, 0.0374),
('12/1/2024', '12/31/2024', 104.08, 104.37, 0.0329),
('1/1/2025', '1/31/2025', 104.37, 104.63, 0.0300),
('2/1/2025', '2/28/2025', 104.63, 104.89, 0.0299),
('3/1/2025', '3/31/2025', 104.89, 105.14, 0.0283);
Данные содержат годовую процентную ставку, обновляемую ежемесячно — типичная ситуация для банковских счетов. Для вычисления конечного баланса требуется разделить годовую ставку на двенадцать, прибавить единицу и умножить на баланс. Например, для января 2024 (округлено до двух знаков):
$100.00 * (1 + (0.0523 / 12.0000)) = $100.00 * (1 + 0.004358) = 100 * 1.004358 = 100.44
Полезной задачей может быть вычисление общей эффективной процентной ставки за произвольный период. Самый точный способ — перемножить месячные множители за рассматриваемый промежуток. Например, чтобы получить годовой итог для 2024-го, нужно перемножить все двенадцать месячных множителей за 2024-й. Но функции, делающей это напрямую, не было — оставались два подхода: итеративный и математический с логарифмами.
Итеративный подход
Простой пример, который проходит по таблице и последовательно считает годовую ставку для 2024:
DECLARE @CurrentDate DATE = '1/1/2024';
DECLARE @InterestRate DECIMAL(7,4) = 1;
WHILE @CurrentDate <= '12/31/2024'
BEGIN
SELECT
@InterestRate = @InterestRate * (1 + AnnualInterestRate / 12.0000)
FROM #BankAccount
WHERE StartDate = @CurrentDate;
SELECT @CurrentDate = DATEADD(MONTH, 1, @CurrentDate);
END
SELECT @InterestRate AS AnnualInterestRate2024;
Результат выглядит следующим образом:
Итеративный метод даёт правильный результат, но он не оптимален. Для больших таблиц с тысячами или миллионами строк этот подход быстро станет медленным и неэффективным. В работе с наборами данных предпочтителен set-based подход.
Set-based метод с логарифмами
Способ основанный на наборе значений опирается на математическое преобразование: берём логарифм (по основанию 10) каждого множителя, суммируем и затем возводим 10 в полученную степень. Это использует правило произведения для логарифмов:
Log (X * Y) = Log X + Log Y
Поэтому годовую ставку за 2024 можно получить так:
SELECT
POWER(10.0000, SUM(LOG10(1 + AnnualInterestRate / 12.0000))) AS AnnualInterestRate2024
FROM #BankAccount
WHERE StartDate >= '1/1/2024'
AND StartDate <= '12/1/2024';
Возвращаемый результат такой:
Результат немного отличается от итеративного метода: это связано с тем, что в итеративном подходе происходит округление на каждом шаге, тогда как в таком методе округление выполняется в конце.
Тот же итог можно получить и так:
SELECT
EXP(SUM(LOG(1 + AnnualInterestRate / 12.0000))) AS AnnualInterestRate2024
FROM #BankAccount
WHERE StartDate >= '1/1/2024'
AND StartDate <= '12/1/2024';
Разница лишь в том, что результат не округляется:
Хотя такой подход гораздо эффективнее, он и менее очевиден: при чтении кода непосвящённый разработчик вряд ли поймёт, зачем применяли логарифмы и экспоненты. Даже при наличии комментариев большинству разработчиков не придёт в голову применять логарифмы в реальных проектах.
Упрощение кода с PRODUCT()
SQL Server 2025 даёт долгожданное решение: функцию PRODUCT(). Эта функция работает точно так же, как агрегат SUM()
, но перемножает значения вместо сложения.
Решение предыдущей задачи с PRODUCT() выглядит просто и наглядно:
SELECT
PRODUCT(1 + AnnualInterestRate / 12.0000) AS AnnualInterestRate2024
FROM #BankAccount
WHERE StartDate >= '1/1/2024'
AND StartDate <= '12/1/2024';
Результат может немного отличаться из-за особенностей округления, но из всех рассмотренных вариантов эта запись — самая простая, читабельная и эффективная. Функция PRODUCT()
оптимизирована для пакетного режима (batch mode) и даёт заметный прирост производительности в пакетном выполнении, независимо от того, используется ли columnstore или rowstore.
Детали работы PRODUCT()
У PRODUCT()
есть особенности, которые важно знать, поскольку они влияют на использование и на возвращаемое значение.
INT vs. BIGINT — возвращаемый тип
Во-первых, PRODUCT()
среди возможных типов, которые она возвращает, допускает тип INTEGER. Это важно, потому что при умножении большого числа множителей легко превысить предел целого числа. Если результат выйдет за границы целого в положительную или отрицательную сторону, будет возвращена ошибка. Ниже приведён пример:
SELECT
PRODUCT(TaxRate)
FROM Sales.OrderLines;
Если этот запрос перемножает множество значений и итог превышает пределы int
, вы получите ошибку:
Msg 8115, Level 16, State 2, Line 68
Arithmetic overflow error converting expression to data type numeric.
Пока нет варианта PRODUCT()
с возвращаемым BIGINT
, поэтому важно следить за размером данных и гарантировать, что результат не превысит диапазон int
. У агрегатов есть исключение — только у COUNT()
существует версия с большим типом: COUNT_BIG()
. С PRODUCT()
нужно быть не менее осторожным, чем с SUM()
.
На заметку, вот какие типы функция может вернуть на момент публикации статьи:
Результат выражения | Возвращаемый тип |
---|---|
tinyint | int |
smallint | int |
int | int |
bigint | bigint |
Категория decimal(p, s) | If (s == 0): decimal(38, 0) Else: decimal(38, 6) |
Категория money и smallmoney | money |
Категория float и real | float |
Обработка NULL
NULL обрабатывается так же, как и для других агрегатных функций — NULL игнорируется. Протестируем, добавив в набор несколько строк с NULL:
ALTER TABLE #BankAccount ALTER COLUMN AnnualInterestRate DECIMAL(7,4) NULL;
INSERT INTO #BankAccount
(StartDate, EndDate, StartBalance, EndBalance, AnnualInterestRate)
VALUES
('4/1/2025', '4/30/2025', 105.14, 105.14, NULL),
('5/1/2025', '5/31/2025', 105.14, 105.14, NULL),
('6/1/2025', '6/30/2025', 105.14, 105.14, NULL);
Вычисление годовой ставки для 2025 можно выполнить так (с учётом того, что в апреле-июне стояли NULL):
SELECT
PRODUCT(1 + AnnualInterestRate / 12.0000) AS AnnualInterestRate2025
FROM #BankAccount
WHERE StartDate >= '1/1/2025'
AND StartDate <= '12/1/2025';
NULL просто пропускаются, и в расчёт попадают только существующие значения.
PRODUCT() как оконная функция
Функцию PRODUCT()
можно применять и как оконную функцию, позволяющую вычислять накопительное произведение по скользящему окну — удобно для расчёта кумулятивных показателей, например нарастающего мультипликатора процентов.
SELECT
StartDate,
DATEPART(YEAR, StartDate) AS CurrentYear,
AnnualInterestRate,
PRODUCT(1 + AnnualInterestRate / 12.0000) OVER (PARTITION BY DATEPART(YEAR, StartDate) ORDER BY StartDate
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS RunningAnnualTotal
FROM #BankAccount
ORDER BY StartDate;
Обратите внимание: если в периоде встречаются NULL (например, 4/1/2025–6/1/2025 в нашем примере), накопительный итог корректно продолжается по непустым значениям вверх по временной шкале. Если в окне нет ни одного непустого значения, то функция возвращает NULL.
Аналогичным образом можно вычислить накопительный итог по кварталам:
SELECT
StartDate,
DATEPART(QUARTER, StartDate) AS CurrentQuarter,
AnnualInterestRate,
PRODUCT(1 + AnnualInterestRate / 12.0000) OVER (PARTITION BY DATEPART(YEAR,
StartDate), DATEPART(QUARTER, StartDate) ORDER BY StartDate
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS RunningQuarterlyTotal
FROM #BankAccount
ORDER BY StartDate;
Заключение
Функция PRODUCT()
предоставляет функциональность, которая упростит жизнь многим инженерам данных и аналитикам. Возможность быстро и эффективно посчитать накопительное произведение простым синтаксисом — давно ожидаемая опция для SQL Server.
Она особенно удобна для работы с процентами и ставками, где вычисления в долгих формулах становятся громоздкими и неэффективными. Единственное, что требует внимания — большие числа: PRODUCT()
возвращает INTEGER
, поэтому результаты, которые по масштабу попадают в диапазон BIGINT
, вызовут ошибку вместо корректного ответа.
Полное описание новой функции можно найти тут: PRODUCT (Transact-SQL)
не понял какая связь между возвращаемым значением типа INTи примерами с десятичной точкой? так что же эта функция возвращает в итоге?
ОтветитьУдалитьВнёс ясности, так понятно?
Удалить