Oracle Magazine - Русское издание (Октябрь 2007)

Том Кайт

Том Кайт: о спасательной аналитике и популярности
(On Rescue Analytics and Popularity, by Tom Kyte)

Источник: журнал Oracle Magazine, November-December 2006
(http://www.oracle.com/technology/oramag/oracle/06-nov/o66asktom.html).

Наш эксперт разъясняет спасительную силу аналитики и делится популярностью.

Вопрос. Первичный ключ моей таблицы – номер строки ROW_NUM, и мне нужно обновить столицы COLA и COLB, чтобы заменить неопределенные значения (nulls) на предшествующие определенные значения. После этого обновления оператор SELECT * должен показывать следующее:

SQL> select * from t1;

  ROW_NUM   COLA          COLB
---------   ----------    --------
        1   Category 1    Mango
        2   Category 1    Mango
        3   Category 1    Mango
        4   Category 1    Banana
        5   Category 1    Banana
        6   Category 1    Banana
        7   Category 2    Vanilla
        8   Category 2    Vanilla
        9   Category 2    Strawberry
9 rows selected.

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

SQL> select * from t1;

  ROW_NUM   COLA          COLB
---------   ----------    --------
        1   Category 1    Mango
        2
        3
        4                 Banana
        5
        6
        7    Category 2   Vanilla
        8
        9                 Strawberry
9 rows selected.

Можно ли это сделать с помощью одного SQL-оператора или нужно писать хранимую процедуру?

Ответ. Аналитические функции позволяют легко материализовать эти данные. Я покажу два подхода. Эта проблема часто возникает в хранилищах данных при анализе разреженных данных с помощью временных рядов.

В сервере Oracle Database 10g Release 2 запрос выглядит так:

SQL> select
  2    row_num,
  3    last_value(cola ignore nulls)
  4       over (order by row_num) cola,
  5    last_value(colb ignore nulls)
  6       over (order by row_num) colb
  7    from t1
  8   order by row_num
  9  /

  ROW_NUM   COLA          COLB
---------   ----------    --------
        1   Category 1    Mango
        2   Category 1    Mango
        3   Category 1    Mango
        4   Category 1    Banana
        5   Category 1    Banana
        6   Category 1    Banana
        7   Category 2    Vanilla
        8   Category 2    Vanilla
        9   Category 2    Strawberry
9 rows selected.

В этом выпуске сервера базы данных в ряде аналитических функций, таких как LAST_VALUE (она возвращает последнее значение заданного столбца в аналитическом окне), появилось предложение игнорирования неопределенных значений IGNORE NULLS. Используя это предложение, вы можете извлечь последнее определенное значение любого заданного столбца в аналитическом окне и использовать его для замены нижеследующих неопределенных значений этого столбца. В более ранних выпусках сервера такой возможности не было, поэтому нужно проявить немного больше изобретательности; равноценный запрос в более ранних выпусках выглядит так (предположим, номер ROW_NUM – 10-разрядное положительное число):

select
  row_num,
  substr(
    max(
     case
     when cola is not null
     then
       to_char(row_num,'fm0000000000')
       ||cola
      end
    ) over (order by row_num),
    11 ) cola,
  substr(
    max(
     case
     when colb is not null
     then
       to_char(row_num,'fm0000000000')
       ||colb
      end
    ) over (order by row_num),
    11 ) colb
  from t1
 order by row_num
/

Если вам интересно узнать, как это обрабатывается, ознакомьтесь на сайте oracle.com/technology/oramag/oracle/04-mar/o24asktom.html с более ранним выпуском колонки Ask Tom "On Format, Negation, and Sliding", в котором я для группирования данных использовал схожую технику (русский перевод: Том Кайт: о форматах вывода, отрицательных нулях и смещении времениприм. пер.).

Теперь, когда у меня есть этот запрос, обновить первоначальные исходные данные можно просто оператором MERGE:

SQL> merge into t1
  2    using (
  3    select
  4      row_num,
  5      last_value(cola ignore nulls)
  6        over (order by row_num) cola,
  7      last_value(colb ignore nulls)
  8       over (order by row_num) colb
  9       from t1
 10    ) t2
 11   on (t1.row_num = t2.row_num)
 12    when matched
 13    then update
 14      set cola = t2.cola,
 15           colb = t2.colb;
9 rows merged.

И снова эта функциональная возможность сервера Oracle Database 10g: оператор MERGE только с предложением WHEN MATCHED. В более ранних версиях нужно использовать также и предложение WHEN NOT MATCHED. В этом случае я знаю, что условие WHEN NOT MATCHED никогда не будет выполнено, поскольку в наборе T2 нет никаких строк ROW_NUM, которых нет в таблице T1. Поэтому я просто использую фиктивное предложение WHEN NOT MATCHED, в котором пытаюсь вставить в строку ROW_NUM значение NULL:

merge into t1
using
(
second query from above
) t2
on (t1.row_num = t2.row_num)
when matched
then update
  set cola = t2.cola,
       colb = t2.colb
when not matched
then insert (row_num)
        values (NULL);

И это все.

Самый популярный ответ

Он был опубликован в первый раз более пяти лет назад и стал на сайте asktom.oracle.com самым читаемым – свыше четверти миллиона просмотров. Вот он:

Вопрос. Я хочу по входным параметрам процедуры объявлять множественные курсоры, в которых будут изменяться только условия в предложении WHERE. Для всех курсоров используется одно и то же тело процедуры.

Ответ. Мне представляется, здесь целесообразно использовать курсорные переменные (тип данных ref cursor). Предположим, вы хотите создать универсальную подпрограмму, которая проверяет свои входные параметры и строит предложение WHERE для каждого параметра, если он не имеет значения NULL. В результате появится большое количество статически определенных курсоров; вместо этого следует использовать курсорные переменные, которые позволяют делать это динамически.

Я наглядно покажу это – напишу подпрограмму, которая будет выдавать какие-то данные таблицы EMP. Эта подпрограмма для ограничения результирующего набора принимает до трех входных параметров. Я хочу иметь до восьми разных курсоров, которые возможны здесь:

  • один без предложения WHERE (все входные параметры имеют значение null);
  • три с единичными предикатами;
  • три с "парами" предикативных условий;
  • один с тремя предикативными условиями.

Кроме того, одна из самых важных вещей при программировании в среде Oracle –использование переменных связывания, поэтому я также хочу использовать их. Для этого потребуются некие "трюки", поскольку я не знаю, сколько их будет во время исполнения подпрограммы (ноль, одна, две или три). Для решения этой проблемы будет использоваться контекст приложения.

Его примерная реализация:

SQL> create or replace
  2    context MY_CTX
  3    using MY_PROCEDURE
  4    /
Context created.

Здесь создается контекст моего приложения, который связывается с моей еще не созданной процедурой MY_PROCEDURE. Обратите внимание, в этом контексте может устанавливать значения только процедура MY_PROCEDURE. Более подробно о контекстах приложений и их использовании см. asktom.oracle.com/~tkyte/article2. Теперь, для удобства я инкапсулирую вызовы процедуры DBMS_OUTPUT.PUT_LINE в небольшой подпрограмме, которая предназначена для работы со строками, размер которых превышает 255 символов (это не требуется в сервере Oracle Database 10g Release 2, в котором предельный размер строк увеличен до 32K).

SQL> create or replace
        procedure p ( p_str in varchar2 )
  2    is
  3      l_str    long := p_str||chr(10);
  4      l_piece long;
  5      n         number;
  6  begin
  7    loop
  8      exit when l_str is null;
  9    n := instr( l_str, chr(10) );
 10   l_piece :=
          substr( l_str, 1, n-1 );
 11   l_str   :=
          substr( l_str, n+1 );
 12   loop
 13     exit when l_piece is null;
 14     dbms_output.put_line
         ( substr( l_piece, 1, 250));
 15     l_piece :=
           substr( l_piece, 251 );
 16   end loop;
 17  end loop;
 18 end;
 19 /
Procedure created.

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

Теперь, "ядро" ответа – подпрограмма, которая динамически конструирует для нас предикат:

create or replace
procedure my_procedure
( p_ename     in varchar2 default NULL,
  p_hiredate  in date default NULL,
  p_sal          in number default NULL)
as
  type rc is REF CURSOR;

  l_cursor rc;
  l_query  varchar2(512)
 	          default 'select *
                            from emp
                            where 1 = 1 ';

  cursor l_template is select * from emp;
  l_rec  l_template%rowtype;

Здесь я использую то, что я называю "шаблонными" (template) курсорами. Я хочу использовать их с динамически открываемыми курсорными переменными. Мне они нужны для определения записей, в которые извлекаются данные. В данном случае, в этом простом примере, я мог бы обойтись без этого и просто определить l_rec as EMP%rowtype, но я хочу показать, как выбирать не только из одной таблицы, а из нескольких таблиц. Это просто помогает мне создавать в PL/SQL хороший тип записи. В шаблонном запросе имеются только предложения SELECT и FROM. Я никогда ничего не помещаю в предложение WHERE (даже при соединениях), потому что я никогда не открываю такие курсоры. Я только использую эти курсоры в определениях записей, показанных ниже, для получения типов данных по умолчанию, имен и т.п. Кроме того, обратите внимание на "трюк" where 1 = 1. Благодаря ему я могу просто добавлять к запросу ноль, один или более предикатов, не затрудняясь проверкой необходимости добавления предложения WHERE или условия AND. У меня всегда есть предложение WHERE, поэтому мне нужно только добавить условия AND. Обратите внимание, если для соединения нескольких таблиц используется старый синтаксис условий соединения, "трюк" where 1 = 1 не нужен – у нас уже есть предикат.

begin
   if ( p_ename is NOT NULL )
   then
    dbms_session.set_context
    ( 'MY_CTX', 'ENAME',
      '%'||upper(p_ename)||'%');
    l_query := l_query ||
     ' and ename like
       sys_context( ''MY_CTX'',
                         ''ENAME'' ) ';
   end if;

Этот технический прием я применяю для каждого входного параметра. Я проверяю, не имеет ли параметр значения NULL, если нет, добавляю его в предложение WHERE и устанавливаю его значение в контексте. Обратите внимание, в предложении WHERE я всегда использую функцию SYS_CONTEXT. Я никогда не использую в запросе литеральные значения – это плохо из соображений:

  • производительности;
  • масштабируемости;
  • использования разделяемого пула;
  • и, возможно, самое главное: безопасности (интоксикация SQL)

Более подробно об интоксикации SQL см. более ранний выпуск этой колонки: oracle.com/technology/oramag/oracle/05-jan/o15asktom.html (русский перевод: Том Кайт: об интоксикации и сравненииприм. пер.).

Кроме того, обратите внимание, для занесения в строковый литерал одинарной кавычки я должен ее удваивать. В сервере Oracle Database 10g Release 1 и более поздних версиях можно использовать новый способ занесения кавычек в строковые литералы:

 l_query := l_query ||
  q'| and ename like
      sys_context( 'MY_CTX',
                        'ENAME' ) |';

Для подготовки ответа, который работает во всех текущих выпусках сервера Oracle Database, я буду использовать в оставшейся части текста сервер Oracle9i Database и старый подход с удвоением ординарных кавычек. Теперь, продолжая, я обрабатываю столбец с датами найма на работу, HIREDATE:

if ( p_hiredate is NOT NULL )
then
   dbms_session.set_context
   ( 'MY_CTX', 'HIREDATE',
   to_char(p_hiredate,
              'yyyymmddhh24miss'));
   l_query := l_query ||
    ' and hiredate >
      to_date(
       sys_context( ''MY_CTX'',
                         ''HIREDATE'' ),
             ''yyyymmddhh24miss'') ';
end if;

Здесь обратите внимание, как осторожно я сохраняю компонент даты и времени (в вашем приложении вы и только вы можете знать, как нужно обрабатывать этот компонент). Кроме того, если у вас этот компонент сравнивается со значениями типа DATE, всегда "обертывайте" вызов функции SYS_CONTEXT вызовом функции TO_DATE; это позволит при выполнении запроса избежать неявного преобразования. И наконец, я обрабатываю третий столбец с окладами, SAL:

if ( p_sal is NOT NULL )
then
   dbms_session.set_context
   ( 'MY_CTX', 'SAL', p_sal);
   l_query := l_query ||
     ' and sal >
        to_number(
          sys_context( ''MY_CTX'',
                            ''SAL'' )
        ) ';
end if;

Здесь обратите внимание на явное преобразование к значению типа данных NUMBER; это позволяет избежать неявных преобразований. Теперь я готов к отладке запроса, используя мою подпрограмму P и курсорную переменную:

  p( l_query );

  open l_cursor for l_query;

  loop
   fetch l_cursor into l_rec;
   exit when l_cursor%notfound;
   dbms_output.put_line
   ( l_rec.ename || ',' ||
     l_rec.hiredate || ',' ||
     l_rec.sal );
  end loop;

  close l_cursor;
end;

И это все. Теперь у меня есть подпрограмма, которая открывает один из восьми возможных разных курсоров. Для проверки ее работы выполним небольшой тест:

SQL> exec my_procedure
select * from emp where 1 = 1
SMITH,17-dec-1980 00:00:00,800
.
.
.
KING,,5
PL/SQL procedure successfully completed.

SQL> exec my_procedure(p_ename=>'a')
select * from emp
where 1 = 1
and ename like
sys_context( 'MY_CTX', 'ENAME' )
ALLEN,20-feb-1981 00:00:00,1600
.
.
.
JAMES,03-dec-1981 00:00:00,950
PL/SQL procedure successfully completed.

И т.д. Этот вопрос и ответ на него – самые обсуждаемые на сайте Ask Tom, поэтому эта страница сайта одна из самых больших. Я советую прочитать оригинальное обсуждение: asktom.oracle.com/~tkyte/cursor.html – интересно ознакомиться с альтернативными способами, предложенными другими читателями, и посмотреть на их различные варианты.

Подзапросы или соединения?

Вопрос. Что лучше, с точки зрения производительности системы базы данных, соединения или подзапросы? Можете ли вы объяснить это на примерах? Кроме того, чем отличаются соединения типа nested loop (вложенный цикл) от хеш-соединений, как определить, какой тип соединений использовать с целью повышения производительности?

Ответ. Вообще говоря, соединения и подзапросы – семантически различные операции. Они могут возвращать полностью отличающиеся результаты, они не взаимозаменяемые операции. Вы должны:

  • использовать подзапрос, когда вам не нужны никакие столбцы из таблицы, на которую ссылается подзапрос;
  • использовать соединение, когда вам нужны какие-то такие столбцы.

Например, запрос:

select *
  from emp
 where deptno in
( select deptno
    from dept );

"лучше" запроса:

select emp.*
  from emp, dept
 where emp.deptno
    = dept.deptno;

Первый запрос имеет больше смысла чисто по семантическим причинам. Он говорит: "извлеки для меня из таблицы EMP все строки, номера отделов в которых (EMP.DEPTNO) имеются в таблице DEPT". Второй запрос говорит: "соедини таблицу EMP с таблицей DEPT". Вы должны дополнительно анализировать этот запрос, чтобы понять его цель (заданный вопрос). Для оптимизатора эти два запроса идентичны и их производительность одинакова.

Помните также, подзапрос просто так нельзя заменить соединением (и наоборот), поскольку часто выдаются разные ответы. Рассмотрим следующее:

Следующие шаги

СПРАШИВАЙТЕ Тома
Том Кайт, вице-президент корпорации Oracle, отвечает на наиболее трудные вопросы, связанные с технологией баз данных Oracle. Наиболее яркие материалы этого форума публикуются в данной колонке.

ЧИТАЙТЕ еще Тома:
Expert Oracle Database Architecture: 9i and 10g Programming Techniques and Solutions

ЧИТАЙТЕ более подробно:
"On Format, Negation, and Sliding"
application contexts

ref cursors

join types
Performance Tuning Guide
download.oracle.com/docs/cd/B19306_01/
server.102/b14211/toc.htm

download.oracle.com/docs/cd/B19306_01/
server.102/b14211/optimops.htm#sthref1385

download.oracle.com/docs/cd/B19306_01/
server.102/b14211/optimops.htm#sthref1394

what language to use

ЗАГРУЖАЙТЕ Oracle Database 10g Express Edition (Oracle Database XE)

SQL> select *
  2       from dept
  3      where deptno in
  4     (select deptno
  5       from emp)
  6    /

DEPTNO   DNAME      LOC
------   -------    ------
    10   ACCOUNTING NEW YORK
    20   RESEARCH   DALLAS
    30   SALES      CHICAGO

SQL>  select dept.*
  2        from dept, emp
  3      where dept.deptno
  4            = emp.deptno
  5     /

DEPTNO   DNAME       LOC
------   -------    ------
    10   ACCOUNTING  NEW YORK
    10   ACCOUNTING  NEW YORK
    20   RESEARCH    DALLAS
    20   RESEARCH    DALLAS
    20   RESEARCH    DALLAS
.
.
.
          30  SALES           CHICAGO
14 rows selected.

Оптимизатор знает, что делать – использовать надлежащую конструкцию, основываясь на заданном вопросе. В общем, подзапросы и соединения не взаимозаменяемы; используйте то, что передает наибольший смысл.

О путях доступа –типах соединений – см. на сайте download.oracle.com/docs/cd/B19306_01/server.102/b14211/toc.htm в книге Performance Tuning Guide:

Какой язык использовать?

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

Вопрос. На моем клиенте язык PL/SQL используется как для кода серверной части приложения, так и на клиентском презентационном уровне. Я думаю, для использования на презентационном уровне и уровне бизнес-логики больше подходят языки третьего поколения, такие как Java и .NET, а язык PL/SQL следует использовать только для обработки большого объема данных. Ваше мнение?

Ответ. Я счел этот вопрос несколько нелепым, поскольку он был задан на сайте Ask Tom, в котором язык PL/SQL используется как для кода серверной части приложения, так и на клиентском презентационном уровне, созданном в среде быстрой разработки веб-приложений Oracle Application Express (раньше она называлась Oracle HTML DB).

Вы можете, наверное, догадаться о сути данного мною ответа: если есть больше одного языка, можно проявить здравый смысл, чтобы использовать только язык PL/SQL или Java, или .NET, или любой другой язык, который лучше всего подходит для решения поставленной задачи. Затем последовал оживленный обмен мнениями, с которым можно ознакомиться на сайте asktom.oracle.com/~tkyte/WhatLanguage.html


Ведущий данной колонки Том Кайт (Tom Kyte, thomas.kyte@oracle.com) работает в корпорации Oracle с 1993 года. Кайт – вице-президент Oracle, возглавляющий группу Oracle Public Sector; он автор таких книг, как "Expert Oracle: 9i and 10g Programming Techniques and Solutions" (Apress, 2005), "Effective Oracle by Design" (Oracle Press, 2003), а также ряда других.

E-mail this page