суббота, 18 июля 2015 г.

Коллекции в Oracle PL/SQL

Кандидатам на позиции, связанные с программированием на PL/SQL, на собеседованиях я задаю вопроc: "Существует ли в языке PL/SQL структура, подобная массиву? Если да, то расскажите о ней."

Из пары дестков кандидатов, с которыми довелось беседовать за три последних года, подавляющее большинство более или менее уверенно рассказывали об index-by table. Только один кандидат рассказал обо всех трех разновидностях коллекций в PL/SQL:

  • ассоциативный массив, или index-by table,
  • вложенная таблица, или nested table, и
  • массив переменной длинны, или varray.

О них и пойдет речь в этой статье.

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

А их особенности приведены в следующей таблице:

ХарактеристикаАссоциативный массивВложенная таблицаМассив переменной длины
Количество элементовне ограниченоне ограниченоограничено при определении
Тип индексаVARCHAR2 или PLS_INTEGERцелое число, начиная с 1целое число, начиная с 1
Может быть разреженным?дада, если удалить элементынет
Может быть NULL?нет, будет пустымдада
Как инициализируется?при объявлении переменнойконструкторомконструктором
Где определяется?на уровне пакета или локальнона уровне схемы, пакета или локальнона уровне схемы, пакета или локально
Может быть атрибутом объектного типа?нетда, если определен на уровне схемыда, если определен на уровне схемы
Можно сохранить в таблице?нетда, если определен на уровне схемыда, если определен на уровне схемы

Прежде чем пояснить и проиллюстрировать особенности коллекций, приведу методы коллекций:

Метод коллекцииЧто делаетОграничение
COUNTвозвращает кол-во элементов коллекции
DELETE[(index)]удаляет элемент с индексом index, или все элементы, если index не указан
EXISTS(index)проверяет, существует ли элемент с индексом index
EXTEND[(number [, index])]добавляет в конец коллекции number элементов со значением NULL или со значением элемента index, или один NULL элемент, если не указаны number и indexне используется с ассоциативным массивом
FIRSTвозвращает индекс первого элемента
LASTвозвращает индекс последнего элемента
LIMITвозвращает максимально возможное кол-во элементов коллекции для varray и NULL для nested tableне используется с ассоциативным массивом
NEXT(index)возвращает индекс следующего элемента
PRIOR(index)возвращает индекс предыдущего элемента
TRIM[(number)]удаляет number последних элементов коллекции, или один, если number не указанне используется с ассоциативным массивом

Разреженность коллекции означает, что при последовательном переборе всех числовых значений индекса от первого (метод FIRST) до последнего (метод LAST) для некоторых промежуточных значений индекса в коллекции может не быть элементов. Поскольку ассоциативный массив и вложенная таблица могут быть разреженными, то

  • для проверки существования элемента нужно использовать метод EXISTS,
  • для получения следующего индекса - метод NEXT,
  • для получения предыдущего индекса - метод PRIOR.

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

set serveroutput on

declare
    type t_aa_list is table of varchar2(50) index by pls_integer;
    l_aa_list t_aa_list; -- can't be initialized with constructor
    i pls_integer;
begin
    dbms_output.put_line('-- index-by table --');

    -- index-by table is not null even before initialization
    dbms_output.put_line('Index-by table before init is ' ||
        case when l_aa_list is null then 'NULL' else 'NOT NULL' end);
    
    l_aa_list(9) := 'zyx';
    l_aa_list(5) := 'asdf';
    l_aa_list(3) := 'qwerty';
    
    l_aa_list.delete(5);
    dbms_output.put_line('Element 5 after deletion ' ||
        case when l_aa_list.exists(5) then 'exists' else 'DOES NOT exist' end);
    
    -- retrieved in a sorted order!
    i := l_aa_list.first;
    while i is not null loop
        dbms_output.put_line(i || ' : ' || l_aa_list(i));
        i := l_aa_list.next(i);
    end loop;
    
    dbms_output.put_line('count: '|| l_aa_list.count);
    l_aa_list.delete; -- delete all elements
    dbms_output.put_line('count: '|| l_aa_list.count);
    if l_aa_list is null then
        dbms_output.put_line('No! I don''t believe it!');
    end if;
end;
/
-- index-by table --
Index-by table before init is NOT NULL
Element 5 after deletion DOES NOT exist
3 : qwerty
9 : zyx
count: 2
count: 0
PL/SQL procedure successfully completed

Следующий пример демонстрирует свойства и методы вложенной таблицы. В отличие от ассоциативного массива, элементы в который добавляются просто присваиванием значения элементу с новым индексом, для добавления элементов во вложенную таблицу необходимо вызывать метод EXTEND (и затем присваивать добавленным элементам значения):

declare
    type t_nt_list is table of varchar2(50);
    l_nt_list t_nt_list;
    i pls_integer;
