вторник, 20 декабря 2016 г.

Два кейса для ora_rowscn в Oracle 11g

В СУБД Oracle системный номер изменения (SCN, system change number) есть непрерывно возрастающее число. Каждая транзакция, и даже каждое чтение этого номера из БД увеличивает его:

SQL> select current_scn from v$database;
CURRENT_SCN
-----------
 7211772580

SQL> select current_scn from v$database;
CURRENT_SCN
-----------
 7211772583

Начиная с версии 10 в таблицах БД Oracle появился псевдостолбец ora_rowscn. Этот псевдостолбец содержит SCN, связанный с последним изменением соответствующей строки. Посмотрим на него поближе.

Создадим таблицу locations, заполним ее данными и выполним запрос со столбцом ora_rowscn:

SQL> create table locations (
  2      id number primary key,
  3      parent number references locations (id) on delete cascade,
  4      name varchar2(50) not null
  5  );
Table created

SQL> 
SQL> insert into locations values (1, null, 'Земля');
1 row inserted
SQL> insert into locations values (2, 1, 'Азия');
1 row inserted
SQL> insert into locations values (3, 2, 'Владивосток');
1 row inserted
SQL> insert into locations values (4, 2, 'Саппоро');
1 row inserted
SQL> insert into locations values (5, 2, 'Стамбул');
1 row inserted
SQL> insert into locations values (6, 1, 'Европа');
1 row inserted
SQL> insert into locations values (7, 6, 'Реймс');
1 row inserted
SQL> insert into locations values (8, 6, 'Санкт-Петербург');
1 row inserted
SQL> commit;
Commit complete

SQL> select id, ora_rowscn from locations;
        ID ORA_ROWSCN
---------- ----------
         1 7211772620
         2 7211772620
         3 7211772620
         4 7211772620
         5 7211772620
         6 7211772620
         7 7211772620
         8 7211772620
8 rows selected

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

Изменим одну строку в отдельной транзакции и повторим запрос:

SQL> update locations set name = upper(name) where id = 3;
1 row updated

SQL> commit;
Commit complete

SQL> select id, ora_rowscn from locations;
        ID ORA_ROWSCN
---------- ----------
         1 7211772627
         2 7211772627
         3 7211772627
         4 7211772627
         5 7211772627
         6 7211772627
         7 7211772627
         8 7211772627
8 rows selected

Изменилось значение ora_rowscn для всех строк. Почему это произошло?

По умолчанию значение SCN, отображаемое в псевдостолбце ora_rowscn, хранится на уровне блока данных (СУБД Oracle пишет и читает данные блоками). А один блок данных в нашем случае вмещает все строки таблицы. Получается, что обновив одну строку, мы "пометили" новым SCN все строки блока, в котором находится измененная строка.

Для того, чтобы SCN хранился индивидуально для каждой строки, таблица должна быть создана с опцией rowdependencies. У нашей таблицы свойство rowdependencies (по умолчанию) отключено:

SQL> select table_name, dependencies
  2  from user_tables
  3  where table_name = 'LOCATIONS';

TABLE_NAME                     DEPENDENCIES
------------------------------ ------------
LOCATIONS                      DISABLED

К сожалению, с помощью alter table данное свойство включить нельзя. Нужно пересоздать таблицу:

SQL> drop table locations;
Table dropped

SQL> create table locations (
  2      id number primary key,
  3      parent number references locations (id) on delete cascade,
  4      name varchar2(50) not null
  5  ) rowdependencies;
Table created

SQL> insert into locations values (1, null, 'Земля');
1 row inserted
SQL> insert into locations values (2, 1, 'Азия');
1 row inserted
SQL> insert into locations values (3, 2, 'Владивосток');
1 row inserted
SQL> insert into locations values (4, 2, 'Саппоро');
1 row inserted
SQL> insert into locations values (5, 2, 'Стамбул');
1 row inserted
SQL> insert into locations values (6, 1, 'Европа');
1 row inserted
SQL> insert into locations values (7, 6, 'Реймс');
1 row inserted
SQL> insert into locations values (8, 6, 'Санкт-Петербург');
1 row inserted
SQL> commit;
Commit complete

SQL> select id, ora_rowscn from locations;
        ID ORA_ROWSCN
---------- ----------
         1 7211772643
         2 7211772643
         3 7211772643
         4 7211772643
         5 7211772643
         6 7211772643
         7 7211772643
         8 7211772643
