В версии 11g в Oracle появилась несколько новых замечательных схем секционирования — например, удобная функциональность интервального секционирования — автоматического создания секций по мере выхода range из заданных границ.
В версиях до 11g необходимо периодически вручную либо заранее добавлять секции, либо разбивать секцию по умолчанию. То есть постоянно необходимо отслеживать состояние таких таблиц. В данной статье я поделюсь своими решениями для автоматизации таких задач секционирования.
Сначала приведу пример для 11g:
Этот скрипт создает секцию p1 для записей, значение столбца res_id которых находится в диапазоне 1-100. Когда вставляются записи со значением столбца res_id меньшим 101, они помещаются в секцию p1, а когда в новой записи значение этого столбца равно или больше 101, сервер Oracle Database 11g создает новую секцию, имя которой генерируется системой. Подробнее с этим примером и прочими новыми схемами секционирования вы можете познакомиться в переводе статьи Арупа Нанды в русском издании Oracle Magazine.
Рассмотренные ниже решения можно применить и в других СУБД, не поддерживающих автоматическое добавление секций
Создадим тестовую таблицу:
Логично, что если в такой таблице не будет пропусков, то новые секции было бы желательно создавать до того как ключ секционирования приблизится к границе максимальной секции. Сколько у нас осталось значений ключа до границы, мы легко можем определить согласно простой формуле: partition_size — (key-start_key_in_partition), где key — текущий ключ секционирования, start_key_in_partition — первый ключ, который попадает в эту секцию, partition_size — количество ключей в секции, а % — операция целочисленного деления(div). Обычно такое секционирование производится на равные секции, и с учетом этого мы можем упростить эту формулу до такой: partition_size — key%partition_size.
Что нам это дает: зная момент мы можем создать триггер, который будет добавлять секции при наступлении данного события.
Создадим данный триггер:
Данный триггер с использованием автономных транзакций автоматически создает новую секцию с именем 'P'+номер секции размером в 10000, когда ID — наш ключ секционирования — остается 4000 значений до границы секции(10000-4000 = 6000, т.е. ID=6000,16000,26000, и тд.), но сначала проверяется не существует ли уже данная секция(такое может произойти, например, при повторном добавлении 6000-й записи, или ручном добавлении секции). Параметры секционирования — 10000 и 4000, вы должны подбирать исходя из вашей конкретной ситуации, но следует учесть, что граница(4000 в примере) должна быть больше максимального количества одномоментно добавляемых записей, т.к. иначе на момент транзакции вставки данных, транзакция не будет «знать» о новой секции, т.к. на начало транзакции ее не существовало, поэтому данные вставлены не будут с жалобой об отсутствии сопоставления секции данному ключу. Этого бы можно было избежать с использованием alter table split default_partition, который я рассмотрю далее, но это скажется на времени выполнения.
Проверим наш триггер, заполнив секцию:
Кроме того, в случае использования сиквенсов, которые из-за кэширования «шагают» не последовательно можно изменить триггер, чтобы он выполнялся для набора значений с 4000 до 3900 записи с конца секции:
заменим условие
В случаях случаях, когда мы указываем секцию по умолчанию, мы можем разделять ее тогда, когда туда уже попали записи, вопрос в том как это автоматически отслеживать.
В Data dictionary мы можем получить информацию о всех секциях секционированных таблиц, сделав выборку из dba_tab_partitions, в которой partition_position указывает порядок секции в таблице, а high_value — параметры секции. Следовательно, мы можем получить имя последней секции в таблице и сделать выборку из нее для получения количества записей в ней.
После получения информации о таблицах, в которых началась запись в дефолтные секции, мы должны отправить уведомление. Для этого мы можем использовать варианты:
Первый вариант реализуется с помощью процедуры dbms_system.ksdwrt, которая имеет два параметра:
Пример:
Второй вариант — использовать пакет utl_mail или более низкоуровневые — utl_smtp или utl_tcp.
utl_mail — это более удобная обертка для utl_smtp, но для ее использования обязательно необходимо установить параметр smtp_out_server. Вы можете сделать это как только для сессии — «ALTER SESSION SET smtp_out_server = ...» так и для системы «ALTER SYSTEM SET smtp_out_server = ...».
Не удивляйтесь, если вы не можете найти этот пакет у себя — изначально он не включен и для его создания вы должны выполнить два скрипта:
Сведем все воедино в пакет pkg_partitions
Методы пакета:
Код пакета:
UPD
Как правильно подсказал zhekappp можно использовать num_rows при включении автоматического сбора статистики. Сбор статистики можно будет включить добавлением задания с помощью dbms_job с dbms_stats.gather_table_stats.
Тогда нужно будет убрать запрос количества записей в секции и изменить запрос на:
Полный код пакета для этого случая можно взять тут: http://www.xt-r.com/2010/10/pkgpartitions.html
Осталось только настроить автоматическое выполнение. Сделаем это с помощью dbms_job.
Например, ежедневное автоматическое выполнение скрипта получения данных:
В версиях до 11g необходимо периодически вручную либо заранее добавлять секции, либо разбивать секцию по умолчанию. То есть постоянно необходимо отслеживать состояние таких таблиц. В данной статье я поделюсь своими решениями для автоматизации таких задач секционирования.
Сначала приведу пример для 11g:
- create table res (
- res_id number not null,
- res_date date,
- hotel_id number(3),
- guest_id number
- )
- partition by range (res_id)
- interval (100) store in (users)
- (
- partition p1 values less than (101)
- );
Этот скрипт создает секцию p1 для записей, значение столбца res_id которых находится в диапазоне 1-100. Когда вставляются записи со значением столбца res_id меньшим 101, они помещаются в секцию p1, а когда в новой записи значение этого столбца равно или больше 101, сервер Oracle Database 11g создает новую секцию, имя которой генерируется системой. Подробнее с этим примером и прочими новыми схемами секционирования вы можете познакомиться в переводе статьи Арупа Нанды в русском издании Oracle Magazine.
Рассмотренные ниже решения можно применить и в других СУБД, не поддерживающих автоматическое добавление секций
Решение для равномерно увеличивающегося ключа секционирования без пропусков
Создадим тестовую таблицу:
- create table test_part(
- id number not null,
- name varchar2(100) not null,
- owner varchar2(100) not null,
- type varchar2(100) not null,
- created date not null,
- constraint test_part_pk
- primary key(id)
- )
- partition by range (id) (partition p1 values less than (10000));
Логично, что если в такой таблице не будет пропусков, то новые секции было бы желательно создавать до того как ключ секционирования приблизится к границе максимальной секции. Сколько у нас осталось значений ключа до границы, мы легко можем определить согласно простой формуле: partition_size — (key-start_key_in_partition), где key — текущий ключ секционирования, start_key_in_partition — первый ключ, который попадает в эту секцию, partition_size — количество ключей в секции, а % — операция целочисленного деления(div). Обычно такое секционирование производится на равные секции, и с учетом этого мы можем упростить эту формулу до такой: partition_size — key%partition_size.
Что нам это дает: зная момент мы можем создать триггер, который будет добавлять секции при наступлении данного события.
Создадим данный триггер:
- create or replace trigger tr_test_part
- before insert on test_part
- for each row
- when (mod(NEW.id,10000) = 6000)
- declare
- l_part_name number;
- l_maxvalue number;
- l_exist number;
- l_partition_exists exception;
- PRAGMA EXCEPTION_INIT(l_partition_exists, -14074);
- PRAGMA AUTONOMOUS_TRANSACTION;
- begin
- l_part_name:=ceil(:NEW.ID/10000)+1;
- BEGIN
- execute immediate 'alter table xtender.test_part add partition p'||l_part_name||' values less than('||l_maxvalue||')';
- EXCEPTION
- when l_partition_exists then null;
- END;
- end tr_test_part;
Данный триггер с использованием автономных транзакций автоматически создает новую секцию с именем 'P'+номер секции размером в 10000, когда ID — наш ключ секционирования — остается 4000 значений до границы секции(10000-4000 = 6000, т.е. ID=6000,16000,26000, и тд.), но сначала проверяется не существует ли уже данная секция(такое может произойти, например, при повторном добавлении 6000-й записи, или ручном добавлении секции). Параметры секционирования — 10000 и 4000, вы должны подбирать исходя из вашей конкретной ситуации, но следует учесть, что граница(4000 в примере) должна быть больше максимального количества одномоментно добавляемых записей, т.к. иначе на момент транзакции вставки данных, транзакция не будет «знать» о новой секции, т.к. на начало транзакции ее не существовало, поэтому данные вставлены не будут с жалобой об отсутствии сопоставления секции данному ключу. Этого бы можно было избежать с использованием alter table split default_partition, который я рассмотрю далее, но это скажется на времени выполнения.
Проверим наш триггер, заполнив секцию:
insert into xtender.test_part
select rownum, o.OBJECT_NAME, o.OWNER, o.OBJECT_TYPE, o.CREATED
from all_objects o
where rownum<1000;
Кроме того, в случае использования сиквенсов, которые из-за кэширования «шагают» не последовательно можно изменить триггер, чтобы он выполнялся для набора значений с 4000 до 3900 записи с конца секции:
заменим условие
when (mod(NEW.id,10000) = 6000)на
when (mod(NEW.id,10000) between 6000 and 6100)
Решение для прочих случаев
В случаях случаях, когда мы указываем секцию по умолчанию, мы можем разделять ее тогда, когда туда уже попали записи, вопрос в том как это автоматически отслеживать.
В Data dictionary мы можем получить информацию о всех секциях секционированных таблиц, сделав выборку из dba_tab_partitions, в которой partition_position указывает порядок секции в таблице, а high_value — параметры секции. Следовательно, мы можем получить имя последней секции в таблице и сделать выборку из нее для получения количества записей в ней.
После получения информации о таблицах, в которых началась запись в дефолтные секции, мы должны отправить уведомление. Для этого мы можем использовать варианты:
- если у вас настроена автоматическая отправка алертов на почту, то просто записать событие в alert.log;
- просто написать процедуру для отправки писем с уведомлением.
Первый вариант реализуется с помощью процедуры dbms_system.ksdwrt, которая имеет два параметра:
- первый(BINARY_INTEGER) — куда записывать, с возможными значениями:1 — в стандартный трейс-файл, 2 — в alert.log, 3 — в оба;
- и второй(varchar2) — собственно сама строка, которую пишем.
Пример:
exec dbms_system.ksdwrt(2, 'Test Alert Message');
Второй вариант — использовать пакет utl_mail или более низкоуровневые — utl_smtp или utl_tcp.
utl_mail — это более удобная обертка для utl_smtp, но для ее использования обязательно необходимо установить параметр smtp_out_server. Вы можете сделать это как только для сессии — «ALTER SESSION SET smtp_out_server = ...» так и для системы «ALTER SYSTEM SET smtp_out_server = ...».
Не удивляйтесь, если вы не можете найти этот пакет у себя — изначально он не включен и для его создания вы должны выполнить два скрипта:
sqlplus sys/<pwd>
SQL> @$ORACLE_HOME/rdbms/admin/utlmail.sql
SQL> @$ORACLE_HOME/rdbms/admin/prvtmail.plb
Сведем все воедино в пакет pkg_partitions
Методы пакета:
- function get_penultimate_maxvalue(p_table_owner varchar2, p_table_name varchar2) return varchar2;
Функция принимает в качестве параметров владельца и имя таблицы и возвращает значение условия(high_value) предпоследней секции. Данная информация может быть нужна, например, в случаях, где последняя секция — секция c maxvalue параметром, и, соответственно, параметр предпоследней секции может быть нужен для определения параметра для новой секции.
- function get_maxvalued_partitions return tables_props_array pipelined;
Функция возвращающая названия таблиц и их владельцев, у которых начала заполняться последняя секция.
Пример использования:
- select
- p.*,
- sys.pkg_partitions.get_penultimate_maxvalue(p.table_owner,p.table_name) pre_maxvalue
- from
- table(sys.pkg_partitions.get_maxvalued_partitions) p
- function get_maxvalued_partitions_html return varchar2;
Функция возвращает тоже самое, что и get_maxvalued_partitions, но в виде html-таблицы
- procedure send_partitions_report(mail varchar2);
Процедура отправки отчета с таблицами, у которых начала заполняться последняя секция. Единственный параметр — адрес, кому отсылать.
Код пакета:
- create or replace package body pkg_partitions is
- /** Функция возвращающая параметр для предпоследней секции
- * @param i_table_name Имя таблицы
- * @return varchar2
- */
- function get_penultimate_maxvalue(p_table_owner varchar2,p_table_name varchar2) return varchar2 is
- l_cursor integer default dbms_sql.open_cursor;
- l_ignore number;
- l_long_val varchar2(4000);
- l_long_len number;
- l_buflen number := 4000;
- l_curpos number := 0;
- begin
- dbms_sql.parse( l_cursor,
- 'select p.high_value from all_tab_partitions p where p.table_owner like :o and p.table_name like :x and p.partition_position= ( select max(p1.partition_position)-1 from all_tab_partitions p1 where p.table_owner like :o and p1.table_name like :x )'
- ,
- dbms_sql.native );
- dbms_sql.bind_variable( l_cursor, ':x', p_table_name );
- dbms_sql.bind_variable( l_cursor, ':o', p_table_owner );
-
- dbms_sql.define_column_long(l_cursor, 1);
- l_ignore := dbms_sql.execute(l_cursor);
- if (dbms_sql.fetch_rows(l_cursor)>0)
- then
- dbms_sql.column_value_long(l_cursor, 1, l_buflen, l_curpos ,
- l_long_val, l_long_len );
- end if;
- dbms_sql.close_cursor(l_cursor);
- return l_long_val;
- end;
-
- /** Функция возвращающая названия таблиц и их владельцев, у которых начала заполняться последняя секция
- * @return
- * table_name varchar2(4000),
- * table_owner varchar2(4000),
- * partitions_count number,
- * partition_name varchar2(4000));
- */
- function get_maxvalued_partitions return tables_props_array pipelined is
- l_cursor integer default dbms_sql.open_cursor;
- l_count number;
- l_ignore integer;
- l_data table_props;
- cursor l_partitions is
- select
- pl.table_owner,
- pl.table_name,
- count(1) cnt,
- max(pl.partition_name) keep(dense_rank last order by (pl.partition_position)) partition_name
- from dba_tab_partitions pl
- where pl.table_name not like 'BIN$%'
- group by pl.table_owner,pl.table_name
- having count(1)>1;
- BEGIN
- FOR part in l_partitions
- LOOP
- dbms_sql.parse( l_cursor,
- 'select count(1) from '||part.table_owner||'.'||part.table_name
- ||' partition('||part.partition_name||')'
- ||' where rownum<2'
- ,
- dbms_sql.native );
- dbms_sql.define_column(l_cursor,1,l_count);
- l_ignore:=dbms_sql.execute_and_fetch(l_cursor);
- dbms_sql.column_value(l_cursor,1,l_count);
- if (l_count>0) then
- l_data.table_name:=part.table_name;
- l_data.table_owner:=part.table_owner;
- l_data.partitions_count:=part.cnt;
- l_data.partition_name:=part.partition_name;
- pipe row(l_data);
- end if;
- END LOOP;
- END;
-
- /** Функция возвращающая названия таблиц и их владельцев в виде html, у которых начала заполняться последняя секция
- * @return
- * table_name varchar2(4000),
- * table_owner varchar2(4000),
- * partitions_count number,
- * partition_name varchar2(4000));
- */
- function get_maxvalued_partitions_html return varchar2 is
- l_cursor integer default dbms_sql.open_cursor;
- l_count number;
- l_ignore integer;
- l_data varchar2(4000);
- cursor l_partitions is
- select
- pl.table_owner,
- pl.table_name,
- count(1) cnt,
- max(pl.partition_name) keep(dense_rank last order by (pl.partition_position)) partition_name
- from dba_tab_partitions pl
- where pl.table_name not like 'BIN$%'
- group by pl.table_owner,pl.table_name
- having count(1)>1;
- BEGIN
- l_data:='<html><body><table border=1>'
- ||'<tr><th>Table name</th>'
- ||'<th>Table owner</th>'
- ||'<th>Partitions count</th>'
- ||'<th>Partition name</th>'
- ||'<th>Pre maxvalue</th>';
-
- FOR part in l_partitions
- LOOP
- dbms_sql.parse( l_cursor,
- 'select count(1) from '||part.table_owner||'.'||part.table_name
- ||' partition('||part.partition_name||')'
- ||' where rownum<2'
- ,
- dbms_sql.native );
- dbms_sql.define_column(l_cursor,1,l_count);
- l_ignore:=dbms_sql.execute_and_fetch(l_cursor);
- dbms_sql.column_value(l_cursor,1,l_count);
- if (l_count>0) then
- l_data:=l_data||'<tr><td>'
- ||part.table_name
- ||'</td><td>'
- ||part.table_owner
- ||'</td><td>'
- ||part.cnt
- ||'</td><td>'
- ||part.partition_name
- ||'</td></tr>';
- end if;
- END LOOP;
- l_data:=l_data||'</table></body></html>';
- return l_data;
- END;
- /**
- * Процедура отправки отчета с таблицами, у которых начала заполняться последняя секция
- */
- procedure send_partitions_report(mail varchar2)
- is
- msg_body varchar2(4000);
- BEGIN
- select pkg_partitions.get_maxvalued_partitions_html into msg_body from dual;
- --EXECUTE IMMEDIATE 'ALTER SESSION SET smtp_out_server = ''our_mailserver''';
- utl_mail.send(
- sender => 'oracleDBA@dbdomain.com',
- recipients => mail,
- subject => 'Maxvalued partitions Report',
- message => msg_body,
- mime_type => 'text/html');
- END;
- end pkg_partitions;
* This source code was highlighted with Source Code Highlighter.
UPD
Как правильно подсказал zhekappp можно использовать num_rows при включении автоматического сбора статистики. Сбор статистики можно будет включить добавлением задания с помощью dbms_job с dbms_stats.gather_table_stats.
Тогда нужно будет убрать запрос количества записей в секции и изменить запрос на:
- select
- pl.table_owner,
- pl.table_name,
- count(1) cnt,
- max(pl.num_rows) keep(dense_rank last order by (pl.partition_position)) partition_rows,
- max(pl.partition_name) keep(dense_rank last order by (pl.partition_position)) partition_name
- from dba_tab_partitions pl
- where pl.table_name not like 'BIN$%'
- group by pl.table_owner,pl.table_name
Полный код пакета для этого случая можно взять тут: http://www.xt-r.com/2010/10/pkgpartitions.html
Автоматическое выполнение
Осталось только настроить автоматическое выполнение. Сделаем это с помощью dbms_job.
Например, ежедневное автоматическое выполнение скрипта получения данных:
- declare
- job binary_integer;
- begin
- dbms_job.submit(
- job,
- 'pkg_partitions.send_partitions_report(''dba@domain.ru'');',
- sysdate,
- 'trunc(sysdate)+1');
- dbms_output.put_line(job);
- end;