9.9.25

Новое в SQL Server 2025: функция PRODUCT()

Автор: Edward Pollack, How to Use the New PRODUCT() Function in SQL Server 2025

С каждой версией 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)


2 комментария:

  1. Анонимный9/9/25 10:32

    не понял какая связь между возвращаемым значением типа INTи примерами с десятичной точкой? так что же эта функция возвращает в итоге?

    ОтветитьУдалить