![]() |
Динамическое пересоздание глобальных временных таблицИсточник: incomua
ВведениеИногда приходиться решать задачи управления таблицами с отличной от классических канонов структурой. По ряду причин, такие таблицы растут как вниз (добавление строк данных), так и вправо (добавление столбцов). Причиной может быть исторически принятая, унаследованная структура данных (например, приложение, портированное в СУБД Oracle без перепроектирования структур), или проблемы производительности на этапе сложного расчета. Например, базовые данные находятся в правильном, нормализованном представлении, а на начальном этапе многоступенчатого расчета производиться "динамическое распрямление вправо" по слабо изменяемому признаку и заполнение заранее созданных глобальных временных таблиц (Global Temporary Table - GTT). Результаты расчета могут быть затем агрегированы или "схлопнуты" по какому-либо признаку. Это может быть актуально, когда объем данных в одном сеансе расчета очень значителен (сотни тысяч или миллионы строк). Как правило, это расчетные задачи OLAP (DSS), например, расчет времени и скорости продаж товаров по всей сети магазинов, прогноз товарного запаса, расчет материального баланса. В результате такого представления GTT, "высота" таблицы сокращается пропорционально количеству магазинов сети (слабо изменяемому признаку), например в 50 раз, с 30 млн. до 600 тыс. строк для каждого типа данных (остатки, продажи и т.д.). Я не стану давать оценок таким структурам с точки зрения классического проектирования, скажу лишь, что они используются и, на этапе расчета, могут давать значительный выигрыш в производительности. Тем более, если динамика роста вправо приемлема (ограничение в Oracle 9i - 1000 столбцов). Реализация такого расчетного механизма возможна, например, с применением динамического SQL и наборов (collections). Итак, каждый сеанс использует для расчетов некоторый набор PL/SQL-пакетов, хранит данные своего расчета в наборе временных таблиц (например, уровня сеанса, ON COMMIT PRESERVE ROWS). Следовательно, мы сталкиваемся с проблемой пересоздания временных таблиц после добавления или удаления слабо изменяющегося признака (магазина). Пересоздание возможно как сразу (в оnline), так и отложенно (с помощью задания, выполняемого по определенному графику, например, ночью, при минимальной нагрузке на сервер). Алгоритм действий
1. Кстати, что-то я не обнаружил в документации описания этого типа блокировки. А ведь, как легко убедиться, именно такие блокировки устанавливаются, когда сеанс вставил какие-то данные в глобальную временную таблицу уровня сеанса... - Примечание В.К. Полный текст пакета представлен в Приложении 2, я же остановлюсь на необходимых деталях и дополнениях. Тонкости реализации1. Получение списка блокирующих сеансов и блокировки GTTПри первой вставке в глобальную временную таблицу сервер Oracle устанавливает на нее блокировку 'TO', которая удерживается при любых дальнейших изменениях, до наступления одного из двух условий:
Таким образом, чтобы найти блокирующий сеанс, необходимо искать блокировки любой пересоздаваемой глобальной временной таблицы, участвующей в расчетах (в нашем примере - ZZZ_TEST). Установки статуса INVALID для создаваемого пакета TM_UTIL (и дальнейшую перекомпиляцию самого себя) можно избежать, используя динамический SQL в теле пакета. В результате, имеем два фильтра для поиска блокирующего сеанса (тип блокировки и имя GTT). В качестве параметров дальнейшей команды ALTER SYSTEM KILL SESSION подаем полученные идентификаторы сеанса, SID и SERIAL#. -- Курсор наличия блокирующих сеансов
CURSOR SESS_bl_cur
IS
SELECT distinct VS.SID, VS.SERIAL#
FROM V$SESSION VS, V$LOCK VL, DBA_OBJECTS OBJ
WHERE OBJ.OBJECT_NAME = 'ZZZ_TEST' AND
VL.TYPE='TO' AND
VL.ID1=OBJ.OBJECT_ID AND
VL.SID=VS.SID;
-- Запись для выборки из курсора
sess_bl_rec SESS_bl_cur%ROWTYPE;
Чтобы пакет смог успешно обратиться к соответствующим представлениям словаря данных, необходимо выдать пользователю-создателю пакета следующие привилегии: GRANT SELECT ON SYS."V_$SESSION" TO "SHOPS_MOD" GRANT SELECT ON SYS."V_$LOCK" TO "SHOPS_MOD" GRANT SELECT ON SYS."DBA_OBJECTS" TO "SHOPS_MOD" Потребуются также (см. далее) привилегии: GRANT SELECT ON SYS."V_$PROCESS" TO "SHOPS_MOD" GRANT SELECT ON SYS."V_$INSTANCE" TO "SHOPS_MOD" Впрочем, если безопасность и принцип минимальности привилегий вас не беспокоят, достаточно будет пары операторов: GRANT SELECT ANY TABLE TO "SHOPS_MOD"; GRANT SELECT ANY DICTIONARY TO "SHOPS_MOD"; 2. Оповещение с помощью DBMS_ALERT "думающих" сеансовСигнал (Alert) создается и контролируется на считающем клиенте, а имя сигнала либо жестко "зашивается" в пакете, либо может ему передаваться во входных параметрах. Отправка сигнала реализуется с помощью автономной транзакции, дабы преждевременно не фиксировать основную транзакцию. PROCEDURE Alert_msg
(a_alert_name in VARCHAR2, a_alert_msg IN VARCHAR2)
AS
PRAGMA AUTONOMOUS_TRANSACTION;
Begin
-- Пошлем сообщение всем, кто зарегистрировался для получения нашего сигнала
DBMS_ALERT.signal(a_alert_name, a_alert_msg);
COMMIT;
end Alert_msg;
3. Прекращение работы блокирующих сеансовРеализуем стандартным методом: ALTER SYSTEM KILL SESSION 'int1, int2' где параметры - полученные ранее SID и SERIAL#. Более мягкий вариант: ALTER SYSTEM DISCONNECT SESSION 'int1 , int2' POST_TRANSACTION IMMEDIATE 4. Проверка и "убивание" зависших KILLED-сеансов для текущего ORACLE_SIDЧасто происходит так, что после завершения сеанса командой ALTER SYSTEM, сервер Oraсle не завершает сеанс, а присваивает ему статус 'KILLED'. Такие "зависшие" сеансы не освобождают ресурсы сервера (блокировки, защелки и т.д.), что не позволяет, в частности, пересоздавать использовавшиеся ими временные таблицы. Справедливости ради стоит заменить, что в версии 9i ситуация кардинально улучшилась. Для решения проблемы нужно "убить" серверный процесс (нить) зависшего KILLED-сеанса. В зависимости от платформы, существуют следующие варианты: 1.(NT): Использую Oracle-утилиту orakill. Добавляю instance_name, полученный из v$instance: SELECT 'orakill ' // i.instance_name // ' ' // p.spid as Kill_cmd
FROM v$process p, v$session s, v$instance i
WHERE p.addr = s.paddr AND
s.status = 'KILLED';
2.(SUSE Linux): Использую стандартную утилиту kill: SELECT '/bin/kill -s KILL ' // p.spid as Kill_cmd
FROM v$process p, v$session s
WHERE p.addr = s.paddr AND
s.status = 'KILLED';
(В представленном далее коде пакета я рассматриваю вариант реализации на SUSE Linux). Но это ещё не всё, т.к. хотелось бы выполнять эту процедуру прямо из тела нашего пакета, а не через внешние задания или планировщики. Фактически, речь идет о технологии выполнения команд ОС из PL/SQL. Это можно сделать с применением Java в Oracle. Создается java-класс OSUtil и функция-обертка, RUN_CMD. Плюс, для удобства представления результатов через DBMS_OUTPUT, можно создать процедуру RC. ------------------------------------------------------------------
-- 4.1. Технология выполнения команд ОС из PL/SQL
-------------------------------------------------------------------
create or replace and compile java source named "OSUtil" as
import java.io.*;
import java.lang.*;
public class OSUtil extends Object
{
public static int RunThis(String args)
{
Runtime rt = Runtime.getRuntime();
int rc = -1;
try
{
Process p = rt.exec(args);
int bufSize = 4096;
BufferedInputStream bis =
new BufferedInputStream(p.getInputStream(), bufSize);
int len;
byte buffer[] = new byte[bufSize];
// Echo back what the program spit out
while ((len = bis.read(buffer, 0, bufSize)) != -1)
System.out.write(buffer, 0, len);
rc = p.waitFor();
}
catch (Exception e)
{
e.printStackTrace();
rc = -1;
}
finally
{
return rc;
}
}
}
/
--------------------------------------------------------------
create or replace function RUN_CMD( p_cmd in varchar2) return number
AUTHID CURRENT_USER as language java
name 'OSUtil.RunThis(java.lang.String) return integer';
/
--------------------------------------------------------------
create or replace procedure RC(p_cmd in varchar2)
as
x number;
begin
x := run_cmd(p_cmd);
DBMS_OUTPUT.PUT_LINE('run_cmd returned : '//rpad(x, 3, ' ')//' for '//p_cmd);
end;
/
-------------------------------------------------------------------------------------
-- 4.2. Дать привилегии (от имени SYS), на выполнение команды (утилиты)
-------------------------------------------------------------------------------------
begin
dbms_java.grant_permission (
'SHOPS_MOD',
'SYS:java.io.FilePermission',
'/bin/kill',
'execute'
);
dbms_java.grant_permission (
'SHOPS_MOD',
'SYS:java.lang.RuntimePermission',
'*',
'writeFileDescriptor'
);
end;
/
-------------------------------------------------------------------------------------
-- 4.3. Сымитируем пакетное выполнение:
-------------------------------------------------------------------------------------
-- В первом сеансе, можно проверить свой SID:
select SID, SERIAL# from v$session where audsid=userenv('SESSIONID');
-- Из другого сеанса можно завершить первый (используя SID как параметр команды kill)
set serveroutput on size 1000000;
exec dbms_java.set_output(1000000);
exec shops_mod.rc('/bin/kill -s KILL 11630');
5. Пересоздание всех необходимых временных таблиц и индексовВыполняются все необходимые пересоздания структур, которым теперь не мешают никакие блокировки. Предполагается, что новые сеансы, работающие с временными таблицами, не появятся, потому что это действие выполняется в период минимальной загруженности сервера. 6. Перекомпиляция всех или выбранных INVALID-объектовПонятно, что после пересоздания временных таблиц все зависимые от них объекты станут недействительными (INVALID), поэтому наша задача их перекомпилировать. Необходимо предотвратить перекомпиляцию пакетом самого себя (TM_UTIL). Это можно сделать двумя способами:
Я буду использовать последний вариант и перекомпилировать объекты только одной схемы, SHOPS_MOD. Если понадобится перекомпилировать зависимые объекты во всех схемах (уже от имени SYSTEM), нужно сделать UNION c 'alter session set current_schema owner'. Таким образом, запрос меняется: SELECT DISTINCT 'alter session set current_schema=' //
owner //
';' cmd,
owner,
1 order_col,
NULL object_name
FROM dba_objects
WHERE status = 'INVALID'
AND object_type IN ('PACKAGE',
'PACKAGE BODY',
'VIEW',
'PROCEDURE',
'FUNCTION',
'TRIGGER')
UNION
SELECT 'ALTER ' //
DECODE (
object_type,
'PACKAGE BODY', 'PACKAGE',
object_type
) //
' ' //
owner //
'.' //
object_name //
' COMPILE' //
DECODE (
object_type,
'PACKAGE BODY', ' BODY',
''
) //
';' cmd,
owner,
2 order_col,
object_name
FROM dba_objects outer
WHERE status = 'INVALID'
AND object_type IN ('PACKAGE',
'PACKAGE BODY',
'VIEW',
'PROCEDURE',
'FUNCTION',
'TRIGGER')
AND ( object_type <>
'PACKAGE BODY'
OR NOT EXISTS ( SELECT NULL
FROM dba_objects
WHERE owner =
outer.owner
AND object_name =
outer.object_name
AND object_type =
'PACKAGE'
AND status =
'INVALID')
)
ORDER BY 2, 3, 4
Приложение 1: Тестовые таблицы-- "Распрямленная" глобальная временная таблица для этапа расчета
CREATE GLOBAL TEMPORARY TABLE ZZZ_TEST
(DATE_ DATE,
ART VARCHAR2(30),
"ВВЦ_п" NUMBER,
"Л21_п" NUMBER,
"Мин_п" NUMBER,
"Икеа_п" NUMBER
-- ...
-- Магазинов может быть очень много
)
ON COMMIT PRESERVE ROWS;
CREATE INDEX I_ZZZ_TEST ON ZZZ_TEST (ART ASC);
-- Справочник объектов хранения (магазинов)
CREATE TABLE N_OBJ_STORAGE
(ID_OBJ NUMBER(7,0) NOT NULL,
ID_PROJECT NUMBER(7,0),
ID_LOC NUMBER(7,0),
TYPE_ VARCHAR2(20),
NAME_ VARCHAR2(200),
SHORTNAME VARCHAR2(20),
ACCOUNTTD VARCHAR2(20),
ADDRESS VARCHAR2(200),
TELEPHONE VARCHAR2(100),
DATEOPEN DATE,
DATECLOSE DATE,
INUSE NUMBER(1,0),
STATUS VARCHAR2(1) -- Флаг изменения записи [I,U,D]
);
Приложение 2: Исходный текст пакета TM_UTIL-------------------------------------------------------
PACKAGE TM_UTIL
IS
TYPE TStringTab IS TABLE OF VARCHAR2(255)
INDEX BY BINARY_INTEGER;
PROCEDURE Alert_msg
(a_alert_name IN VARCHAR2,
a_alert_msg IN VARCHAR2);
FUNCTION KP_Recreate_TEST
RETURN NUMBER;
-- Возврат: 0 - Ошибка; 1 - Было пересоздание; 2 - Не было пересоздания
END;
-------------------------------------------------------
PACKAGE BODY TM_UTIL
IS
-- Вызов:
-- Alert_msg(alert_name, alert_msg);
PROCEDURE Alert_msg
(a_alert_name in VARCHAR2, a_alert_msg IN VARCHAR2)
AS
PRAGMA AUTONOMOUS_TRANSACTION;
Begin
-- Пошлем сообщение всем, кто зарегистрировался...
DBMS_ALERT.signal(a_alert_name,a_alert_msg);
COMMIT;
end Alert_msg;
/*==========================================================================*/
/* Ввод/модификация нового Канала Реализации (К.Р.):
/* Возврат: 0 - Ошибка; 1 - Было пересоздание; 2 - Не было пересоздания
/*==========================================================================*/
FUNCTION KP_Recreate_TEST
RETURN NUMBER
AS
-- Курсор факта изменения Обьекта хранения (поле STATUS: [I,U,D]).
-- Т.е. при удалении К.Р. запись не удаляется, а STATUS := 'D'
CURSOR STATUS_cur
IS
select ID_OBJ, ID_LOC, TYPE_, SHORTNAME
from N_OBJ_STORAGE WHERE STATUS IS NOT NULL;
st_rec STATUS_cur%ROWTYPE;
-- Курсор наличия блокирующих сеансов.
-- Можно использовать параметризованный курсор (для OBJECT_NAME и TYPE)
CURSOR SESS_bl_cur
IS
SELECT distinct VS.SID, VS.SERIAL#
FROM V$SESSION VS, V$LOCK VL, DBA_OBJECTS OBJ
WHERE OBJ.OBJECT_NAME = 'ZZZ_TEST' AND
VL.TYPE='TO' AND
VL.ID1=OBJ.OBJECT_ID AND
VL.SID=VS.SID;
sess_bl_rec SESS_bl_cur%ROWTYPE;
-- Курсор "убивания" подвисших KILLED-сеансов
CURSOR ORAKILL_cur
IS
SELECT '/bin/kill -s KILL ' // p.spid as Kill_cmd
FROM v$process p, v$session s
WHERE p.addr = s.paddr AND
s.status = 'KILLED';
orakill_rec ORAKILL_cur%ROWTYPE;
-- Перекомпилируем в цикле все INVALID-обьекты схемы SHOPS_MOD
CURSOR RECOMPILE_cur
IS
SELECT 'ALTER '
// DECODE (object_type,
'PACKAGE BODY', 'PACKAGE',
object_type
)
// ' '
// owner
// '.'
// object_name
// ' COMPILE'
// DECODE (object_type, 'PACKAGE BODY', ' BODY', '') cmd,
owner, object_name
FROM dba_objects OUTER
WHERE status = 'INVALID'
AND owner = 'SHOPS_MOD'
AND object_type IN
('PACKAGE',
'PACKAGE BODY',
'VIEW',
'PROCEDURE',
'FUNCTION',
'TRIGGER'
)
AND ( object_type <> 'PACKAGE BODY'
OR NOT EXISTS (
SELECT NULL
FROM dba_objects
WHERE owner = OUTER.owner
AND object_name = OUTER.object_name
AND object_type = 'PACKAGE'
AND status = 'INVALID')
);
recompile_rec RECOMPILE_cur%ROWTYPE;
l_alert_name Varchar2(30) := 'TM_RECREATE'; -- Имя сигнала
l_alert_msg Varchar2(500) :=
'ВНИМАНИЕ: Для введения в строй нового Канала реализации, ' // Chr(13)
// 'необходимо сохранить данные и выйти из текущей формы расчета. ' // Chr(13)
// 'Через 3 минуты сеанс будет завершен!';
la_smag TStringTab;
l_strSQL VARCHAR2(16000);
l_strSQL2 VARCHAR2(16000);
l_strSQL3 VARCHAR2(16000);
f_exit BOOLEAN := false; -- Выход: по умолчанию - НЕТ
l_ret NUMBER := 2; -- Возврат: по умолчанию - Не было пересоздания
BEGIN
-- Были ли измененные К.Р.?
-- Используется как факт начала пересоздания (%NOTFOUND=FALSE)
OPEN STATUS_cur;
FETCH STATUS_cur INTO st_rec;
IF STATUS_cur%NOTFOUND THEN
f_exit:=true;
ELSE
f_exit:=false;
END IF;
CLOSE STATUS_cur;
-- Выход, если нечего перестраивать (2).
IF f_exit=true THEN RETURN l_ret; END IF;
-- Да: были измененные К.Р. => Проверить, есть ли блокирующие сеансы.
-- Получаем список "нехороших" сеансов.
-- Если есть нехорошие сеансы - разослать им сообщение о выходе,
-- ждать 3 минуты, переоткрыть.
-- Если после перечитывания еще живут - KILL SESSION,
-- а потом - прекращение процессов (нитей)
OPEN SESS_bl_cur;
FETCH SESS_bl_cur INTO sess_bl_rec;
IF SESS_bl_cur%FOUND THEN
-- Пошлем сообщение всем "считающим" сеансам
TM_UTIL.Alert_msg(l_alert_name, l_alert_msg);
CLOSE SESS_bl_cur;
-- Подождем 3 минуты...
DBMS_LOCK.SLEEP(180);
-- Перечитаем заново... кто же нас не послушался ;)
OPEN SESS_bl_cur;
LOOP
FETCH SESS_bl_cur INTO sess_bl_rec;
EXIT WHEN SESS_bl_cur%NOTFOUND;
-- 2.2. Убиваем "нехорошие" сеансы после перезапроса
l_strSQL :='ALTER SYSTEM KILL SESSION ''' // to_char(sess_bl_rec.SID) // ','
// to_char(sess_bl_rec.SERIAL#) // '''';
EXECUTE IMMEDIATE l_strSQL;
END LOOP;
-- Убиваем KILLED-сеансы (если такие есть) для текущего ORACLE_SID
OPEN ORAKILL_cur;
LOOP
FETCH ORAKILL_cur INTO orakill_rec;
EXIT WHEN ORAKILL_cur%NOTFOUND;
-- Убить процесс (нить) зависшего KILLED-сеанса
shops_mod.rc(orakill_rec.Kill_cmd);
END LOOP;
CLOSE ORAKILL_cur;
END IF; -- По факту нехороших сеансов
CLOSE SESS_bl_cur; -- Для обоих случаев выхода
-- Да, были измененные К.Р. и, возможно, убивали сеансы.
-- Пересоздать нужные временные таблицы и их индексы
-- Удаляем временные таблицы (индексы удаляются автоматически)
l_ret:= 0; -- Как бы прогнозируем ошибку
EXECUTE IMMEDIATE 'drop table ZZZ_TEST';
-- Создаем новые таблицы
-- Создадим набор новых "доступных" магазинов
SELECT SHORTNAME
BULK COLLECT INTO LA_SMAG
FROM N_OBJ_STORAGE
WHERE TYPE_='КР' AND INUSE=1;
-- Пересоздание ZZZ_TEST
l_strSQL := 'CREATE GLOBAL TEMPORARY TABLE ZZZ_TEST (DATE_ DATE, ART VARCHAR2(30), ';
FOR j IN 1 .. la_smag.COUNT LOOP
l_strSQL := l_strSQL // '"' // la_smag(j) // '_п" NUMBER, ';
END LOOP;
l_strSQL := l_strSQL // '"Прод_маг" NUMBER) ON COMMIT PRESERVE ROWS';
EXECUTE IMMEDIATE l_strSQL;
-- Пересоздание индексов
EXECUTE IMMEDIATE 'CREATE INDEX SHOPS_MOD.I_ZZZ_TEST ON SHOPS_MOD.ZZZ_TEST (ART)';
-- Очищаем статус Каналов Реализации
EXECUTE IMMEDIATE 'UPDATE N_OBJ_STORAGE SET STATUS=NULL';
-- Перекомпилируем в цикле все INVALID-обьекты схемы SHOPS_MOD
-- в т.ч. наиболее нужные для нас считающие пакеты
OPEN RECOMPILE_cur;
LOOP
FETCH RECOMPILE_cur INTO recompile_rec;
EXIT WHEN RECOMPILE_cur%NOTFOUND;
-- Выполнить сформированный ALTER XXX SHOPS_MOD.XXX COMPILE
EXECUTE IMMEDIATE recompile_rec.cmd;
END LOOP;
CLOSE RECOMPILE_cur;
-- 6. Итоговый Commit и возврат результата
COMMIT;
l_ret:=1; -- Было успешное пересоздание
RETURN l_ret;
END KP_Recreate_TEST;
/*==========================================================================*/
END;
Эту статью написал и предложил для публикации в рассылке Alex Volny. Публикуется с разрешения автора. Все вопросы по содержанию статьи направляйте ему. Если автор захочет прокомментировать ваши вопросы для всех подписчиков, эти комментарии будут опубликованы в рассылке. |