Август/Сентябрь 2003


Тема номера: Объектно-реляционная технология Oracle9i


Патрик Барель

Использование подтипов
(Using Subtypes,
by Patrick Barel)

Источник: Oracle Professional, March 2003, http://www.pinnaclepublishing.com/op/OPmag.nsf/
WebIndexByIssueSecure/ab81e6b7bba8764485256ce100676a60!OpenDocument&Login

При работе с большими объемами данных значительная часть времени тратится на создание основного цикла с курсором, который запрашивает столько данных, сколько требуется извлечь за один раз. Часто из-за этого приходится строить курсор на множестве таблиц, что существенно влияет на производительность. Однако данные можно запрашивать, когда они требуются, и хранить в кэше до тех пор, пока такие же данные не потребуются опять. В этой статье Патрик Барель рассказывает, как подтипы могут помочь при создании пакета, используемого для накопления данных в коллекции.

Предположим, из таблицы EMP требуется извлечь все записи сразу, а из таблицы DEPT только тогда, когда они требуются. После извлечения записи из таблицы DEPT сохраним запись в коллекции, например, PL/SQL-таблице, чтобы когда она потребуется в следующий раз, доступ к ней был проще. В обычном случае необходимо создать тип записи для хранения кэшируемых данных, а затем коллекцию, основанную на этом типе.

Рассмотрим следующую часть кода:

 -- тип записи для хранения кэшируемых данных
 TYPE dept_rt IS RECORD(
  dname dept.dname%TYPE
 );

 -- таблица для хранения кэшируемых данных
 TYPE dept_tt IS TABLE OF dept_rt
  INDEX BY BINARY_INTEGER;

 -- пакетная переменная для хранения кэшируемых данных
pv_dept dept_tt;

Сначала создается тип записи с полями, которые необходимо кэшировать. Затем создается коллекция (PL/SQL-таблица), основанная на этом типе. Затем создается глобальная переменная для хранения данных. Коллекция заполняется по мере необходимости при помощи функции, похожей на функцию Листинга 1.

Листинг 1. Private-функция для извлечения данных из коллекции или базы данных.

FUNCTION ppf_get_dept( pi_deptno IN dept.deptno%TYPE ) 
  RETURN dept_rt
 IS
  CURSOR c_dept( p_deptno dept.deptno%TYPE )

  IS
   SELECT dept.dname
    FROM dept
   WHERE dept.deptno = p_deptno;

  lv_returnvalue             dept_rt;
 BEGIN
  BEGIN
   lv_returnvalue  := pv_dept( pi_deptno );
  EXCEPTION
   WHEN NO_DATA_FOUND THEN

    --== данных еще нет к коллекции ==--
    --== возвращаемся в базу данных,     ==--
    --== и запрашиваем необходимые данные   ==--
    IF c_dept%ISOPEN THEN CLOSE c_dept; END IF;

    OPEN c_dept( p_deptno => pi_deptno );
    FETCH c_dept INTO lv_returnvalue;

    IF c_dept%FOUND THEN
     --== если запись найдена,    ==--
     --== добавляем ее в коллекцию ==--
     pv_dept( pi_deptno )  := lv_returnvalue;

    ELSE
   --== выдаем ошибку, т.к. данные не найдены ==--
     raise_application_error(
     -20001, 'Отдел не найден' );
    END IF; -- c_dept%found

    IF c_dept%ISOPEN THEN CLOSE c_dept; END IF;
  END;
  --== возвращаем запись, которая могла быть извлечена как из ==--
  --== коллекции, так и из базы данных ==--
  RETURN lv_returnvalue;
 END ppf_get_dept;

Этот код выглядит как хорошее решение. Но что произойдет, если потребуется кэшировать другие данные таблицы? Тогда придется изменять, как описание записи, так и курсор, используемый в функции, что показано ниже:

 TYPE dept_rt IS RECORD(
   dname dept.dname%TYPE
  , loc  dept.loc%TYPE --<-- добавлено
  );
  ...
  CURSOR c_dept( p_deptno dept.deptno%TYPE )
  IS
   SELECT dept.dname
     , dept.loc --<-- добавлено
    FROM dept
   WHERE dept.deptno = p_deptno;

