|
|
|||||||||||||||||||||||||||||
|
Подробности OracleИсточник: oracle Александр Просторов
Этот текст задуман как сборник неочевидных фактов о сервере Oracle, полезных и интересных разработчику. Своим появлением он обязан в первую очередь компании "МВ" - в течение некоторого времени я читал там курс основ программирования для Oracle, и в ходе подготовки к нему выяснил, что несколько утверждений, которые я полагал очевидными фактами, на самом деле не только не очевидные, но даже и не факты. В результате я решил собрать в одном месте то, что выпадает из поля зрения при беглом знакомстве, но может привести к неожиданным и неприятным последствиям. Оглавление.
Двойная группировка. Как и следует ожидать, Oracle поддерживает использование агрегатных функций в двух режимах - совместно с предложением GROUP BY, либо без оного. В последнем случае вся исходная выборка считается одной группой, а результат всегда состоит из одной строки. Интересно же то, что кроме этих двух вариантов, сервер поддерживает вложенные вызовы агрегатных функций - как во втором операторе примера: SQL> select owner, count(*) from dba_objects where owner like '%SYS%' group by owner ; OWNER COUNT(*) ------------------------------ ---------- SYS 13681 SYSTEM 332 WMSYS 131 SQL> select max (owner), max (count(*)) from dba_objects where owner like '%SYS%' group by owner ; MAX(OWNER) MAX(COUNT(*)) ------------------------------ ------------- WMSYS 13681 Результат вложенного вызова вычисляется как если бы внешняя агрегатная функция применялась к результату подзапроса, содержащего внутреннюю агрегатную функцию и выражение GROUP BY. То есть, в примере сервер сначала вычисляет COUNT(*) в выборке, сгруппированной по схемам, а затем применяет функцию MAX к результатам этой выборки, уже без дополнительной группировки. Последовательности. Неожиданной особенностью последовательностей является то, что атрибут NEXTVAL, возвращающий очередное значение последовательности, вычисляется не при каждом использовании. Упомянутый в SQL-операторе, скажем, в SELECT, он вычисляется только один раз для каждой очередной строки результата. SQL> create sequence s1 ; Sequence created SQL> create sequence s2 increment by 2 ; Sequence created SQL> select s1.nextval, s1.currval, s1.nextval, s2.currval, s2.nextval, s2.nextval from all_objects where rownum <= 10; NEXTVAL CURRVAL NEXTVAL CURRVAL NEXTVAL NEXTVAL ------- ------- ------- ------- ------- ------- 1 1 1 1 1 1 2 2 2 3 3 3 3 3 3 5 5 5 4 4 4 7 7 7 5 5 5 9 9 9 6 6 6 11 11 11 7 7 7 13 13 13 8 8 8 15 15 15 9 9 9 17 17 17 10 10 10 19 19 19 Алгоритм действий сервера выглядит следующим образом: если используется атрибут NEXTVAL, очередное значение вычисляется один раз для каждой строки и подставляется в каждое место, где упоминается атрибут NEXTVAL или CURRVAL соответствующей последовательности. В том числе, не является ошибкой упоминание атрибута CURRVAL перед NEXTVAL в пределах одного оператора; все равно новое значение будет сначала вычислено и только потом подставлено. В то же время это не дает возможности в одной строке сослаться на старое значение последовательности и тут же вычислить новое. SQL> create table seq_values ( s11 number(3), s12 number(3), s21 number(3), s22 number(3)); Table created SQL> insert into seq_values values ( s1.nextval, s1.nextval, s2.nextval, s2.nextval ) ; 1 row inserted SQL> insert into seq_values values ( s1.nextval, s1.nextval, s2.nextval, s2.nextval ) ; 1 row inserted SQL> insert into seq_values select s1.nextval, s1.nextval, s2.nextval, s2.nextval from dual ; 1 row inserted SQL> select * from seq_values ; S11 S12 S21 S22 ---- ---- ---- ---- 11 11 21 21 12 12 23 23 13 13 25 25 SQL> update seq_values set s11 = s1.nextval, s12 = s1.nextval ; 3 rows updated SQL> select * from seq_values ; S11 S12 S21 S22 ---- ---- ---- ---- 14 14 21 21 15 15 23 23 16 16 25 25 Русские буквы. Oracle опередил большинство других инструментальных средств в сомнительной практике использования национальных символов в идентификаторах. Последствия такого решения видны на следующем примере: SQL> create table dic$currencies ( currency_id integer, currency varchar2(3)) ; Table created SQL> select * from diс$currencies ; ORA-00942: table or view does not exist Причина ошибки в том, что одна из букв "с" в названии таблицы - русская. Сервер разрешает использование символов национальных алфавитов в идентификаторах без их заключения в кавычки; допускается создание названных по-русски таблиц, колонок, представлений и прочих элементов БД. К сожалению, исправление опечаток (допущенных при создании объекта, особенно при проектировании в CASE-средствах) приводит к подобным "странным" проблемам; после нескольких подобных случаев лично я создал системный триггер, запрещающий использование русских букв в создаваемых объектах. Исключения в declare. Блок операторов в Oracle состоит из трех секций, обрамленных ключевыми словами declare, begin, exception и end. Несколько неожиданный, хотя логичный факт - операторы обработки исключений в секции exception действуют для кода, выполняемого в секции begin но не действуют для кода, выполняемого в секции declare. SQL> declare s varchar2(1) := '12345' ; begin dbms_output.put_line ( 'Все в порядке' ) ; exception when others then dbms_output.put_line ( 'Ошибка: ' // sqlerrm ) ; end ; / ORA-06502: PL/SQL: numeric or value error: character string buffer too small ORA-06512: at line 2 Эту особенность следует иметь в виду, применяя в declare сложную инициализацию - например, вызов функций, способных привести к исключению. Такое исключение может быть обработано только внешним по отношению к declare блоком - либо в вызывающей подпрограмме, либо в дополнительном, объемлющем блоке begin/exception/end. Оптимизатор. Подобно другим компиляторам, Oracle не всегда выполняет действия, вроде бы затребованные пользователем, но ненужные для получения конечного результата. Так, результатом следующего запроса из общих соображений должна была быть ошибка, а вовсе не единица: SQL> select count(*) from ( select 1/0 from dual ) ; COUNT(*) ---------- 1 Execution Plan ---------------------------------------------------------- 0 SELECT STATEMENT Optimizer=CHOOSE 1 0 SORT (AGGREGATE) 2 1 TABLE ACCESS (FULL) OF 'DUAL' В то же время не стоит чрезмерно полагаться на эту способность; такой результат получается далеко не всегда: SQL> declare i integer ; begin i := 1/0 ; dbms_output.put_line ( 'Переменная i нигде больше не используется' ) ; end ; / ORA-01476: divisor is equal to zero ORA-06512: at line 4 Операции с NULL. Для проверки знания особенностей операций с NULL я предлагаю Вам предсказать результаты выполнения следующего скрипта: declare i integer ; procedure write ( expr varchar2 ) is begin dbms_output.put_line ( expr ) ; end ; procedure test ( expr varchar2, value varchar2 ) is begin write ( rpad ( expr, 15 ) // ' = ' // coalesce ( value, 'null' )) ; end ; procedure test ( expr varchar2, value boolean ) is begin test ( expr, case when value then 'true' when not value then 'false' when value is null then 'null' else 'something strange' end ) ; end ; begin write ( '' ) ; test ( '1 + null', 1 + null ) ; test ( '2 * null', 2 * null ) ; test ( '3 // null', 3 // null ) ; write ( '' ) ; test ( 'true and null', true and null ) ; test ( 'false and null', false and null ) ; test ( 'true or null', true or null ) ; test ( 'false or null', false or null ) ; write ( '' ) ; test ( '(null = null)', null = null ) ; test ( '(null <> null)', null <> null ) ; test ( 'not (null)', not ( null )) ; write ( '' ) ; test ( '(true > false)', true > false ) ; test ( '(true > null)', true > null ) ; test ( '(true >= null)', true >= null ) ; write ( '' ) ; test ( 'length (null)', length ( null )) ; test ( 'length ('''')', length ( '' )) ; test ( 'lpad ('''', 0)', lpad ( '', 0 )) ; write ( '' ) ; select ( select 1 from dual where 1 = 0 ) into i from dual ; test ( 'i (select)', i ) ; write ( '' ) ; if null then write ( '"if null then" works' ) ; else write ( '"if null else" works' ) ; end if ; end ; Интересный момент в результатах этого скрипта - видно использование трехзначной логики при вычислении логических выражений. В то же время, пожалуй, вряд ли стоит строить приложения, рассчитанные на те или иные тонкости работы с null (особенно на пресловутый результат length ('')); скорее стоит максимально использовать специальные операции - is null, is not null, coalesce. Размножение строк в connect by. Обычно иерархический запрос можно считать сочетанием фильтра (на попадание строки в дерево) и упорядочивания (в порядке вершин дерева). Однако, эта модель становится неверной, как только условие в connect by позволяет привязать узел сразу к нескольким родительским узлам. SQL> create table tree as select 1 id, cast ( null as integer ) parent_id from dual union all select 2 id, 1 parent_id from dual union all select 3 id, 1 parent_id from dual ; Table created SQL> insert into tree select * from tree ; 3 rows inserted SQL> select * from tree start with id = 1 connect by prior id = parent_id ; ID PARENT_ID -- --------- 1 2 1 3 1 2 1 3 1 1 2 1 3 1 2 1 3 1 10 rows selected В этом случае происходит дублирование дочерних узлов; так, в примере каждая из двух двоек привязывается к каждой из двух единиц, образуя в итоге четыре записи с id, равным двум. Сочетание where с connect by. В иерархическом запросе (start with..connect by) построение дерева выполняется как часть предложения where. Порядок действий при этом следующий (см. *):
Последствия этого можно увидеть на следующем примере: SQL> create table tree as select rownum id, case when rownum = 1 then null else round ( dbms_random.value ( 1, rownum - 1 )) end parent_id, mod ( rownum, 3 ) modulo from dba_objects where rownum <= 10 ; Table created SQL> create table modulos as select id, modulo from tree where modulo = 0 ; Table created SQL> select * from tree where modulo = 0 start with id = 1 connect by prior id = parent_id ; ID PARENT_ID MODULO -- --------- ------ 3 1 0 9 3 0 6 1 0 SQL> select t.* from tree t, modulos m where t.id = m.id start with t.id = 1 connect by prior t.id = t.parent_id ; ID PARENT_ID MODULO -- --------- ------ SQL> Здесь дважды выполняется почти одно и то же действие - но наличие соединения таблиц приводит к другому результату, поскольку фильтрация выполняется до выполнения connect by, а не после. SQL> select t.*, m.* from tree t, modulos m where t.id = m.id or t.id = 1 start with t.id = 1 connect by prior t.id = t.parent_id ; ID PARENT_ID MODULO ID MODULO ---------- ---------- ---------- -------------------- 1 1 3 0 1 1 6 0 1 1 9 0 Execution Plan ---------------------------------------------------------- 0 SELECT STATEMENT Optimizer=CHOOSE 1 0 FILTER 2 1 CONNECT BY (WITHOUT FILTERING) 3 2 COUNT 4 3 NESTED LOOPS 5 4 TABLE ACCESS (FULL) OF 'MODULOS' 6 4 TABLE ACCESS (FULL) OF 'TREE' Отсюда видно, что Oracle действует таким же образом: выполняет соединение таблиц до построения дерева и фильтрует результат после построения. Сам по себе, однако, результат весьма неожиданный. Условие "t.id = 1", вроде бы привязанное логическим "или", на самом деле стало обязательным для всех строк результата - поскольку это единственное условие, выполняющееся после построения дерева. Этот запрос оказывается эквивалентным примерно следующему: select * from ( select * from ( select t.*, m.id m_id, m.modulo m_modulo from tree t join modulos m on ( t.id = m.id ) union select t.*, m.id m_id, m.modulo m_modulo from tree t cross join modulos m ) t start with t.id = 1 connect by prior t.id = t.parent_id ) t where t.id = 1 С моей точки зрения, этот пример показывает, что при сочетании where с connect by следует досконально понимать логику работы сервера, но лучше - позаботиться о явном, однозначном выражении своих мыслей с помощью подзапросов. Второй из этих вариантов может быть и не более читаем, нежели первый - но гораздо менее коварен. Natural Join. Пожалуй, из общих соображений natural join следует назвать сомнительной операцией. Она может быть удобна для администрирования, для разовых запросов в случае, если администратор знает, что две таблицы соединяются но не помнит, как именно называется связующее поле. Разработчик же вряд ли имеет право применять эту операцию в программном коде: любое изменение структуры базы может привести к незаметному изменению фактически выполняемого запроса. Добавления в таблицу поля с названием "name" или "date_start" окажется вполне достаточно, чтобы разрушить логику работающего приложения и вполне вероятно, нанести тяжелые повреждения данным. SQL> select d.*, s.sid, s.username, s.program from dual d natural join v$session s ; DUMMY SID USERNAME PROGRAM ----- --- ------------------------------ ----------- X 1 ORACLE.EXE X 2 ORACLE.EXE X 3 ORACLE.EXE X 4 ORACLE.EXE X 5 ORACLE.EXE X 6 ORACLE.EXE X 7 ORACLE.EXE X 8 TEST X 9 TEST X 11 TEST X 12 TEST sqlplus.exe X 13 TEST sqlplus.exe X 14 TEST X 15 TEST X 16 TEST X 17 TEST 16 rows selected Таким образом, особенно если запрос содержит дальнейшую группировку, легко незаметно получить неверные данные (ошибочно связав не имеющие связи таблицы). Из-за этого вряд ли стоит использовать natural join вообще; для экономии работы пальцами, пожалуй, стоит дождаться операции наподобие foreign key join - ее можно определить достаточно надежно. (*) В документации Oracle этот момент сформулирован весьма невнятно и допускает совершенно разные толкования. Изложенное - результат некоторых экспериментов, а также уверенности, что текст документации можно понять и таким образом. Ссылки по теме
|
|