Гудков К.С.
Государственный научно-исследовательский институт
авиационных систем,
Московский физико-технический институт
Сравнение методов реализации агрегатной функции произведения чисел в
СУБД MS SQL Server
В СУБД Microsoft SQL Server 2014 (и в более ранних версиях) отсутствует агрегатная
функция для вычисления произведения чисел [1]. Она может оказаться полезной,
например, для вычисления среднего геометрического чисел.
Существует несколько традиционных подходов для вычисления произведения
чисел в СУБД Microsoft SQL Server:
· Реализация пользовательской агрегатной функции среды CLR.
· Математический метод, базирующийся на факте, что логарифм произведения
равен сумме логарифмов.
· Метод, основанный на присваивании переменной внутри SELECT.
· Метод, основанный на использовании курсоров.
· Метод, основанный на использовании рекурсивных обобщённых табличных
выражений (Common Table Expressions, CTE).
Для сравнения
производительности была сгенерирована таблица из 100 тысяч записей, случайным
образом разбитая на группы с небольшим числом перемножаемых чисел, среди
которых встречаются и положительные, и отрицательные, и ноль. Задача состояла в
вычислении произведения всех чисел для каждой из групп по отдельности. Сценарий
по созданию таблицы выглядит следующим образом:
if (object_id('RandomIntNumbers')
is not null)
drop table RandomIntNumbers
create table RandomIntNumbers(ID int primary key,
RandGroupID int, RandInt int)
Для заполнения таблицы использовались
обобщённые табличные выражения:
;with
CTERandomIntNumbers(ID, RandGroupID, RandInt) as
(
select 1 as ID,
abs(cast(cast(newid() as Binary(4)) as int)) % 60000 as
RandGroupID,
cast(cast(newid() as Binary(4)) as int) % 10 as RandInt
union all
select ID + 1 as ID,
abs(cast(cast(newid() as binary(4)) as int) % 60000) as
RandGroupID,
cast(cast(newid() as binary(4)) as int) % 10 as RandInt
from
CTERandomIntNumbers
where ID < 100000
)
insert into
RandomIntNumbers(ID, RandGroupID, RandInt)
select ID, RandGroupID, RandInt
from CTERandomIntNumbers
option(maxrecursion 0)
Для замера времени работы сценария были
созданы две переменные типа datetime: @BeginTime и @EndTime. Переменная
@BeginTime инициализировались в начале выполнения запроса, а переменная
@EndTime инициализировалась в конце выполнения запроса. Для их инициализации
использовалась функция getdate(). Суммарное время работы вычислялось при
помощи: select datediff(millisecond, @BeginTime, @EndTime).
Лучшие результаты с
точки зрения скорости работы были показаны при помощи CLR. Среднее время работы
данного метода составило 413 миллисекунд. Данный метод универсален, то есть
пользовательская агрегатная функция среды CLR работает для любой таблицы. Для
реализации агрегата необходимо реализовать 4 стандартных метода Init, Accumulate, Merge и Terminate. Кроме
того, необходимо создать 2 объекта SQL-сервера: сборку (assembly) и
непосредственно агрегатную функцию (aggregate). Примеры реализации агрегатных функций CLR показаны, например, здесь [2].
На втором месте с точки
зрения скорости работы оказался математический метод:
select RandGroupID, round(exp(sum(log(case when
RandInt <> 0 then abs(RandInt) else 1 end))) *
case when
sum(case when RandInt < 0 then 1 else 0 end) % 2 = 1 then -1 else 1 end *
case when
sum(case when RandInt = 0 then 1 else 0 end) > 0 then 0 else 1 end, 0) as
Product
from RandomIntNumbers
group by RandGroupID
Среднее время работы
данного метода составило 513 миллисекунд.
Конструкция "CASE WHEN THEN ELSE END" была
необходима, чтобы решить проблему отрицательных чисел и нуля. Среди недостатков
этого метода можно указать отсутствие гарантии корректной работы, вызванное
присутствием ошибок округлений, способных привести к неправильному результату.
Третьи результаты показал
метод, основанный на присваивании переменной внутри SELECT. Для его работы
необходимо создать скалярную функцию SQL.
if (object_id('SQLProduct') is not null)
drop function
SQLProduct
go
create function SQLProduct(@RandGroupID int)
returns int
begin
declare
@ProdVar as int
set @ProdVar
= 1
select
@ProdVar = @ProdVar * RandInt
from
RandomIntNumbers RIN
where
RandGroupID = @RandGroupID
return
@ProdVar
end
Кроме того, как видно из устройства функции SQLProduct, для быстрой
работы необходимо также создать некластерный индекс по полю RandGroupID.
Среднее время работы данного метода составило 1380 миллисекунд. Перечислим
недостатки метода, основанного на присваивании переменной внутри SELECT:
· Необходимость создания скалярной функции для каждой из таблиц, в которых
необходимо вычислить произведение чисел.
· Необходимость создания некластерного индекса по идентификатору группы.
Без него среднее время работы метода составило 8 минут.
· Отсутствие гарантии корректной работы, которое можно продемонстрировать
на простейшем примере:
declare @TestTable as table(ID int)
insert into @TestTable(ID) values(2)
insert into @TestTable(ID) values(4)
declare @TestVar as int
set @TestVar = 1
select @TestVar = @TestVar * ID
from @TestTable
order by ID * ID
select @TestVar
Переменная @TestVar будет
содержать 4 вместо ожидаемых 8.
Четвертые результаты
показал метод, основанный на стандартном использовании курсоров. Среднее время
его работы 4040 миллисекунд.
Последнее место с точки
зрения скорости работы показал метод, основанный на использовании рекурсивных
обобщённых табличных выражений. На описанных в рамках статьи данных метод не
закончил работу в течении часа.
Таким образом, метод,
основанный на присваивании переменной внутри SELECT, и математический метод не
гарантируют корректность результата. Методы, основанные на обобщённых табличных
выражениях и курсорах, работают слишком медленно по сравнению с методом,
основанным на реализации пользовательской агрегатной функции среды CLR. Поэтому
именно этот метод и является лучшим для вычисления произведения чисел в
Microsoft SQL Server.
Литература:
1. https://msdn.microsoft.com/ru-ru/library/ms173454.aspx
2. https://msdn.microsoft.com/ru-ru/library/ms131056.aspx