8 rows selected

SQL> select table_name, dependencies
  2  from user_tables
  3  where table_name = 'LOCATIONS';

TABLE_NAME                     DEPENDENCIES
------------------------------ ------------
LOCATIONS                      ENABLED

Теперь в таблице должны храниться SCN для каждой строки индивидуально. Проверим это:

SQL> update locations set name = upper(name) where id = 3;
1 row updated

SQL> commit;
Commit complete

SQL> select id, ora_rowscn from locations;
        ID ORA_ROWSCN
---------- ----------
         1 7211772643
         2 7211772643
         3 7211772645
         4 7211772643
         5 7211772643
         6 7211772643
         7 7211772643
         8 7211772643
8 rows selected

Хранение SCN для каждой строки требует дополнительно 6 байт на строку.

Для перевода существующей таблицы в режим rowdependencies с сохранением ее данных и зависимостей и без приостановки работы с ней, можно воспользоваться пакетом dbms_redefinition (см., например, статью Online Table Redefinition (DBMS_REDEFINITION) Enhancements in Oracle Database 11g Release 1).

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

Кейс 1. Оптимистическая блокировка

Оптимистическая блокировка - это когда мы оптимистично полагаем, что между моментом, когда мы прочитали данные из БД, и более поздним моментом, когда мы станем сохранять измененные данные в БД, эти данные не были изменены в БД никем другим. А как проверить, что данные в БД не были изменены? Сравнив текущие данные в БД с ранее считанными - еще не измененными - данными.

Допустим, мы прочитали из таблицы locations значение столбца name строки с id = 8:

select name into :loc_name from locations where id = 8;

и отобразили его в пользовательском интерфейсе для редактирования. После редактирования названия ("Санкт-Петербург" исправили на "Санкт-Петербургъ"), сохраняем изменения:

update locations set
    name = 'Санкт-Петербургъ'
where id = 8
    and name = :loc_name
;

Последнее условие name = :loc_name гарантирует нам, что строка обновится только в случае, если считанное ранее значение name не изменилось в БД за то время, что нам понадобилось на его редактирование. Если же обновлено 0 строк, то изменение не сохранено и пользователь должен получить сообщение об ошибке.

Что, если для редактирования было считано не одно, а 50 полей? В таком случае нужно сравнивать все 50 полей! Альтернатива этому - иметь в таблице столбец с версией/номером изменения, который должен обновляться каждым предложением update для этой таблицы. Тогда этот столбец нужно считывать вместе с другими перед началом редактирования и сравнивать считанное значение с текущим при сохранении изменений:

-- читаем
select name, ..., version
into :loc_name, ..., :loc_version
from locations 
where id = 8
;

-- ...

-- сохраняем изменения
update locations set
    name = 'Санкт-Петербургъ',
    ...
    version = version + 1
where id = 8
    and version = :loc_version
;

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

При условии, что для таблицы включено свойство rowdependencies, перепишем предыдущие запросы так:

-- читаем
select name, ..., ora_rowscn
into :loc_name, ..., :loc_rowscn 
from locations 
where id = 8
;

-- ...

-- сохраняем изменения
update locations set
    name = 'Санкт-Петербургъ',
    ...
where id = 8
    and ora_rowscn = :loc_rowscn
;

Если обновлено 0 строк, это значит, что строка была изменена в другом сеансе, и пользователь должен получить сообщение об ошибке.

Кейс 2. Получение изменений за период

Нередко возникает задача получить все строки таблицы, добавленные или измененные за последний час (6 часов, неделю, ...) или после некоторого момента в прошлом. Это нужно, когда сделанные изменения должны время от времени обрабатываться, например, передаваться в другую систему.

Такую задачу иногда решают добавлением столбца для сохранения в нем даты-времени последнего изменения строки. При этом все изменения таблицы с помощью insert и update должны устанавливать в этом поле текущие дату-время. Нужно либо строго придерживаться этого правила при написании insert и update, либо повесить на таблицу триггер before insert or update, который гарантирует обновление этого поля.

Если нельзя добавить столбец в таблицу (приложение сломается?), то можно повесить на таблицу триггер, который будет регистрировать все изменения строк в другой таблице. Например, так:

create table locations_log (
    id number primary key,
    last_when timestamp with time zone
);

