четверг, 9 октября 2014 г.

Операции над множествами в СУБД Oracle

В СУБД Oracle операции над множествами можно выполнять, как минимум, в трех контекстах с помощью трех различных механизмов. Это

  1. операции над битовыми полями,
  2. операции над вложенными таблицами,
  3. операции над множествами строк таблиц.

В этом порядке они и будут рассмотрены ниже, но сначала - суперкраткое введение в понятие множества.

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

  • {1,2,3} - это множество, элементы которого числа 1, 2, 3.
  • {1,2,3,2} - это не множество, поскольку число 2 встречается два раза.
  • {} - это пустое множество, не содержащее элементов.

Одни множества могут быть подмножествами других множеств:

  • {1,2,3} есть подмножество множества {1,2,3,4,5}.
  • {1,2,3} есть подмножество множества {1,2,3}, поскольку каждое множество является подмножеством самого себя.
  • {} есть подмножество всякого множества.

Важно, что порядок элементов множества не задан. Поэтому {1,2,3}, {2,3,1} и {3,2,1} - это три обозначения одного и того же множества.

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

  • объединение двух множеств дает множество, элементами которого являются элементы обеих множеств-операндов, например:
    • объединение {1,2,3} и {4,5,6} есть {1,2,3,4,5,6}
    • объединение {1,2,3} и {3,4,5} есть {1,2,3,4,5}
    • объединение {} и {1,2,3} есть {1,2,3}
  • пересечение двух множеств дает множество, все элементы которого входят в каждое из множеств-операдов:
    • пересечение {1,2,3} и {4,5,6} есть {}
    • пересечение {1,2,3} и {3,4,5} есть {3}
    • пересечение {} и {1,2,3} есть {}
  • разность двух множеств дает множество, элементы которого входят в первое множество, но не входят во второе:
    • разность {1,2,3} и {4,5,6} есть {1,2,3}
    • разность {1,2,3} и {3,4,5} есть {1,2}
    • разность {} и {1,2,3} есть {}

Нам также понадобится понятие мультимножество (multiset). Мультимножество - это множество, в котором допускаются одинаковые элементы. Например, {1,2,3,2} или {1,1}. Все операции над множествами применимы также к мультимножествам.

Как представляются множества в компьютере?

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

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

  • бит 1, 0001 - яблоко
  • бит 2, 0010 - груша
  • бит 3, 0100 - абрикос
  • бит 4, 1000 - слива

Тогда

Биты16-ноеМножество
00000пустое множество {}
00113{яблоко, груша}
1010A{слива, груша}
1111F{яблоко, груша, абрикос, слива} - полное множество, подмножествами которого являются перечисленные выше.

При представлении множества в виде битового поля операции над множествами реализуются побитовыми логическими операциями над битовыми полями. Частным случаем битового поля является целочисленный тип, например, 32-х или 64-хразрядное целое, для которого определены побитовые операции.

Рассмотрим проверку, является ли одно множество подмножеством другого.

  • Является {абрикос,груша} подмножеством {груша, яблоко}?
    • (0110 И 0011) не равно 0110 - не является
  • Является {абрикос} подмножеством {абрикос, слива}?
    • (0100 И 1100) равно 1000 - является
  • Является {} подмножеством {абрикос, слива}?
    • (0000 И 1100) равно 0000 - является

Проверка вхождения единственного элемента в множество еще проще:

  • Входит ли абрикос в множество {груша, яблоко}?
    • (0100 И 0011) дает 0 - не является
  • Входит ли абрикос в множество {абрикос, слива}?
    • (0100 И 1100) дает отличное от 0 значение - является

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

  • Объединение {яблоко} и {груша, абрикос} есть {яблоко, груша, абрикос}
    • 0001 ИЛИ 0110 дает 0111
    • 1 ИЛИ 6 дает 7
  • Объединение {} и {яблоко, груша} есть {яблоко, груша}
    • 0000 ИЛИ 0011 дает 0011
    • 0 ИЛИ 3 дает 3
  • Пересечение {яблоко} и {груша, абрикос} есть {}
    • 0001 И 0110 дает 0000
    • 1 И 6 дает 0
  • Пересечение {яблоко, груша} и {яблоко, груша} есть {яблоко, груша}
    • 0011 И 0011 дает 0011
    • 3 И 3 дает 3
  • Разность {яблоко} и {груша, абрикос} есть {яблоко}
    • (0001 ИСКЛ.ИЛИ 0110) И 0001 дает 0001
    • (1 ИСКЛ.ИЛИ 6) И 1 дает 1
  • Разность {} и {яблоко, груша} есть {}
    • (0000 ИСКЛ.ИЛИ 0011) И 0000 дает 0000
    • (0 ИСКЛ.ИЛИ 3) И 0 дает 0