begin
    dbms_output.put_line('-- nested table ----');

    -- nested table is null before initialization
    dbms_output.put_line('Nested table before init is ' ||
        case when l_nt_list is null then 'NULL' else 'NOT NULL' end);

    l_nt_list := t_nt_list('qwerty', 'asdf', 'xyz');

    l_nt_list.delete(2); -- delete makes nested table sparse!
    dbms_output.put_line('Element 2 after deletion ' ||
        case when l_nt_list.exists(2) then 'exists' else 'DOES NOT exist' end);

    dbms_output.put_line('limit: '|| l_nt_list.limit);
    --l_nt_list(4) := 'hello'; -- ORA-06533: Subscript beyond count
    -- unlike varray, there is no limit to extend nested table!
    l_nt_list.extend(2);
    l_nt_list(4) := 'hello';
    l_nt_list(5) := 'world';

    i := l_nt_list.first;
    while i is not null loop
        dbms_output.put_line(i || ' : ' || l_nt_list(i));
        i := l_nt_list.next(i);
    end loop;

    dbms_output.put_line('count: '|| l_nt_list.count);
    l_nt_list.trim(2); -- delete 2 last elements
    dbms_output.put_line('count: '|| l_nt_list.count);
    l_nt_list.delete; -- delete all elements
    dbms_output.put_line('count: '|| l_nt_list.count);
    if l_nt_list is null then
        dbms_output.put_line('No! I don''t believe it!');
    end if;
end;
/
-- nested table ----
Nested table before init is NULL
Element 2 after deletion DOES NOT exist
limit: 
1 : qwerty
3 : xyz
4 : hello
5 : world
count: 4
count: 2
count: 0
PL/SQL procedure successfully completed

Следующий пример демонстрирует свойства и методы массива переменной длины. Если во вложенную таблицу после ее инициализации можно добавить сколько угодно новых элементов, то максимальный размер массива переменной длины ограничен при его определении.

declare
    type t_va_list is varray(3) of varchar2(50);
    l_va_list t_va_list;
    i pls_integer;
begin
    dbms_output.put_line('-- varray ----------');

    -- varray is null before initialization
    dbms_output.put_line('Varray before init is ' ||
        case when l_va_list is null then 'NULL' else 'NOT NULL' end);

    l_va_list := t_va_list('qwerty', 'asdf');
    -- l_va_list.delete(2); -- doesn't work with varray which is always dense!
    l_va_list(2) := null;
    dbms_output.put_line('Element 2 after assigning null ' ||
        case when l_va_list.exists(2) then 'exists' else 'DOES NOT exist' end);

    dbms_output.put_line('limit: '|| l_va_list.limit);
    --l_va_list(4) := 'hello'; -- ORA-06533: Subscript beyond count
    l_va_list.extend;
    l_va_list(3) := 'hello';
    -- extend no further than varray's limit!
    --l_va_list.extend; -- ORA-06532: Subscript outside of limit

    -- traverse varray in reverse order
    i := l_va_list.last;
    while i is not null loop
        dbms_output.put_line(i || ' : ' || l_va_list(i));
        i := l_va_list.prior(i);
    end loop;

    dbms_output.put_line('count: '|| l_va_list.count);
    l_va_list.trim(2); -- delete 2 last elements
    dbms_output.put_line('count: '|| l_va_list.count);
    l_va_list.delete; -- delete all elements
    dbms_output.put_line('count: '|| l_va_list.count);
    if l_va_list is null then
        dbms_output.put_line('No! I don''t believe it!');
    end if;
end;
/
-- varray ----------
Varray before init is NULL
Element 2 after assigning null exists
limit: 3
3 : hello
2 : 
1 : qwerty
count: 3
count: 1
count: 0
PL/SQL procedure successfully completed

Поскольку массив переменной длины не может быть разреженным, то для обхода всех его элементов можно безопасно использовать цикл FOR i IN <varray>.FIRST .. <varray>.LAST LOOP.

Теперь рассмотрим работу со столбцами таблиц, в которых хранятся вложенные таблицы и массивы переменной длины. (Хранить в таблице ассоциативные массивы нельзя.)

Прежде всего, определю соответствующие коллекционные типы и таблицу со столбцами этих типов:

SQL> CREATE TYPE t_nt_names IS TABLE OF VARCHAR2(50);
  2  /
Type created

SQL> 
SQL> CREATE TYPE t_va_names IS VARRAY(3) OF VARCHAR2(50);
  2  /
Type created

SQL> 
SQL> create table ay_test (
  2      name varchar2(30) not null,
  3      aliases  t_va_names,
  4      friends t_nt_names
  5  ) nested table friends store as ay_friends;
Table created

SQL> select table_name, nested from user_tables where table_name like 'AY%';

TABLE_NAME                     NESTED
------------------------------ ------
AY_FRIENDS                     YES
AY_TEST                        NO