create or replace trigger locations_aiur_trg
after insert or update on locations
for each row
begin
    merge into locations_log tgt
    using (select :new.id id from dual) src
    on (tgt.id = src.id)
    when matched then
        update set last_when = systimestamp
    when not matched then
        insert (id, last_when) values (:new.id, systimestamp)
    ;
end;
/

Благодаря merge, многочисленные изменения одной и той же строки locations будут отражены только в одной строке locations_log.

Теперь, чтобы периодически (или просто время от времени) обрабатывать только измененные строки, нужно запоминать дату-время последней обработки и при следующей обработке выбирать строки, измененные позднее. Для случая, когда таблица locations содержит столбец last_when, это выглядит примерно так:

declare
    l_curr_when timestamp with time zone := systimestamp;
    -- для 1-го раза предыдущее время обработки равно текущему
    l_prev_when timestamp with time zone := 
        get_prev_when(p_default => l_curr_when);
begin
    for r in (
        select * 
        from locations 
        where last_when >= l_prev_when
            and last_when < l_curr_when
    ) loop
        -- здесь обрабатываем изменения
        null;
    end loop;
    -- запоминаем время, относительно которого
    -- более ранние изменения уже обработаны
    save_prev_when(l_curr_when);
end;

Функция get_prev_when возвращает метку времени, сохраненную процедурой save_prev_when или, если сохраненного значения нет, то метку времени, полученную через параметр p_default. (Код этих функции и процедуры не привожу, чтобы не загромождать пример. Все просто: save_prev_when сохраняет значение в некоторой служебной табличке, а get_prev_when выбирает значение оттуда.)

При первом выполнении приведенный код обработает 0 строк, а при втором и последующих будет обрабатывать строки, измененные в интервале между временем предыдущего выполнения (l_prev_when) и текущим временем (l_curr_when).

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

select l.* 
from locations l, locations_log ll 
where l.id = ll.id 
    and ll.last_when >= l_prev_when 
    and ll.last_when < l_curr_when

Что нам может дать ora_rowscn в такой ситуации? Значения SCN обладают тем же свойством, что и значения, возвращаемые systimestamp (или sysdate): они возрастают со временем. Каждая последующая версия строки таблицы имеет большее значение ora_rowscn, чем предыдущая.

Отсюда, код обработки изменений можно переписать так:

declare
    l_curr_scn number;
    l_prev_scn number;
begin
    -- текущий SCN, который больше не повторится
    select current_scn into l_curr_scn from v$database;
    -- для 1-го раза предыдущий SCN равен текущему
    l_prev_scn := get_prev_scn(p_default => l_curr_scn);
    for r in (
        select * 
        from locations 
        where ora_rowscn between l_prev_scn and l_curr_scn
    ) loop
        -- здесь обрабатываем изменения
        null;
    end loop;
    -- запоминаем SCN, по который изменения уже обработаны
    save_prev_scn(l_curr_scn);
end;

Если воспользоваться ora_rowscn, то не нужны ни дополнительная таблица для регистрации изменений, ни даже столбец с меткой времени последнего изменения.

Псевдостолбец ora_rowscn предоставляет нам готовый механизм для выбора строк, измененных после того, как мы запомнили некоторый более ранний SCN. При условии, что для таблицы включено свойство rowdependencies.

В заключение, удалю таблицы, с которыми экспериментировал:

SQL> drop table locations;
Table dropped
SQL> drop table locations_log;
Table dropped

1 комментарий:

  1. Андрей, привет!
    оптимистические блокировки с использованием ora_rowscn работают только в том случае, если обновляемые поля есть в части where.
    здесь Том показывает, что ora_rowscn не работает
    https://asktom.oracle.com/pls/apex/f?p=100:11:0::::P11_QUESTION_ID:2680538100346782134
    и сообщает
    "They do not re-evaluate the rowscn during the current mode get of the block - the rowscn does not trigger a restart"

    но! если добавить поле в where, то работает

    update locations set
    name = 'Санкт-Петербургъ',
    ...
    where id = 8
    and ora_rowscn = :loc_rowscn
    and nvl2(name,1,0) != 2
    ;

    причем интересно, что "a restart" происходит только в том случае если соседняя сессия изменила значение поля на другое ('Санкт-Петербургъ' на 'Ленинград', например).

    ОтветитьУдалить