Как видим, разность множеств реализуется с помощью двух побитовых операций: вначале ИСКЛЮЧАЮЩЕЕ ИЛИ, затем И. Операция ИСКЛЮЧАЮЩЕЕ ИЛИ сама по себе дает симметричную разность двух множеств-операндов, а последующая операция И исключает из результата биты, отсутствующие в первом множестве.

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

СУБД Oracle, предоставляя пользователю язык программирования четвертого поколения SQL, в то же время тщательно скрывает физические структуры представления данных. Поэтому побитовые операции в СУБД Oracle нужно еще поискать. Но поиск вознаграждается находками:

  • функции пакета UTL_RAW для операндов с типом данных RAW,
  • побитовые операции языка Java, исполняемые встроенной JVM.

Последний вариант, как более экзотический по сравнению в первым, я здесь не стану рассматривать. Хотя не составляет большого труда написать класс на языке Java со статическими методами, реализующими побитовые операции над целочисленными параметрами, и опубликовать эти методы через PL/SQL функции-обертки (см. мои статьи Java в СУБД Oracle).

Продемонстрирую побитовые "операции над множествами", реализованные в пакете UTL_RAW. Операнды и результаты функций bit_or, bit_xor и bit_and имеют тип RAW и их значения представлены в шестнадцатеричном виде:

SQL> -- Объединение {яблоко} и {груша,абрикос} есть {яблоко, груша, абрикос}
SQL> SELECT utl_raw.bit_or('01', '06') FROM dual;

UTL_RAW.BIT_OR('01','06')
--------------------------------------------------------------------------------
07

SQL> -- Пересечение {яблоко} и {яблоко, груша} есть {яблоко}
SQL> SELECT utl_raw.bit_and('01', '03') FROM dual;

UTL_RAW.BIT_AND('01','03')
--------------------------------------------------------------------------------
01

SQL> -- Разность {яблоко} и {груша, абрикос} есть {яблоко}
SQL> SELECT utl_raw.bit_and(utl_raw.bit_xor('01', '06'), '01') FROM dual;