Это не является очень большой проблемой для демонстрационного пакета, так как код можно легко просмотреть и найти места, в которых следует внести изменения. Однако, если описания расположены далеко друг от друга, то легко забыть одно из изменений. Компилятор хотя и обнаруживает ошибку, но не всегда точно указывает, в чем она заключается.

Использование подтипов

Чтобы решить эту "проблему", можно создать глобальный курсор для извлечения данных из базы данных. Затем создать тип, основанный на %ROWTYPE курсора. Для этого необходимо использование описания SUBTYPE. Рассмотрим следующий код: CURSOR c_dept( p_deptno dept.deptno%TYPE ) IS SELECT dept.dname FROM dept WHERE dept.deptno = p_deptno; -- Тип записи для хранения кэшируемых данных SUBTYPE dept_rt IS c_dept%ROWTYPE; -- Табличный тип для хранения кэшируемых данных TYPE dept_tt IS TABLE OF dept_rt INDEX BY BINARY_INTEGER; -- пакетная переменная для хранения кэшируемых данных pv_dept dept_tt;

Код private-функции будет таким же, за исключением объявления курсора, которое перенесено в раздел объявления глобальных переменных тела пакета.

Теперь, если потребуется добавить еще одно поле в коллекцию, необходимо модифицировать только описание курсора, как показано ниже:

CURSOR c_dept( p_deptno dept.deptno%TYPE )
 IS
  SELECT dept.dname
    , dept.loc -- добавлено
   FROM dept
  WHERE dept.deptno = p_deptno;

Таким образом, я упростил себе (и, конечно, другим) поддержку пакета.

Заметьте, что скрипты для создания пакетов базы данных, использующих подтипы, называются subtype_demo.spk, subtype_demo2.spk, subtype_demo3.spk и subtype_demo4.spk.

Производительность подтипов

Если вы решите сравнить время выполнения обычного курсора, построенного на соединении таблиц EMP и DEPT, с версией, когда данные из таблицы DEPT содержатся в коллекции, то обнаружите, что "кэш"-версия медленнее. Существуют, однако, случаи, когда она может быть весьма удобной. Предположим, например, что обрабатываются две таблицы – одна содержит подписки на журнал, а другая используется для связи с подписчиками. Таблица subscription (подписка) состоит из 100,000 записей, а связанная таблица состоит из 400,000 записей. Для 100,000 подписок имеется примерно 50,000 различных записей в связанной таблице. При простом соединении двух таблиц, извлечение всех записей занимает значительное время – иногда около 45 секунд. Если создать версию с механизмом кэширования, то для извлечения тех же данных потребуется только 15 секунд, однако большинство связанных данных будут возвращены из переменной-коллекции.

Другое использование подтипов

Подтипы могут также использоваться для создания типов, основанных на базовых типах, но с заданной точностью. Например, когда требуется вывести данные на экран, можно использовать DBMS_OUTPUT.PUT_LINE. Параметр этой процедуры - символьная строка максимальной длины 255. Чтобы знать это, не требуется запоминать максимальную длину, если создать подтип output:

SUBTYPE output IS VARCHAR2(255);

Затем, когда надо написать код, можно создать переменную, основанную на этом подтипе, и передать ее в DBMS_OUTPUT.PUT_LINE:

DECLARE
   SUBTYPE output IS VARCHAR2(255);
   lv_output output;

  BEGIN
   lv_output := 'Эта переменная - подтип'
   dbms_output.put_line(lv_output);
  END;

Еще подтипы можно использовать, когда надо убедиться, что переменная, представляющая количество денег, всегда будет длиной 20 цифр с точностью 2. Для этого создается подтип money:

SUBTYPE money IS NUMBER(20,2);

Это тривиальные примеры, которые, однако, могут сделать код более документируемым. Рассмотрим две функции, показанные на Листинге 2.

Листинг 2. Пакет subtype_test.