Как видим, для хранения вложенных таблиц - значений столбца FRIENDS - одновременно с таблицей AY_TEST "за кулисами" была создана таблица AY_FRIENDS. А вот для хранения значений массива переменной длины отдельная таблица не требуется, - значения столбца ALIASES будут храниться непосредственно в таблице AY_TEST.

Вставим запись в таблицу AY_TEST и посмотрим, что получилось:

SQL> insert into ay_test (name, aliases, friends)
  2  values (
  3      'Andrei',
  4      t_va_names('Andrey', 'Andrew'),
  5      t_nt_names('Katya', 'Ivan')
  6  );
1 row inserted

SQL> select * from ay_test;

NAME                 ALIASES                        FRIENDS
-------------------- ------------------------------ ----------------------------
Andrei               T_VA_NAMES('Andrey', 'Andrew') T_NT_NAMES('Katya', 'Ivan')

SQL> select * from ay_friends;

ORA-22812: cannot reference nested table column's storage table

Последний запрос показывает, что выборка прямо из таблицы, в которой хранятся значения столбца типа nested table, невозможна. Для работы с вложенной таблицей и ее отдельными строками используется конструкция TABLE():

SQL> select * from table(select friends from ay_test where name = 'Andrei');
COLUMN_VALUE
--------------------------------------------------------------------------------
Katya
Ivan

SQL> update table(select friends from ay_test where name = 'Andrei')
  2  set column_value = 'Катя'
  3  where column_value = 'Katya';
1 row updated

SQL> select * from table(select friends from ay_test where name = 'Andrei');
COLUMN_VALUE
--------------------------------------------------------------------------------
Катя
Ivan

SQL> insert into table(select friends from ay_test where name = 'Andrei')
  2  values ('Маугли');
1 row inserted

SQL> select * from table(select friends from ay_test where name = 'Andrei');
COLUMN_VALUE
--------------------------------------------------------------------------------
Катя
Ivan
Маугли

SQL> delete from table(select friends from ay_test where name = 'Andrei')
  2  where column_value = 'Маугли';
1 row deleted

SQL> select * from table(select friends from ay_test where name = 'Andrei');
COLUMN_VALUE
--------------------------------------------------------------------------------
Катя
Ivan

Теперь проделаю аналогичные манипуляции со столбцом ALIASES, который хранит массивы переменной длины.

SQL> select * from table(select aliases from ay_test where name = 'Andrei');
COLUMN_VALUE
--------------------------------------------------------------------------------
Andrey
Andrew

SQL> update table(select aliases from ay_test where name = 'Andrei')
  2  set column_value = 'Андрей'
  3  where column_value = 'Andrew';

ORA-25015: cannot perform DML on this nested table view column

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

SQL> declare
  2      l_aliases t_va_names;
  3  begin
  4      -- retrieve column value
  5      select aliases into l_aliases from ay_test where name = 'Andrei';
  6  
  7      -- modify local varray
  8      for i in l_aliases.first .. l_aliases.last loop
  9          if l_aliases(i) = 'Andrew' then
 10              l_aliases(i) := 'Андрей';
 11          end if;
 12      end loop;
 13      l_aliases.extend;
 14      l_aliases(3) := 'Andorei'; -- in Japanese
 15  
 16      -- update column value
 17      update ay_test set aliases = l_aliases where name = 'Andrei';
 18  end;
 19  /
PL/SQL procedure successfully completed

SQL> select * from table(select aliases from ay_test where name = 'Andrei');
COLUMN_VALUE
--------------------------------------------------------------------------------
Andrey
Андрей
Andorei
Покажу, что конструкция TABLE() работает с вложенными таблицами и массивами переменной длины, инициализированными на месте:
SQL> select * from table(t_va_names('hello', 'world'));
COLUMN_VALUE
--------------------------------------------------------------------------------
hello
world

SQL> select * from table(t_nt_names('hello', 'world', '!'));
COLUMN_VALUE
--------------------------------------------------------------------------------
hello
world
!

С ассоциативным массивом TABLE() не работает. Более того, с ассоциативными массивами нельзя работать в SQL, можно только в PL/SQL. Ассоциативный массив нельзя инициализировать подобно тому, как инициализируются varray и nested table - у него нет инициализирующего конструктора.

Итак, были рассмотрены три разновидности коллекций PL/SQL и особенности работы с ними. За рамками статьи осталось наполнение коллекций с помощью SELECT ... BULK COLLECT INTO ... и использование коллекций с конструкцией FORALL для многократного выполнения одной команды DML с разными значениями связанных переменных. (Это был намек, с чем еще стоит поэкспериментировать самостоятельно.)

В заключение, удаляю следы моих экспериментов из БД:

SQL> drop table ay_test;
Table dropped

SQL> select * from user_tables where table_name like 'AY%';
No rows selected

SQL> drop type t_nt_names;
Type dropped

SQL> drop type t_va_names;
Type dropped

Как видим, вместе с таблицей AY_TEST была удалена и таблица AY_FRIENDS.

1 комментарий: