Ноябрь/Декабрь 2003


Профессионалу администратору


Том Кайт

Том Кайт: о секционировании, поиске по справочным таблицам, ссылочной целостности
(On Partitions, Lookups, and Integrity, By Tom Kyte)

Источник: журнал Oracle Magazine, March-April 2003
( http://otn.oracle.com/oramag/oracle/03-mar/o23asktom.html).

Наш эксперт показывает, как выполнять секционирование, поиск по справочным таблицам, проверку ссылочной целостности, запросы для аудита операций DDL, а также, как определять число записей в файлах ОС.

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

ИмяТаблицы   Структура
----------   --------------------
TABLE_1
TABLE_2      Такая же как TABLE_1
TABLE_3      Такая же как TABLE_1
TABLE_4
TABLE_5      Такая же как TABLE_4
TABLE_6      Такая же как TABLE_4
TABLE_7      Такая же как TABLE_4
TABLE_8      Такая же как TABLE_4
TABLE_9
TABLE_10     Такая же как TABLE_1
TABLE_11     Такая же как TABLE_1
TABLE_12
...          ...

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

Имя Таблицы        Примечания
----------        ------------------------
TABLE_1           будет также содержать данные таблиц TABLE_2, TABLE_3, TABLE_10 и TABLE_11
TABLE_4           будет также содержать данные таблиц TABLE_5, TABLE_6, TABLE_7 & TABLE_8
TABLE_12          только свои данные

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

Это делается достаточно просто. Представим далее, что таблицы t1, t2 и t3 – ваши существующие таблицы. Таблица t – это таблица, которую вы хотите создать (позднее, если требуется, вы можете уничтожить t1 и переименовать t в t1). Я буду использовать следующий подход:

SQL> create table t1 
as select sysdate dt, 
all_objects.* from all_objects;
Table created.

SQL> create table t2 
as select add_months(sysdate,-12) dt, 
all_objects.* from all_objects;
Table created.

SQL> create table t3 
as select add_months(sysdate,-24) dt, 
all_objects.* from all_objects;
Table created.

SQL> create table t (dt, OWNER, 
OBJECT_NAME, SUBOBJECT_NAME,OBJECT_ID, 
DATA_OBJECT_ID, OBJECT_TYPE, CREATED,
LAST_DDL_TIME, TIMESTAMP, STATUS, 
TEMPORARY, GENERATED, SECONDARY)
partition by range(dt) (
partition part2000 values less than 
(to_date('01-jan-2001','dd-mon-yyyy')),
partition part2001 values less than 
(to_date('01-jan-2002','dd-mon-yyyy')),
partition part2002 values less than 
(to_date('01-jan-2003','dd-mon-yyyy'))
) as
select sysdate dt, all_objects.* 
from all_objects where 1=0;
Table created.

Я только что создал секционированную таблицу, которая секционирована по дате и имеет точно такую же структуру, что и ваша существующая таблица. Установлены такие диапазоны, чтобы таблица t1 размещалась в одной секции, таблица t2 – в другой и т.д. Теперь нужно загрузить вашу заполненную таблицу t1 в пустую секцию part2000:

SQL> alter table t
  2  exchange partition part2000
  3  with table t3
  4  without validation
  5  /
Table altered.

То же самое нужно сделать и для оставшихся пар "таблица-секция": перенести t2 в part2001, а t3 – в par2003. Теперь t – это таблица, секции которой представляют собой то, что раньше было таблицами t1 , t2 и t3.  

Все о поиске по справочным таблицам

Мне говорили, что вместо многократного поиска справочных данных в справочной таблице (lookup) (например, поиск в таблице описаний (description) описания (description) по его идентификатору (description_id)), более эффективно загружать справочную таблицу в PL/SQL-таблицу и выполнять поиск по ней. Можете ли вы продемонстрировать, как мне загружать данные в PL/SQL-таблицу? Как выполнять поиск с помощью PL/SQL-таблицы? Я хочу загружать в PL/SQL-таблицу значения более чем из одного только столбца, могу ли я делать это? Не имеют ли PL/SQL-таблицы только одну размерность?

Ответ будет следующим: "все зависит". Предположим, у вас есть следующий код:

For x in ( select * from t )
Loop
   select description 
     into l_description 
     from lookup 
    where id = x.description_id;

   вставка в другую_таблицу

Для данного кода поиск по PL/SQL-таблице может быть более быстрым; но не таким быстрым, чтобы получить корректный ответ! Следуя моей мантре: "если вы можете сделать это одним SQL-оператором, делайте это", вам нужно использовать соединения. В следующем примере сравнивается вставка с использованием справочной таблицы и без нее (для определения общего затраченного времени используется измерительный инструментарий, доступный по адресу asktom.oracle.com/~tkyte/runstats.html):

SQL> create table lookup
  2  (id primary key,
  3   description )
  4  organization index
  5  as
  6  select object_id, object_name
  7    from all_objects;
Table created.

Это будет справочной таблицей: таблицей идентификаторов (id) и описаний (description). Через минуту вы увидите, как загружать ее в ассоциативную PL/SQL-таблицу, с помощью которой будет выполняться поиск. Но сначала я сгенерирую другую таблицу с тестовыми данными – входными данными, по которым я хочу выполнять поиск:

SQL> create table t
  2  as
  3  select object_id
  4    from all_objects;
Table created.

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

SQL> create table target
  2  (id number, descript varchar2(30));
Table created.

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

SQL> declare
  2    type lookupTableType is table of
  3     lookup.description%type index by
  4     binary_integer;
  5
  6     l_lookup lookupTableType;
  7  begin
  8     for x in (select * from lookup)
  9     loop
 10        l_lookup(x.id) :=
 11           x.description;
 12     end loop;
 13
 14     for x in (select * from t)
 15     loop
 16        begin
 17           insert into target
 18           values
 19           ( x.object_id,
 20             l_lookup(x.object_id) );
 21        exception
 22           when no_data_found then
 23              insert into target
 24              values
 25              ( x.object_id, null );
 26        end;
 27     end loop;
 28  end;
 29  /
PL/SQL procedure successfully completed.
Elapsed: 00:00:13.24

Как видно, для выполнения поиска по справочной таблице требуется немногим более 13 секунд. Можем ли мы сделать это быстрее? Несомненно:

SQL> begin
  2     insert into target
  3     select t.object_id, l.description
  4       from t, lookup l
  5      where t.object_id = l.id(+);
  6  end;
  7  /
PL/SQL procedure successfully completed.
Elapsed: 00:00:00.43

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

Медленно ли выполняется проверка ссылочной целостности

У нас есть таблица city, содержащая список городов, в которых находятся офисы наших клиентов. В таблице city хранятся метаданные – значения столбцов вряд ли будут изменяться. Мы имеем некоторые пособия для служащих, рассчитываемые на основании городов, в которых служащие работают.

Мы используем форму, написанную на языке Visual Basic, для вставки информации о служащих в таблицу employee. Для столбца city таблицы employee определен внешний ключ, ссылающийся на первичный ключ таблицы city.

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

Первоначально у нас будет много ввода данных, а количество городов в таблице city равно 25. Я не был убежден доводами консультанта. Действительно ли они обоснованы?

Вы благоразумны, консультант неправ.

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

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

Выполнял ли консультант тестирование, чтобы проверить влияние на производительность? Давайте выполним быструю проверку. Я буду использовать следующую простую схему:

drop table cities;
create table cities as 
select username city from all_users
where rownum <= 50;
 
alter table cities add constraint cities_pk primary key(city);
 
create table t1 ( x char(80), city 
references cities );
 
create table t2 ( x char(80), city varchar2(30) );

Теперь для нагрузочного тестирования я вставлю в таблицу t1 большой объем данных с включенной проверкой ссылочной целостности, а в таблицу t2 без проверки. Для этого я буду использовать следующий код:

alter session set sql_trace=true;
declare
    type array is table of varchar2(30) 
           index by binary_integer;
    l_data array;
begin
    select * BULK COLLECT into l_data 
      from cities;
    for i in 1 .. 1000
    loop
        for j in 1 .. l_data.count
        loop
            insert into t1 
            values ('x', l_data(j) );
        end loop;
    end loop;
end;
/

Копируем этот блок PL/SQL и заменяем t1 на t2 для второго теста. Теперь, используя утилиту TKPROF для форматирования результатов трассировки SQL_TRACE, обнаруживаем следующее (для экономии места часть информации вычеркнута):

INSERT into t1
values
 ('x', :b1 )

call	count	cpu	query	current
-----	-----	----	-----	-------
Parse	1 	0.00	2	0
Execute	37000	9.25	479	78969
Fetch	0	0.00	0	0
----- 	-----	----	-----	-------
total	37001 	9.26	481	78969

INSERT into t2
values
 ('x', :b1 )

call	count	cpu	query	current 
----	-----	----	-----	-------
Parse	1	0.00	0 	0
Execute	37000	7.87 	484	41900
Fetch	0	0.00	0	0
---- 	-----	----	-----	-------
total	37001	7.88	484	41900

Итак, с включенной проверкой ссылочной целостности мы затрачиваем 0.00025 (9.25/37,000) секунд времени ЦП на вставку одной строки и 0.000212703 (7.87/37,000) секунд времени ЦП на вставку одной строки без проверки. Заметят ли это ваши конечные пользователи? Заметите ли это вы? Подавляющее большинство задержек при работе конечных пользователей будет связано с программой Visual Basic, рисующей экраны. Не думаете ли вы, что справочная подпрограмма этого консультанта будет выполняться за .000037297 секунд? Если не думаете, вы лучше разбираетесь в базах данных.

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

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

Запросы для операций DDL

Мне нужно знать, как идентифицировать операции языка определения данных (DDL, data definition language), выполненные за последние 24 часа. Если разработчик перезаписывает какую-то функцию, есть ли какой-либо способ извлечения ее старого кода из таблиц схемы SYSTEM? Кроме того, как мне идентифицировать зависимые объекты и получить список объектов во всех схемах, зависящих от данного объекта?

Вы хотите организовать аудит. Я не смогу помочь вам получить информацию за последние 24 часа, но завтра вы будете иметь ее. Когда кто-то перезаписывает код, старый код пропадает, стирается, и уже нигде не хранится. Исключая копирования старой резервной копии базы данных и восстановления базы данных на момент времени в прошлом (point-in-time recovery), перед тем, когда кто-то удалил процедуру, код которой пропал.

Вы можете использовать DDL-триггер (BEFORE CREATE – перед созданием) для "захвата" кода, а также и для аудита. Это полезно во многих случаях, поэтому я подробно остановлюсь на идее и покажу здесь ее реализацию. Я начну с создания таблицы для хранения старого кода (и его множественных версий) и регистрации, кто и когда перезаписал его:

SQL> create table old_code
  2  as
  3  select user username, 0 version, 
            sysdate date_changed, 
            user_source.*
  4    from user_source
  5   where 1=0
  6  /
Table created.

SQL> create sequence version_seq;
Sequence created.

Теперь для сохранения "старого кода" перед его перезаписыванием я могу использовать триггер BEFORE CREATE:

SQL> create or replace 
         trigger create_trigger
  2  before create on schema
  3  declare
  4    l_date  date := sysdate;
  5    l_ver   number;
  6  begin
  7    if (ora_dict_obj_type in 
              ( 'PACKAGE', 
                'PACKAGE BODY', 
                'PROCEDURE', 
                'FUNCTION' ) )
  8    then
  9       select version_seq.nextval 
            into l_ver 
            from dual;
 10
 11       insert into old_code
 12       select user, l_ver, 
               l_date, user_source.*
 13         from user_source
 14        where name = ora_dict_obj_name
 15         and type = ora_dict_obj_type;
 16    end if;
 17  end;
 18  /
Trigger created.

Для демонстрации этого триггера в действии в следующем примере я заменяю в базе данных функцию:

SQL> create or replace 
     function f return number
  2  as
  3  begin
  4          return 0;
  5  end;
  6  /
Function created.

SQL> create or replace 
     function f return date
  2  as
  3  begin
  4          return sysdate;
  5  end;
  6  /
Function created.

SQL> select line, text from 
     old_code order by line;

LINE	TEXT
----  -------------------------
   1	function f return number
   2	as
   3 	begin
   4	return 0;
   5	end;

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

Что касается зависимостей, представления словаря данных USER_DEPENDENCIES, ALL_DEPENDENCIES и DBA_DEPENDENCIES содержат все что вам нужно.

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

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

ТЕСТИРУЙТЕ производительность
asktom.oracle.com/~tkyte/runstats.html

ЧИТАЙТЕ
Еще Тома
asktom.oracle.com/~tkyte/
asktom_articles.html

Документацию Oracle
otn.oracle.com/documentation

Определение количества записей в файлах ОС

По ночам мы загружаем в наше хранилище данных от 4 до 15 плоских файлов из двух разных унаследованных систем. Как часть нашего обычного процесса ежедневного аудита загрузки данных, нам нужно проверять счетчики записей (передаются по FTP). Все файлы имеют фиксированную длину записей. Длина записей изменяется от 100 символов до 3,500 и больше. Во всех файлах используется кодировка US7ASCII. Число записей в "узких" файлах находится в диапазоне от менее 50 до 250,000; число записей в "широких" файлах находится в диапазоне от 10,000 до 4 миллионов. Общий диапазон размера файлов колеблется от нескольких килобайтов до 100МБ и выше. На выполнение команды "wc -l" (подсчет числа строк) для наших самых больших файлов обычно затрачивается около минуты. Определение фактического количества записей в плоском файле ОС представляет собой трудную задачу. Я видел примеры загрузки листинга каталогов в таблицу базы данных с помощью Java и примеры выполнения команд ОС из PL/SQL-кода (также с помощью Java), но я не Java-программист, и мне нужно скомпоновать эти два примера так, чтобы подсчитать число записей во всех файлах данного каталога и сохранить результат в таблице базы данных. Есть какие-нибудь предложения?

Файлы имеют фиксированную длину записей, поэтому все что вам нужно сделать – извлечь размер файла и разделить его на размер записей. Это можно сделать просто. Используя тип данных BFILE (указатель на большой двоичный файл), вы можете извлекать размеры файлов и, зная длину логических записей (LRECL, logical record length) в этих файлах, вы можете вычислить количество записей:

SQL> create or replace directory
  2  MY_FILES as '/tmp/';
Directory created.

SQL> create or replace
  2  function number_of_records
              ( p_filename in varchar2, 
                p_lrecl in number )
  3  return number
  4  as
  5   l_bfile    bfile;
  6   l_recs     number;
  7  begin
  8   l_bfile := 
        bfilename('MY_FILES', 
                  p_filename);
  9   dbms_lob.fileopen( l_bfile );
 10   l_recs := 
         dbms_lob.getlength(l_bfile) / 
           p_lrecl;
 11   dbms_lob.fileclose( l_bfile );
 12   return l_recs;
 13  end;
 14  /
Function created.

SQL> !wc -l /tmp/foo.dat
     18 /tmp/foo.dat

SQL> exec dbms_output.put_line
    (number_of_records( 'foo.dat', 20 ));
18
PL/SQL procedure successfully completed.

Если по каким-то причинам вы не знаете LRECL, вам потребуется выполнить еще один шаг:

SQL> create or replace
  2  function number_of_records
              ( p_filename in varchar2 )
  3  return number
  4  as
  5   l_bfile    bfile;
  6   l_lrecl    number;
  7   l_recs     number;
  8  begin
  9   l_bfile := 
       bfilename( 'MY_FILES', 
                  p_filename );
 10   dbms_lob.fileopen( l_bfile );
 11   l_lrecl := 
         dbms_lob.instr
         ( l_bfile, 
           utl_raw.cast_to_raw(chr(10)));
 12   l_recs := 
           dbms_lob.getlength( l_bfile )/ 
                   l_lrecl;
 13   dbms_lob.fileclose( l_bfile );
 14   return l_recs;
 15  end;
 16  /
Function created.

SQL> exec dbms_output.put_line
         (number_of_records('foo.dat'));
18
PL/SQL procedure successfully completed.

Эта функция ищет в файле позицию первого символа новой строки (chr(10)) и предполагает, что это и есть LRECL для данного файла.


Ведущий данной колонки Том Кайт (Tom Kyte, thomas.kyte@oracle.com) с 1993 года работает в Oracle. Кайт – вице-президент группы Oracle Government, Education, and Healthcare, он автор книги “Expert One on One: Oracle”( Прим. пер. Имеется русский перевод: Oracle для профессионалов. Книга 1. Архитектура и основные особенности. Книга 2. Расширение возможностей и защита. – ДиаСофт, 2003 г.) и соавтор книги “Beginning Oracle Programming”“Введение в программирование баз данных Oracle” (обе книги были опубликованы издательством Wrox Press, www.wrox.com).

E-mail this page