Создание пользовательских статистических функций

advertisement
Создание пользовательских статистических
функций
Суммировать данные по-новому путем написания своих собственных статистических функций.
Одним из аспектов Oracle Database, который давно считается очень мощным, является возможность писать
пользовательский процедурный код и сочетать его реляционной обработкой. Такой код очень часто принимает
вид хранимых функций, которые можно вызывать из предложений SELECT или других предложений. Создание и
использование функций, обрабатывающих одну строку в Oracle Database, очень хорошо понятно, но знаете ли
вы, что точно также можно создавать и ваши собственные статистические агрегатные функции? Да, вы можете
это делать, и гораздо проще, чем вы себе думаете. Эта статья показывает вам, как это делается.
Сценарий
Предположим, что вы работаете в компании по прокату автомобилей. У вас есть таблица, показанная в Листинге
1, в которой каждая строка представляет выдачу в прокат одного автомобиля заказчику. Для каждого проката вы
имеете время выдачи автомобиля и время его возврата (столбцы rental_out и rental_in). Ваша таблица хранит эти
два значения в виде типа TIMESTAMP. Ваша задача состоит в том, чтобы создавать
различные отчеты основываясь на среднем времени срока проката автомобиля. Например, вы хотите иметь
возможность определять средний период проката по штатам и сравнить каждый период проката со скользящим
средним.
Листинг 1: Таблица Car_rental
CAR_ID
-------1
1
1
1
2
2
2
2
CUST_ID
-------101
102
103
104
113
114
115
116
RENTAL_STATE
--------MI
MI
MI
MI
WI
WI
WI
WI
RENTAL_OUT
-----------------------05-MAR-06 08.00.00.00 AM
12-MAR-06 08.34.24.00 AM
17-MAR-06 11.19.00.00 PM
28-MAR-06 09.45.15.00 AM
06-MAR-06 08.00.00.00 AM
13-MAR-06 08.34.24.00 AM
18-MAR-06 11.19.00.00 PM
29-MAR-06 09.45.15.00 AM
RENTAL_IN
-----------------------11-MAR-06 02.00.00.00 PM
17-MAR-06 07.23.19.00 PM
26-MAR-06 06.00.00.00 AM
30-MAR-06 03.27.13.00 PM
11-MAR-06 02.00.00.00 PM
17-MAR-06 07.23.19.00 PM
26-MAR-06 06.00.00.00 AM
30-MAR-06 03.27.13.00 PM
.
.
.
Использование типа TIMESTAMP дает возможность вычислить продолжительность каждого индивидуального
проката достаточно просто: вычитаете одно значение типа TIMESTAMP из другого и получаете результат
типа INTERVAL DAY TO SECOND:
SELECT rental_in - rental_out
FROM car_rental;
RENTAL_IN-RENTAL_OUT
6 6:0:0.0
5 10:48:55.0
8 6:41:0.0
.
.
.
Однако, вычисление средней продолжительности периода проката быстро превращает эту задачу в менее
тривиальную:
SELECT AVG(rental_in - rental_out)
FROM car_rental;
SQL Error: ORA-00932:
inconsistent datatypes:
expected NUMBER
got INTERVAL DAY TO SECOND
Встроенная статистическая функция AVG не поддерживает интервальные типы (то же самое относится и к
функции SUM). Для многих эта проблема может привести к написанию процедурного кода, который выполняется
на клиентской машине и извлекает и суммирует большие объемы данных через сеть с помощью циклов и
курсоров, со всеми вытекающими отсюда проблемами с производительностью и расширяемостью, которые могут
возникнуть в результате использования такого подхода. Однако, знание того, как писать статистические функции
делает эту проблему не более сложной, чем небольшое ускорение.
Написание статистических функций
Рисунок 1 концептуально показывает, как статистическая функция — любого типа статистической функции —
должна быть вычислена. Прежде всего, вы должны выполнить некую инициализацию. Затем вы передаете внутрь
несколько значений в качестве входных данных для функции. И наконец, вы получаете в ответ единственное
результирующее значение.
Рисунок 1. Как производится вычисление статистической функции
Рисунок 2. Методы статистической функции
Пользовательская статистическая функция имеет в точности один параметр. Тогда каждое входное значение
представляет отдельную строку таблицы или результата запроса. Нет возможности написать пользовательскую
статистическую функцию, которая принимает множество параметров.
Написание статистической функции в Oracle Database это дело создания объектного типа с методами,
соответствующими каждой фазе Рисунка 1, плюс один дополнительный метод для поддержки параллельного
вычисления. Рисунок 2 иллюстрирует эти методы.
При вычислении статистических функций на основании сгруппированных значений Oracle Database делает
следующее:
1. Вызывает ODCIAggregateInitialize для создания экземпляра типа, который вы построили для реализации
агрегирования. Этот экземпляр известен как агрегирующий контекст.
2. Вызывает ODCIAggregateIterate в цикле для передачи внутрь контекста всех значений в группе, которая должна
быть агрегирована.
3. Вызывает ODCIAggregateTerminate для генерации единственного результирующего значений.
Это процесс представляет собой простейшую форму агрегирования. Oracle Database также может
распараллелить эту операцию путем разделения данных на блоки и выполнения перечисленных выше шагов над
каждым блоком и затем выполняя один или более вызовов ODCIAggregateMerge для объединения результатов от
каждого блока в единственный результат.
Создание типа Creating the Type
Возвращаясь к сценарию проката автомобилей, вашей задачей будет написание статистической функции для
вычисления средней продолжительности проката группы автомобилей, выраженного в виде значения
типа INTERVAL DAY TO SECOND. Начать необходимо с создания объектного типа для реализации методов
статистической функции. Листинг 2 показывает спецификацию объектного типа с именем AvgInterval. Листинг 3
показывает тело объектного типа. Вы должны реализовать, по крайней мере, методы, приведенные в Листинге 2.
Это включает три метода для начала, снабжения (feeding) и завершения агрегации, а также один метод для
слияния результатов после параллельного вычисления. Все методы должны возвращать значение
типа NUMBER, которое представляет статус удачного или неудачного завершения. Вы можете изменить имена
параметров, но число параметров и их порядок фиксированы.
Листинг 2: Спецификация типа AvgInterval
CREATE OR REPLACE TYPE AvgInterval
AS OBJECT (
runningSum INTERVAL DAY(9) TO SECOND(9),
runningCount NUMBER,
STATIC FUNCTION ODCIAggregateInitialize
( actx IN OUT AvgInterval
) RETURN NUMBER,
MEMBER FUNCTION ODCIAggregateIterate
( self IN OUT AvgInterval,
val
IN
DSINTERVAL_UNCONSTRAINED
) RETURN NUMBER,
MEMBER FUNCTION ODCIAggregateTerminate
( self
IN
AvgInterval,
returnValue OUT DSINTERVAL_UNCONSTRAINED,
flags
IN
NUMBER
) RETURN NUMBER,
MEMBER FUNCTION ODCIAggregateMerge
(self IN OUT AvgInterval,
ctx2 IN
AvgInterval
) RETURN NUMBER
);
/
Листинг 3: Тело типа AvgInterval
CREATE OR REPLACE TYPE BODY AvgInterval AS
STATIC FUNCTION ODCIAggregateInitialize
( actx IN OUT AvgInterval
) RETURN NUMBER IS
BEGIN
IF actx IS NULL THEN
dbms_output.put_line('NULL INIT');
actx := AvgInterval (INTERVAL '0 0:0:0.0' DAY TO SECOND, 0);
ELSE
dbms_output.put_line('NON-NULL INIT');
actx.runningSum := INTERVAL '0 0:0:0.0' DAY TO SECOND;
actx.runningCount := 0;
END IF;
RETURN ODCIConst.Success;
END;
MEMBER FUNCTION ODCIAggregateIterate
( self IN OUT AvgInterval,
val
IN
DSINTERVAL_UNCONSTRAINED
) RETURN NUMBER IS
BEGIN
DBMS_OUTPUT.PUT_LINE('Iterate ' || TO_CHAR(val));
IF val IS NULL THEN
/* Это никогда не должно случиться */
DBMS_OUTPUT.PUT_LINE('Null on iterate');
END IF;
self.runningSum := self.runningSum + val;
self.runningCount := self.runningCount + 1;
RETURN ODCIConst.Success;
END;
MEMBER FUNCTION ODCIAggregateTerminate
( self
IN AvgInterval,
ReturnValue OUT DSINTERVAL_UNCONSTRAINED,
flags
IN NUMBER
) RETURN NUMBER IS
BEGIN
dbms_output.put_line('Terminate ' || to_char(flags) ||
to_char(self.runningsum));
IF self.runningCount <> 0 THEN
returnValue := self.runningSum / self.runningCount;
ELSE
/* Возможен случай пустой группы, поэтому нужно быть осторожным при
делении на ноль. */
returnValue := self.runningSum;
END IF;
RETURN ODCIConst.Success;
END;
MEMBER FUNCTION ODCIAggregateMerge
(self IN OUT AvgInterval,
ctx2 IN
AvgInterval
) RETURN NUMBER IS
BEGIN
self.runningSum := self.runningSum + ctx2.runningSum;
self.runningCount := self.runningCount + ctx2.runningCount;
RETURN ODCIConst.Success;
END;
END;
/
В объектном типе необходимо определить любые переменные, которые нужны для поддержки состояния в
процессе агрегации. Как вы поддерживаете состояние, целиком зависит от вас и от того, как вы решите
реализовать логику агрегации. Имеет смысл использовать накапливающееся значение суммы и счетчик
количества значений для последующего вычисления среднего значения (хотя возможны и другие подходы),
поэтому в верху спецификации AvgInterval определены две переменные (смотрите Листинг 2):
runningSum INTERVAL DAY(9)
TO SECOND(9),
runningCount NUMBER,
runningSum определена с максимальной точностью (9) для учета и дней, и десятичных долей секунд. По
умолчанию можно использовать только два знака для учета дней и только шесть знаков после десятичной точки
для учета десятичных долей секунды. Использование же максимальной точности в обоих случаях
делает AvgInterval полезной в общем случае с любыми значениями INTERVAL DAY TO SECOND, не зависимо от
точности.
Далее в спецификации AvgInterval идут объявления функций для интерфейса ODCIAggregate. Они могут
следовать в любом порядке, но давайте начнем с начала, то есть с функции инициализации:
.
.
.
STATIC FUNCTION ODCIAggregateInitialize
( actx IN OUT AvgInterval
) RETURN NUMBER,
.
.
.
Oracle Database вызывает эту функцию, чтобы инициализировать процесс статистических
вычислений. Единственный аргумент имеет тип, который вы создаете для реализации статистической функции (в
данном случае AvgInterval). Вообще говоря назначение функции ODCIAggregateInitialize состоит в создании
нового экземпляра базового объектного типа и последующей инициализации переменных экземпляра,
используемых в вычислении статистического результата. Ключевое
слово STATIC позволяет Oracle Database вызывать эту функцию независимо от того, существует ли в
действительности экземпляр данного типа.ODCIAggregateInitialize единственная из функций
интерфейса ODCIAggregate определяемая как STATIC.
Экземпляр объекта, который вернул метод ODCIAggregateInitialize становится агрегирующим (или
статистическим) контекстом. Если вы вызываете одну и ту же статистическую функцию несколько раз в одном
предложении, то Oracle Database создаст, по крайней мере, по одному контексту для каждого вызова. Если
же Oracle Database распараллеливает выполнение, то она создаст по одному контексту для каждого из потоков.
В Листинге 3 вы можете посмотреть код ODCIAggregateInitialize. Теперь же сфокусируемся на случае, в котором
входной параметр имеет пустое значение (null). Для создания экземпляра объекта используется неявно
определенный конструктор. Значения, передаваемые в конструктор соответствуют runningSum и runningCount, и
оба инициализированы в 0.
Далее посмотрим на ODCIAggregateIterate в Листинге 3. Это «рабочая лошадка» всего агрегирующего
типа. Oracle Database будет вызывать эту функцию для каждой строки—по одному разу для каждого значения для
накопления статистических значений. Логика здесь достаточно проста:
self.runningSum := self.runningSum + val;
self.runningCount := self.runningCount + 1;
Как только поступает новое значение для вычисления статистики, функция добавляет это значение к
накопленной сумме и увеличивает значение счетчика. После того как обработаны все
значения, Oracle Databaseвызывает ODCIAggregateTerminate, как показано в Листинге 3, которая, в свою очередь,
использует два значения для вычисления среднего:
returnValue := self.runningSum /
self.runningCount;
Знайте также о возможности того, что контекст будет создан и завершен даже для пустой группы. Учитывая
вычисления, показанные здесь для returnValue, пустая группа приведет в результате к ошибке деления на ноль.
Код в Листинге 3 старается избежать такого деления и сообщает об ошибке в случае если счетчик равен 0.
(Деление на ноль в сценарии для данной статьи объявляет себя как ошибка "ORA01873: the leadingprecision of the interval is too small (точность интервала слишком мала)".)
Отметьте, что в Листинге 3 тип данных, используемый для аргумента val в ODCIAggregateIterate, а
именно DSINTERVAL_UNCONSTRAINED, эквивалентен типу INTERVAL DAY(9) TO SECOND(9). Необходимо
использовать неограниченный тип потому, что вы не можете задать точность в списке параметров метода.
Однако, вы также не можете использовать неограниченный тип для значения текущей суммы.
Последний метод класса, который нужно рассмотреть, это ODCIAggregateMerge, и он также приведен в Листинге
3. Вспомните из Рисунка 2, что процедура агрегирования может быть распараллелена. Oracle Databaseсоздаст
отдельные агрегирующие (или статистические) контексты для каждого параллельного потока
выполнения. Предполагая, что мы имеем три параллельных потока, последовательность событий будет
следующей
1. ODCIAggregateInitialize вызывается для потока 1
2. ODCIAggregateInitialize вызывается для потока 2
3. ODCIAggregateInitialize вызывается для потока 3
4. ODCIAggregateIterate вызывается параллельно во всех потоках: 1, 2 и 3
5. ODCIAggregateMerge вызывается для слияния, например, потоков 2 и 3
6. ODCIAggregateMerge вызывается для слияния потока 1 с результатом шага 5
7. ODCIAggregateTerminate вызывается для генерации окончательного результата
Oracle Database вызывает ODCIAggregateMerge с двумя аргументами. Каждый аргумент представляет собой
контекст агрегации:
MEMBER FUNCTION ODCIAggregateMerge
(self IN OUT AvgInterval,
ctx2 IN
AvgInterval
) RETURN NUMBER IS
Задача функции слияния состоит в изменении первого контекста агрегации—self—таким образом, чтобы он
отражал объединение первого и второго—ctx2—контекстов. Вы вычисляете среднее значение, поэтому вы
можете сложить текущие значения сумм и счетчиков:
.
.
.
self.runningSum :=
self.runningSum + ctx2.runningSum;
self.runningCount :=
self.runningCount + ctx2.runningCount;
RETURN ODCIConst.Success;
.
.
.
Эффект этой операции слияния состоит в том, что значения текущей суммы и счетчика в self (первый контекст в
списке аргументов) теперь хранят значения такие, которые были бы получены в случае «прогонки» всех значений
через этот один контекст. Контекст ctx2 теперь больше не нужен. (Если вы когда-либо писали пользовательскую
статистическую функцию на таких языках как C или Java и вы распределяли память или какие-то другие ресурсы
для контекста ctx2, то вам необходимо освободить память или ресурсы в этой точке процесса вычислений.)
Определение функции
После того, как вы создадите объектный тип и методы для реализации новой статистической функции, вы
должны построить между именем функции, которое вы хотите использовать в SQL, и базовым типом. Делается
это с помощью предложения приведенного ниже:
CREATE OR REPLACE FUNCTION avg_interval
( x DSINTERVAL_UNCONSTRAINED
) RETURN DSINTERVAL_UNCONSTRAINED
PARALLEL_ENABLE
AGGREGATE USING AvgInterval;
/
Это предложение создает функцию avg_interval, которая имеет один аргумент, совместимый с любым значением
типа INTERVAL DAY TO SECOND. PARALLEL_ENABLE разрешает использовать
распараллеливание.Предложение AGGREGATE USING связывает функцию avg_interval с базовым
типом AvgInterval. Вызовите avg_interval и - Oracle Database вызовет различные методы типа AvgInterval для
генерации результата.
Использование новой функции
Использование созданной новой статистической функции avg_interval не может быть проще: вызывается она так
же как и любая другая встроенная статистическая функция. Теперь можно легко вычислить среднюю
продолжительность периода проката даже для всех данных:
SELECT avg_interval
(rental_in - rental_out)
FROM car_rental;
AVG_INTERVAL(RENTAL_IN-RENTAL_OUT)
+000000004 15:25:17.000000000
И вы также легко и просто можете вычислить среднюю продолжительность проката для каждого штата в
отдельности:
SELECT
rental_state,
avg_interval
(rental_in - rental_out)
FROM car_rental
GROUP BY rental_state;
RE
---MI
MN
WI
AVG_INTERVAL(RENTAL_IN-RENTAL_O...
--------------------------------------+000000005 12:31:08.250000000
+000000003 12:07:43.250000000
+000000004 12:31:08.250000000
Код в этой статье содержит вызовы DBMS_OUTPUT. Если вы выполняете приведенные выше
предложения SELECT из SQL Developer, то можно разрешить видимость сообщений из DBMS_OUTPUT, щелкнув
на пиктограмму панели инструментов Enable DBMS Output в закладке DBMS Output. (В SQL*Plus нужно выполнить
команду SET SERVEROUTPUT ON.) Теперь можете выполнить запрос, и вы увидите,
как Oracle Databaseвызывает различные методы ODCI для типа AvgInterval:
NULL INIT
Iterate +000000006
Iterate +000000005
Iterate +000000008
.
.
. NULL INIT
Iterate +000000005
Iterate +000000004
Iterate +000000007
.
.
.
06:00:00.000000000
10:48:55.000000000
06:41:00.000000000
06:00:00.000000000
10:48:55.000000000
06:41:00.000000000
NULL INIT
Iterate +000000003 14:37:00.000000000
Iterate +000000003 00:48:55.000000000
Iterate +000000007 12:41:00.000000000
.
.
.
Terminate 0+000000066 06:13:39.000000000
Terminate 0+000000028 01:01:46.000000000
Terminate 0+000000054 06:13:39.000000000
Три строки с NULL INIT показывают, как Oracle Database создает новый контекст агрегирования для каждой
группы. Строки итерации показывают значения, поступающие в каждую группу. И
наконец, Oracle Databaseвызывает метод завершения по одному разу для каждой группы для формирования
окончательных результатов. (Если вы выполняете это код на многопроцессорной системе, то из-за
использования параллелизма, результаты вывода DBMS_OUTPUT не будут так красиво упорядочены, как
приведенные выше.) Конечно, вы вероятно можете не захотеть использовать вызовы DBMS_OUTPUT в рабочем
коде.
За кулисами Oracle Database выполняет для вас достаточно много вспомогательной работы:

Она группирует данные.

Она отфильтровывает пустые значения (nulls) (так как это требует стандарт ISO SQL).

Она осуществляет распараллеливание.
Все, о чем вам нужно беспокоиться при написании методов ODCI, это две вещи: как сложить одну группу
значений и как слить две группы. Вы даже бесплатно получаете поддержку аналитического синтаксиса.
Аналитический синтаксис
Любая статистическая функция, которую вы создаете, может также быть использована как аналитическая
функция. Oracle Database сама обрабатывает все детали, связанные с разбивкой и упорядочением.Например,
чтобы сравнить каждый отдельный срок проката со средним сроком проката в том же самом штате, вы можете
написать запрос, похожий на этот:
SELECT
rental_in - rental_out time,
rental_state st,
avg_interval
(rental_in - rental_out)
OVER (PARTITION BY rental_state)
state_avg
FROM car_rental;
Результат работы будет выглядеть следующим образом:
RENTAL_TIME
-----------6 6:0:0.0
5 10:48:55.0
8 6:41:0.0
6 6:41:0.0
4 10:48:55.0
...
ST
-MI
MI
MI
MN
WI
STATE_AVG
----------5 12:31:8.250
5 12:31:8.250
5 12:31:8.250
3 12:7:43.250
4 12:31:8.250
Если вы посмотрите на результаты вывода из DBMS Output , то вы заметите несколько интересных деталей. На
однопроцессорной машине вы увидите одно вхождение NULL INIT за которым следует несколькоNON-NULL INIT.
Это говорит о том, что Oracle Database создает один контекст агрегации и затем повторно использует этот
контекст для каждого разбиения определяемого опцией OVER аналитической функции.
МетодODCIAggregateInitialize в Листинге 3 использует это. Когда первый параметр не является пустым (non-null),
то метод не создает новый объект, а вместо этого повторно инициализирует существующий объект.
Вы также можете заметить достаточно много завершающих вызовов, больше, чем вы ожидаете,
это происходит потому что Oracle может вызывать функцию завершения, чтобы снова получить среднее значение
для каждой выводимой строки. В большинстве из этих завершающих вызовов аргумент флагов (смотрите Листинг
3) будет установлен в 1 (ODCIConst.AGGREGATE_REUSE_CTX), указывая, что вы ожидаете,
что OracleDatabase будет использовать контекст повторно.
Поддержка аналитического кадра
Синтаксис аналитических функций включает поддержку агрегации над скользящим окном данных. Следующий
запрос использует такое условие кадра (framing clause) для сравнения каждого значения срока проката с текущим
средним для окна, состоящего из трех периодов с центром в текущем значении срока проката. Условие кадра
выглядит так
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
Скользящее окно, таким образом, состоит из текущей рассматриваемой записи, одной записи непосредственно
предшествующей текущей и одной записи непосредственно следующей за текущей. Вот полный запрос:
SELECT
rental_in - rental_out time,
rental_state st,
avg_interval
(rental_in - rental_out)
OVER (PARTITION BY rental_state
ORDER BY rental_out
ROWS BETWEEN 1 PRECEDING
AND 1 FOLLOWING)
state_avg
FROM car_rental;
Oracle Database может выполнить этот запрос с использованием методов определенных в Листингах 2 и
3. Однако, при движении такого окна каждое перемещение приводит к новой группировке, которая должна быть
вычислена «с нуля». Вы можете повысить эффективность написав метод для удаления значения из
агрегирования позволяя, тем самым, Oracle Database сдвигать окно удаляя значения с заднего конца диапазона и
добавляя значения в передний конец диапазона. Метод, который вам нужен, называется ODCIAggregateDelete.
Вот его объявление, которое необходимо добавить к спецификации типа в Листинге 2 (не забудьте отделить его
от других объявлений запятой):
MEMBER FUNCTION ODCIAggregateDelete
( self IN OUT AvgInterval,
val
IN
DSINTERVAL_UNCONSTRAINED
) RETURN NUMBER
А вот его реализация, которую нужно разместить в теле типа (Листинг 3):
MEMBER FUNCTION ODCIAggregateDelete
( self IN OUT AvgInterval,
val
IN
DSINTERVAL_UNCONSTRAINED
) RETURN NUMBER IS
BEGIN
DBMS_OUTPUT.PUT_LINE
('Delete ' || TO_CHAR(val));
IF val IS NULL THEN
/* Will never happen */
DBMS_OUTPUT.PUT_LINE
('Null on delete');
END IF;
self.runningSum :=
self.runningSum - val;
self.runningCount :=
self.runningCount - 1;
RETURN ODCIConst.Success;
END;
Теперь, как только окно ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING изменяется для каждой
обработанной запросом строки, Oracle Database будет делать вызов метода ODCIAggregateDelete для удаления
интервала для строки, уходящей из окна, за которым следует вызов метода ODCIAggregateIterate для добавления
интервала для следующей строки, поступающей в окно. Это более эффективный подход, чем агрегирование с
нуля для каждого перемещения окна.
Будь креативным
Пользовательские статистические функции открывают дверь в интересный мир новых возможностей. Вы можете
написать функцию SUM, которая возвращает 0 вместо пустого значения (null), когда в группе не ни одной строки
для обработки. Вы можете написать функцию MUL для генерации произведения группы чисел путем их
перемножения. Вы даже можете очень легко и просто решить проблему всех строковых значений в группе.
Пользовательские статистические функции могут быть мощным инструментом для решения проблем в SQL.
Подбор материала, перевод и подготовка статьи
Игорь Изварин
Download