UTL_RAW.BIT_AND(UTL_RAW.BIT_XO
--------------------------------------------------------------------------------
01

SQL>  -- симметричная разность {яблоко} и {груша, абрикос} есть {яблоко, груша, абрикос}
SQL> SELECT utl_raw.bit_xor('01', '06') FROM dual;

UTL_RAW.BIT_XOR('01','06')
--------------------------------------------------------------------------------
07

Проверка, является ли множество подмножеством другого, выполняется так:

SQL> SELECT 1 FROM dual WHERE utl_raw.bit_and('03', '07') = '03';
         1
----------
         1

Проверка, имеют ли два множества хотя бы один общий элемент:

SQL> SELECT 1 FROM dual WHERE utl_raw.bit_and('03', '05') != '00';
         1
----------
         1

Другой контекст, в котором мы встречаем операции над множествами, а точнее, над мультимножествами, это операции над вложенными таблицами (nested tables). Вложенные таблицы - один из коллекционных типов данных в Oracle SQL (TABLE OF ...). Другие коллекционные типы данных - ассоциативный массив (TABLE OF ... INDEX BY ...) и массив переменной длины (VARRAY(N) OF ...).

Начиная с версии 10 СУБД Oracle поддерживает следующие операции над вложенными таблицами:

  • MULTISET UNION [ALL|DISTINCT]- объединение мультимножеств,
  • MULTISET INTERSECT [ALL|DISTINCT]- пересечение мультимножеств,
  • MULTISET EXCEPT [ALL|DISTINCT]- разность мультимножеств.

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

Создам тип number_nt и продемонстрирую операции над множествами, представленными с помощью вложенных таблиц.

SQL> CREATE TYPE number_nt AS TABLE OF NUMBER;
  2  /
Type created

SQL> -- инициализация вложенной таблицы при ее создании
SQL> SELECT number_nt(1,2,3,4,5) FROM dual;

NUMBER_NT(1,2,3,4,5)
--------------------------------------------------------------------------------
NUMBER_NT(1, 2, 3, 4, 5)

SQL> -- объединение {1,2,3} и {3,4,5} есть мультимножество {1,2,3,3,4,5}
SQL> SELECT number_nt(1,2,3) MULTISET UNION number_nt(3,4,5) FROM dual;

NUMBER_NT(1,2,3)MULTISETUNIONNUMBER_NT(3,4,5)
--------------------------------------------------------------------------------
NUMBER_NT(1, 2, 3, 3, 4, 5)

SQL> -- объединение {1,2,3} и {3,4,5} есть множество {1,2,3,4,5}
SQL> SELECT number_nt(1,2,3) MULTISET UNION DISTINCT number_nt(3,4,5) FROM dual;

NUMBER_NT(1,2,3)MULTISETUNIONDISTINCTNUMBER_NT(3,4,5)
--------------------------------------------------------------------------------
NUMBER_NT(1, 2, 3, 4, 5)

SQL> -- пересечение {1,2,3,3} и {3,3,4,5} есть мультимножество {3,3}
SQL> SELECT number_nt(1,2,3,3) MULTISET INTERSECT number_nt(3,3,4,5) FROM dual;

NUMBER_NT(1,2,3,3)MULTISETINTERSECTNUMBER_NT(3,3,4,5)
--------------------------------------------------------------------------------
NUMBER_NT(3, 3)

SQL> -- пересечение {1,2,3,3} и {3,3,4,5} есть множество {3}
SQL> SELECT number_nt(1,2,3) MULTISET INTERSECT DISTINCT number_nt(3,3,4,5) FROM dual;

NUMBER_NT(1,2,3)MULTISETINTERSECTDISTINCTNUMBER_NT(3,3,4,5)
--------------------------------------------------------------------------------
NUMBER_NT(3)

SQL> -- разность {1,2,2,3} и {3,4,5} есть мультимножество {1,2,2}
SQL> SELECT number_nt(1,2,2,3) MULTISET EXCEPT number_nt(3,4,5) FROM dual;

NUMBER_NT(1,2,2,3)MULTISETEXCEPTNUMBER_NT(3,4,5)
--------------------------------------------------------------------------------
NUMBER_NT(1, 2, 2)

SQL> -- разность {1,2,2,3} и {3,4,5} есть множество {1,2}
SQL> SELECT number_nt(1,2,2,3) MULTISET EXCEPT DISTINCT number_nt(3,4,5) FROM dual;

NUMBER_NT(1,2,2,3)MULTISETEXCEPTDISTINCTNUMBER_NT(3,4,5)
--------------------------------------------------------------------------------
NUMBER_NT(1, 2)

Добавлю, что не только вложенные таблицы, инициализированные, как в примерах выше, или извлеченные из столбцов таблиц БД, могут служить операндами для операций над мультимножествами. При помощи параметра MULTISET функции CAST можно результат команды SELECT привести к типу вложенной таблицы:

SQL> SELECT
  2      CAST(MULTISET(SELECT level lvl FROM dual CONNECT BY level < 5) AS number_nt)
  3      MULTISET UNION
  4      CAST(MULTISET(SELECT ROWNUM FROM all_users WHERE ROWNUM < 3) AS number_nt)
  5  FROM dual;

CAST(MULTISET(SELECTLEVELLVLFROMDUALCONNECTBYLEVEL<5)ASNUMBER_NT)MULTISETUNIONCA
--------------------------------------------------------------------------------
NUMBER_NT(1, 2, 3, 4, 1, 2)

Проверка, является ли мультимножество подмультимножеством (!) другого, выполняется с помощью оператора SUBMULTISET:

SQL> SELECT 1
  2  FROM dual
  3  WHERE number_nt(1,2) SUBMULTISET OF number_nt(1,2,3);

         1
----------
         1

SQL> SELECT 1
  2  FROM dual
  3  WHERE number_nt(1,2) SUBMULTISET OF number_nt(1,2);

         1
----------
         1

SQL> SELECT 1
  2  FROM dual
  3  WHERE number_nt() SUBMULTISET OF number_nt(1,2,3);

         1
----------
         1

Наконец, третий контекст, в котором в СУБД Oracle реализованы операции над множествами - это операции над множествами строк таблиц. Из трех рассматриваемых в этой статье контекстов работы с множествами данный контекст, пожалуй, в первую очередь встречается изучающим SQL.

В языке SQL множество строк таблицы, над которым выполняется некоторая операция, определяется через условие WHERE или через его отсутствие - в последнем случае операция выполняется над всеми строками таблицы.

SELECT * FROM all_users; все пользователи СУБД Oracle
SELECT * FROM all_users
WHERE created > add_months(sysdate, -12);
пользователи, созданные за последний год
SELECT * FROM all_users
WHERE 1 != 1;
пустое множество пользователей
SELECT created FROM all_users; мультимножество дат создания пользователей
SELECT DISTINCT created FROM all_users; множество дат - дубли подавлены

Операции объединения, пересечения и разности множеств в языке SQL задаются ключевыми словами UNION [ALL], INTERSECT и MINUS, соответственно:

SQL> -- объединение множеств есть множество
SQL> SELECT * FROM all_users WHERE username LIKE 'A%'
  2  UNION
  3  SELECT * FROM all_users WHERE username LIKE 'S%';

USERNAME                          USER_ID CREATED
------------------------------ ---------- -----------
ANONYMOUS                              35 27.08.2011
APEX_040000                            47 27.08.2011
APEX_PUBLIC_USER                       45 27.08.2011
AY                                     48 06.08.2014
SYS                                     0 27.08.2011
SYSTEM                                  5 27.08.2011

6 rows selected

SQL> -- UNION ALL дает мультимножество в результате
SQL> SELECT created FROM all_users WHERE username LIKE 'A%'
  2  UNION ALL
  3  SELECT created FROM all_users WHERE username LIKE 'S%';

CREATED
-----------
06.08.2014
27.08.2011
27.08.2011
27.08.2011
27.08.2011
27.08.2011

6 rows selected

SQL> -- пересечение множеств есть множество
SQL> SELECT * FROM all_users WHERE username LIKE 'S%'
  2  INTERSECT
  3  SELECT * FROM all_users WHERE username LIKE '%S';

USERNAME                          USER_ID CREATED
------------------------------ ---------- -----------
SYS                                     0 27.08.2011

SQL> -- разность множеств есть множество
SQL> SELECT * FROM all_users WHERE username LIKE '%S'
  2  MINUS
  3  SELECT * FROM all_users WHERE username LIKE 'S%';

USERNAME                          USER_ID CREATED
------------------------------ ---------- -----------
ANONYMOUS                              35 27.08.2011
CTXSYS                                 32 27.08.2011
FLOWS_FILES                            44 27.08.2011
MDSYS                                  42 27.08.2011

Как можно проверить равенство двух множеств? Множества равны, если их симметричная разность есть пустое множество:

SQL> (SELECT level lvl FROM dual CONNECT BY level < 5
  2  MINUS
  3  SELECT ROWNUM FROM all_users WHERE ROWNUM < 5)
  4  UNION
  5  (SELECT ROWNUM FROM all_users WHERE ROWNUM < 5
  6  MINUS
  7  SELECT level lvl FROM dual CONNECT BY level < 5)
  8  ;

no rows selected

Проверка на вхождение элемента в мультимножество строк, возвращаемых командой SELECT, выполняется так:

SQL> SELECT 1
  2  FROM dual
  3  WHERE 'SYSTEM' IN (SELECT username FROM all_users);

         1
----------
         1

SQL> SELECT 1
  2  FROM dual
  3  WHERE (2, 'SYSTEM') IN (SELECT user_id, username FROM all_users);

no rows selected

Команда SELECT может вернуть 0 строк, или пустое множество, что оказывается эквивалентно NULL:

SQL> SELECT 1
  2  FROM dual
  3  WHERE (SELECT username FROM all_users WHERE 1 != 1) IS NULL;

         1
----------
         1

Однако NULL не годится на роль пустого множества с оператором IN. Тогда как пустое множество является подмножеством всякого множества, NULL не входит ни в одно из множеств строк, возвращаемых SELECT:

SQL> SELECT 1
  2  FROM dual
  3  WHERE NULL IN (SELECT username FROM all_users);

no rows selected

SQL> SELECT 1
  2  FROM dual
  3  WHERE NULL IN (SELECT username FROM all_users WHERE 1 != 1);

no rows selected

Я разбирал некоторые особенности поведения NULL в статье Многоликий NULL.

На этом закончу рассмотрение операций над множествами в СУБД Oracle. Для экспериментов с вложенными таблицами выше я создал тип number_nt - думаю, он может пригодиться для самых разных приложений. Например, если мне понадобится таблица из нескольких конкретных чисел, я смогу написать:

SQL> SELECT column_value FROM TABLE(number_nt(1966, 1991, 1999, 2014));

COLUMN_VALUE
------------
        1966
        1991
        1999
        2014

Комментариев нет:

Отправить комментарий