CREATE OR REPLACE PACKAGE subtype_test
 IS
  SUBTYPE money IS NUMBER( 20, 2 );

  FUNCTION add( pi_amount IN NUMBER, pi_add IN NUMBER )
   RETURN NUMBER;


  FUNCTION add2( pi_amount IN money, pi_add IN money )
   RETURN money;
 END subtype_test;

 CREATE OR REPLACE PACKAGE BODY subtype_test
 IS
  FUNCTION add( pi_amount IN NUMBER, pi_add IN NUMBER )
   RETURN NUMBER
  IS
  BEGIN
   RETURN( pi_amount + pi_add );
  END;

  FUNCTION add2( pi_amount IN money, pi_add IN money )
   RETURN money
  IS
  BEGIN
   RETURN( pi_amount + pi_add );
  END;

 END subtype_test;

Эти функции выполняют абсолютно одинаковые задачи; различие только во входных переменных и возвращаемом типе. При вызове функции add во входном параметре можно передать любое число, и возвратится число с максимальной точностью. При вызове функции add2 во входном параметре можно передать те же параметры, но возвратится значение с точностью, указанной в подтипе.

SET SERVEROUTPUT ON
 DECLARE
 lv_TEST NUMBER;
 BEGIN
 lv_test := subtype_test.add(10.125,2.2542);
 dbms_output.put_line('number : '||to_char(lv_test));
 lv_test := subtype_test.add2(10.125,2.2542);
 dbms_output.put_line('money : '||to_char(lv_test));
 END;
 /

Результат:

number : 12.3792
 money : 12.38

Во второй функции не надо объявлять переменную, округленную до 2 знаков для соответствия количеству денег, т.к. это подразумевается в подтипе. Кроме того, вторая функция говорит, как следует из спецификации, что ожидается входной параметр типа money, и в результате получаются тоже money.

По примеру Oracle ...

Oracle использует в PL/SQL множество подтипов. Если посмотреть на спецификацию пакета SYS.STANDARD, то обнаружится:

type NUMBER is NUMBER_BASE;

Это базовый числовой тип. От него Oracle производит множество различных числовых типов, таких как:

subtype FLOAT is NUMBER;
 subtype REAL is FLOAT;
  subtype "DOUBLE PRECISION" is FLOAT;

  -- другие числовые подтипы
  subtype INTEGER is NUMBER(38,0);
  subtype INT is INTEGER;
  subtype SMALLINT is NUMBER(38,0);
  subtype DECIMAL is NUMBER(38,0);
  subtype NUMERIC is DECIMAL;
 subtype DEC is DECIMAL;

Отсюда видно, что в Oracle есть один базовый числовой тип (number_base), а все подтипы – это остальные типы данных, которые мы знаем (и даже, может быть, не знаем), которые основаны на базовом типе. То же самое происходит с символьными типами, типами даты и т.п. Просмотрите этот пакет, если хотите знать больше о работе Oracle и какие типы данных можно использовать в PL/SQL-коде. В этом пакете можно также найти описание множества функций, которые используются ежедневно, таких как nvl и to_number, и многие другие. Например, знаете ли вы, какое описание имеет функция USER? (Вот именно, это функция!)

function USER return varchar2 is
  c varchar2(255);
  begin
     select user into c from sys.dual;
     return c;
  end;

Ограничения

Подтипы могут быть основаны на cursor%rowtype, table%rowtype, table.field%type, скалярных типах, а внутри программы - на жестком ref-курсоре. Если подтип объявить в спецификации, то его можно использовать в любом месте программы, добавляя к нему префикс - имя пакета.

Заключение

Кэширование данных может существенно повысить производительность, так как не требуется переключаться и обращаться к базе данных. Использование подтипов упрощает поддержку пакета и делает его более прозрачным, однако всегда следует проверять, что быстрее: соединение таблиц или Кэширование данных одной из таблиц в коллекции. Если версия с использованием коллекции хотя бы немного быстрее, то я рекомендую выбрать это решение, так как данные имеют тенденцию расти, и вы обнаружите, что разница в производительности будет больше при использовании больших объемов данных. Использование подтипов может сделать код более документированным и более легким в поддержке. А если изменяется точность переменной, то исправление вносится только в одном месте.

E-mail this page