Рекомендации по обновлению триггеров для конкретных столбцов

#oracle #triggers #optimistic-locking

Вопрос:

Добро пожаловать в Oracle pro

В базе данных Oracle 12 (обновление уже запланировано ;-)) у нас есть настройка различных таблиц, обновляющих общую базовую таблицу с помощью триггеров «после обновления«, таких как:

Search_Flat
ID поле_а поле_b Field_C

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

 CREATE OR REPLACE TRIGGER tr_tbl_1_au_search
    AFTER UPDATE OF
        field_a,
        field_b
    ON schemauser.search_flat
FOR EACH ROW
    BEGIN
        IF :new.field_a <> :old.field_a THEN 
            UPDATE schemauser.search_flat SET field_a = :new.field_a WHERE id = :new.ID;
        END IF;
        IF :new.field_b <> :old.field_b THEN 
            UPDATE schemauser.search_flat SET field_b = :new.field_b WHERE id = :new.ID;
        END IF;
    END;
 

В качестве альтернативы мы также могли бы настроить триггер следующим образом:

 CREATE OR REPLACE TRIGGER tr_tbl_1_au_search
    AFTER UPDATE OF
        field_a,
        field_b
    ON schemauser.search_flat
FOR EACH ROW
    BEGIN
        IF :new.field_a <> :old.field_a OR :new.field_b <> :old.field_b THEN 
            UPDATE schemauser.search_flat 
            SET field_a = :new.field_a, 
                field_b = :new.field_b 
            WHERE id = :new.ID;
        END IF;
    END;
 

Теперь вопрос заключается в настройке самих триггеров. Какой подход лучше в отношении:

  • время блокировки строк search_flat
  • общая производительность затронутых компонентов (т. е. table_1, триггер и search_flat)

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

ORA-02049: время ожидания: распределенная транзакция ожидает блокировки

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

Я с нетерпением жду обсуждения и ваших мыслей.

Комментарии:

1. В коде есть триггер SEARCH_UPDATE и обновление SEARCH_UPDATE; должен ли один из них быть ТАБЛИЦЕЙ1? Кроме того, операторы обновления изменяют только одну строку за раз или они обновляют несколько строк одновременно? Составной триггер может помочь повысить производительность, если для каждой инструкции обновляется несколько строк.

2. количество записей в 4 таблицах подачи слишком велико для обновления материализованного представления при обновлении — если скорость изменения слишком высока для fast refresh on commit использования материализованного представления, подобный метод на основе триггера также, вероятно, будет слишком медленным. Возможно, было бы лучше описать проблемы с производительностью, которые у вас есть, поскольку могут быть лучшие альтернативы

3. Привет @JonHeller , триггер table1 (tr_tbl_1_au_search) обновляет ровно одну строку в SEARCH_FLAT (идентифицируется по идентификатору) при каждом выполнении. Таким образом, никогда не будет затронуто несколько строк одновременно. Но может случиться так, что затронутая строка в таблице 1 обновляется одновременно разными серверами приложений, что приведет к упомянутой проблеме с таймаутом ORA.

4. Привет @ChrisSaxon, мы говорили о миллионах записей, умноженных на 4 вкл. несколько соединений в каждом. Обновление при фиксации занимает до нескольких минут, в то время как триггеры обновления составляют ок. 98% в течение миллисекунд. Пока только один сервер приложений обновляет базовые таблицы, производительность абсолютно нормальная. Мы также обсудили эту проблему с нашими специалистами по БД, и я не думаю, что подход MV будет успешным.

5. Вы говорите, что находитесь в базе данных Oracle 12. Это 12.1 или 12.2? В версии 12.2 для этого вы можете использовать материализованное представление в реальном времени. («Это» означает «ведение сглаженной таблицы для поиска без каких-либо задержек/задержек в результатах»). Кроме того, FWIW, 2-й вариант, который вы представили (одно обновление вместо нескольких обновлений), лучше. «Время блокировки» не имеет значения, так как записи будут заблокированы до тех пор, пока транзакция, вызвавшая триггер, не будет зафиксирована. В вашем подходе вам также необходимо лучше обрабатывать сравнения с нулем и следить за взаимоблокировками. Все сказанное, я думаю, вам следует отказаться от триггеров.

Ответ №1:

Как я понимаю, в вашем посте у вас есть 4 живые таблицы (называемые «таблица1», «таблица2» и т. Д.), По которым вы хотите выполнять поиск, Но запросы от них слишком медленные, поэтому вы хотите вместо этого поддерживать единую плоскую таблицу для поиска и иметь триггеры, чтобы эта плоская таблица всегда обновлялась. Вы хотите знать, какой из двух подходов к запуску лучше.

Я думаю, что ответ «ни то, ни другое», так как оба склонны к тупикам. Представьте себе такой сценарий

Пользователь 1 —

 UPDATE table1 
SET field_a = 500 
WHERE <condition effecting 200 distinct IDs>
 

Пользователь 2 примерно в то же время —

 UPDATE table1 
SET field_b = 700 
WHERE <condition effecting 200 distinct IDs>
 

Триггеры начинают обработку. Вы не можете контролировать порядок, в котором обновляются строки. Может быть, дело обстоит так:

Триггер пользователя 1, индекс времени 100 ->

 UPDATE search_flat SET field_a = 500 WHERE id = 90;
 

Триггер пользователя 2, индекс времени 101 ->

 UPDATE search_flat SET field_b = 700 WHERE id = 91;
 

