Как повысить производительность UPDATE - этот вопрос возникает очень часто, особенно в ETL-разработке.
Наиболее частые случаи массовых обновлений:
- обновление каждой строки в таблице;
- обновление небольшой части строк в очень большой таблице.
В первом случае самый быстрый способ - перестроить таблицу с нуля. Остальные методы будут работать хуже.
Второй случай чаще всего встречается в хранилищах данных и пакетных заданиях, запускаемых на ночь.Создадим таблицы и наполним их данными.
Условия создания - наличие primary key.
Первая таблица будет содержать меньшее количество строк, из нее, как из источника, будут браться значения для второй таблицы с большим количеством данных.
Создаем первую таблицу:
create table stage_src
(id number generated always as identity,
code number,
name varchar2(10));
alter table stage_src add constraint pk_srcid primary key (id);
Наполним данными:
begin
insert into stage_src
(code, name)
select 12 + level as code, 'a'||(1 + level) as name
from dual
connect by level <= 100000;
commit;
end;
Создаем вторую таблицу:
create table stage_trg
(id number generated always as identity,
code number,
name varchar2(10));
alter table stage_trg add constraint pk_trgid primary key (id);
begin
insert into stage_trg
(code, name)
select 12 + level as code, 'a'||(1 + level) as name
from dual
connect by level <= 1000000;
commit;
end;
Применяемые методы:
- Explicit Cursor Loop (Явный курсор в цикле)
- Implicit Cursor Loop (Неявный курсор в цикле)
- UPDATE with nested SET subquery (Обновление с вложенным подзапросом)
- Updateable Join View
- BULK COLLECT / FORALL UPDATE
- MERGE (Слияние)
- Parallel DML MERGE
- Parallel PL/SQL
💧 Explicit Cursor Loop
declare
cursor stg is
select * from stage_src;
rec_stg stg%rowtype;
begin
open stg;
loop
fetch stg into rec_stg;
exit when stg%notfound;
update stage_trg
set code = rec_stg.code,
name = replace (rec_stg.name, 'a', 'b')
where id = rec_stg.id;
end loop;
close stg;
end;
Время выполнения: 5.165 sec
💧 Implicit Cursor Loop
Приведенное время обоих медодов - это время повторных запусков. Время же первого запуска отличается еще больше: явный курсор - 8.163 sec , неявный курсор - 5,334 sec.
Здесь подтверждается практический вывод по работе с этими типами курсоров: неявный курсор всегда будет эффективнее.
Почему так?
Отвечает Том Кайт".. PL/SQL, как и Java, интерпретируется. PL/SQL VM(virtual machine), запускающая PL/SQL, написана на С. Чем больше кода - тем больше интерпретировать. Интерпретированный код работает медленнее, чем собственный код (объектный/двоичный код). Таким образом, у виртуальной машины гораздо больше работы, больше CPU циклов, меньше выполнения напрямую на С - следовательно, меньше производительность..."
Здесь то самое переключение контекста между PL/SQL и SQL для каждого FETCH в цикле.
Подробнее о механизме переключения можно прочитать в этой статье.И прямая цитата Кайта, которую хочется вынести в эпиграф:
- if you can do it in a single sql statement - do so.
- if you cannot, do it in as little PLSQL as you can.
- if you cannot, because plsql cannot do it, try some java (eg: sending an email with an attachment)
- if you cannot do it in java (too slow, or you have some existing 3gl code already) do it in C as an extproc.
Метод |
Время 1-го запуска, sec |
Время 2-го запуска, sec |
Explicit Cursor Loop |
2230.454 |
1727.143 |
Implicit Cursor Loop |
1470.823 |
1466.088 |
Разница во времени при первом запуске подтверждает затраты на переключение контекста. Второй запуск уже дает разницу, которой можно пренебречь.
А вот время выполнения по отношению к времени выполнения методов с таблицами, где объединение идет по primary key, то есть по индексированному полю - катастрофически увеличилось.
Поэтому для использования этих методов объединение таблиц должно быть по индексируемому полю.
А если забежать вперед, то предварительное заключение - на данный момент вообще нет причин использовать для DML-операций эти методы.
💧UPDATE with nested SET subquery (Обновление с вложенным подзапросом)
Время выполнения: 2.315 sec
Метод имеет право на жизнь при наличии индексов и собранной статистики. Применим на небольших объемах. И если в запросе есть коррелирующий подзапрос - в плане, возможно, будет объединение NESTED LOOPS.
💧 Updateable Join View
Время выполнения: 3.234 sec
До-MERGE-вый метод. Встретите в legacy - не пугайтесь, работает при наличии индексов и статистики.
💧 BULK COLLECT / FORALL UPDATE
declare
Время выполнения: 1.434 sec
Причина такого быстрого обновления в том, что BULK COLLECT уменьшает переключение контекста между SQL и механизмом PL/SQL и позволяет механизму SQL получать записи сразу.
Тот же тест, но с курсором.
💧 MERGE
Подробнее o MERGE здесь.
💧 Parallel DML MERGE
execute immediate 'ALTER SESSION ENABLE PARALLEL DML';
merge /*+ parallel(trg) parallel(src) */
Время выполнения: 0.186 sec
В случае MERGE необходимо распараллеливать и целевую таблицу, и таблицу-источник.
Базе данных проще распараллеливать большие операторы на основе наборов, чем построчный императивный код.
💧 Parallel PL/SQL
Обратите внимание, что данные передаются в функцию через Ref Cursor. Это особенность параллельных функций Oracle; они будут распределять строки одного Ref Cursor-а между параллельными потоками, при этом каждый поток будет работать с другим подмножеством входного набора данных.
Выполним запрос с использованием табличной функции.
Выполнять таким образом обновление через запрос - мягко говоря, не очень хорошо, но это ближайший эквивалент в PL/SQL того, что применяется в ETL-инструментах (third-party ETL Tool типа DataStage). И чтобы избежать защелок в функции нужно указать AUTONOMOUS TRANSACTION.
TL;DR
MERGE выиграл. Но сбрасывать со счетов 8 метод тоже не стоит.
И еще несколько выводов:
Переключение контекста влияет на производительность.
MERGE эффективнее всего работает с Hash Join.
Расспараллеливать большие объединения нужно.
Внешние ключи не будут особенно влиять на производительность.
Если обновляем таблицу с Bitmap-индексами - лучше всего вставьте транзакции в глобальную временную таблицу и примените MERGE (примера в статье нет, на практике почти всегда будут B-Tree, но если столкнетесь - примите к сведению).
И небольшой манифест с учетом опыта в 12с:
- хватит использовать явные курсоры,
- если можно, не вызывайте PL/SQL функции в SQL,
- если можно от чего-то отказаться в пользу SQL - do it!
- не писать своего, если это есть в пакетах Oracle (в частности dbms..), не плодить сущности в БД, пользоваться уже написанным, особенно на промышленных БД,
- отключайте индексы, если сканируете по ROWID и видите скан других индексов в плане.
Спасибо за внимание! Надеюсь, пригодится.
Источники:
0 Комментарии