Триггер пользователя 1, индекс времени 102 ->

 UPDATE search_flat SET field_a = 500 WHERE id = 91;  (waits on user 2's session)
 

Триггер пользователя 2, индекс времени 103 ->

 UPDATE search_flat SET field_b = 700 WHERE id = 90;  (deadlock error)
 

Первоначальное обновление пользователя 2 завершается ошибкой и откатывается.

У вас есть несколько параллельных процессов, все они обновляют один и тот же набор строк search_flat , не контролируя порядок обработки. Это рецепт для тупиковых ситуаций.

Если вы хотите сделать это безопасно, вам не следует рассматривать ни один из FOR EACH ROW подходов к триггеру, которые вы описали. Скорее, сделайте составной триггер для этого.

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

 -- Aside: consider setting this at the system level if on 12.2 or later
--   alter system set temp_undo_enabled=false;

CREATE GLOBAL TEMPORARY TABLE table1_updates_gtt (
  id          NUMBER,
  field_a     VARCHAR2(80),
  field_b     VARCHAR2(80)
) ON COMMIT DELETE ROWS;

CREATE GLOBAL TEMPORARY TABLE table2_updates_gtt (
  id          NUMBER,
  field_a     VARCHAR2(80)
) ON COMMIT DELETE ROWS;

-- .. so on for table3 and 4.

CREATE OR REPLACE TRIGGER table1_search_maint_trg
  FOR INSERT OR UPDATE OR DELETE ON table1  -- with similar compound triggers for table2, 3, 4.
    COMPOUND TRIGGER

  AFTER EACH ROW IS
  BEGIN
    -- Update the table-1 specific GTT with the changes.
    CASE WHEN INSERTING OR UPDATING THEN
      -- Assumes ID is immutable primary key
      INSERT INTO table1_updates_gtt (id, field_a) VALUES (:new.id, :new.field_a);
         WHEN DELETING THEN
      INSERT INTO table1_updates_gtt (id, field_a) VALUES (:old.id, null);  -- or figure out what you want to do about deletes.
    END CASE;
  END AFTER EACH ROW;

  AFTER STATEMENT IS
  BEGIN
    -- Write the data from the GTT to the search_flat table.
    -- NOTE: The ORDER BY in the next line is what saves us from deadlocks.
    FOR r IN ( SELECT id, field_a, field_b FROM table1_updates_gtt ORDER BY id ) LOOP
      -- TODO: replace with BULK processing for better performance, if DMLs can affect a lot of rows
      UPDATE search_flat sf
      SET    sf.field_a = r.field_a,
             sf.field_b = r.field_b
      WHERE  sf.id = r.id
      AND    ( sf.field_a <> r.field_a 
               OR (sf.field_a IS NULL AND r.field_a IS NOT NULL) 
               OR (sf.field_a IS NOT NULL AND r.field_a IS NULL)
               OR sf.field_b <> r.field_b 
               OR (sf.field_b IS NULL AND r.field_b IS NOT NULL) 
               OR (sf.field_b IS NOT NULL AND r.field_b IS NULL)
             );
    END LOOP;             
          
  END AFTER STATEMENT;

END table1_search_maint_trg;
 

Кроме того, как отмечали многочисленные комментаторы, для этого, вероятно, лучше использовать материализованный вид. Если вы используете 12.2 или более позднюю версию, материализованные представления в реальном времени (они же «ВКЛЮЧИТЬ ВЫЧИСЛЕНИЕ ЗАПРОСОВ») предлагают многообещающие возможности для такого рода вещей. Никаких COMMIT накладных расходов для вашего приложения и результатов поиска в режиме реального времени. Просто время поиска немного сокращается, если в базовых таблицах много последних обновлений.

Комментарии:

1. Привет @Мэтью, спасибо за ваше объяснение, которое звучит разумно для меня и, по крайней мере, объясняет причину тупика. Обновления для отдельных базовых таблиц не являются массовыми обновлениями, и для каждого действия пользователя будут обновляться только отдельные строки. Тем не менее, этого, по-видимому, достаточно для тупиков; -)

2. Вы спросили в комментарии выше о версии, которая в настоящее время является Oracle 12.2, но переход на 19.x запланирован на 4 квартал. Так что подход с составными триггерами, похоже, стоит попробовать, а я их еще не знал. В качестве долгосрочного решения я также еще раз проверю возможности с помощью MV, предположительно после миграции. Я буду держать вас в курсе здесь 😉

3. К сожалению, если ваше приложение выполняет однорядные обновления базовых таблиц и у вас возникают тупики, составной триггер не решит вашу проблему (каждый AFTER STATEMENT запуск будет обрабатывать только одну строку). Вам нужно будет либо изменить базовое приложение, чтобы обновить все базовые таблицы в согласованном порядке (например, по ID ), либо забыть об использовании триггеров.

4. Еще одна вещь-дизайнеры часто уделяют слишком много внимания (имо) результатам» в реальном времени». Но если (А) приложение не таково, когда одни и те же пользователи обновляют данные, а затем запускают поиск, ожидая, что их обновления будут отображаться в результатах, или (Б) приложение дает обещания, которые требуют точности данных в реальном времени (например, приложение для продажи билетов), «почти в реальном времени» обычно так же хорошо и открывает ГОРАЗДО больше вариантов дизайна.

5. Приложение соответствует вашему варианту А), когда пользователь «Службы поддержки» обновляет инцидент и мгновенно обновляет представление результатов впоследствии (т. Е. поиск) для проверки последующих процессов. Таким образом, обновление в режиме «реального времени» является одним из их требований. Прежнее решение обрабатывало синхронизацию между нормализованными таблицами и поиск внутри приложения, что приводило к некоторым несоответствиям данных. Поэтому мы хотим, чтобы база данных заботилась о синхронизированных данных как о части суверенитета БД.