Продуктивність PL-SQL
(PL/SQL Performance, Arup Nanda, Arup Nanda)
Аруп Нанда Oracle ACE Director
Досліджуємо випадки, коли заміщення коду (in-lining of code), правильно застосовувана вбудована (native) компіляція та використання чисел типу simple integers можуть підвищити продуктивність коду
Oracle Database 11g надає багато нових відмінних можливостей для підвищення продуктивності PL/SQL-коду, з яких найбільш значущими є native compilation (вбудована компіляція) та intra-unit inlining (внутрішньомодульне заміщення виклику).
Native compilation (вбудована компіляція) – це не зовсім нова можливість, проте тепер немає «вузьких» місць її використання, наприклад установка компілятора C – це дійсно нове. (Oracle назвав цю чудову нагоду "Real Native Compilation"). Крім того, новий тип даних simple_integer робить виконання коду краще за вбудованої компіляції . Intra-unit inlining (внутрішньомодульне заміщення виклику) – це техніка оптимізації, яка застосовується до PL/SQL-коду під час компіляції для створення ефективного коду.
У цій статті будуть розглянуті деякі випадки використання нових можливостей. Буде також перевірено їхню продуктивність при різних сценаріях: коли використовується вбудована (natively) компіляція, коли використовуються числа типу simple integer, коли використовується заміщення (inlining) та їх різні комбінації.
Реальна вбудована компіляція (Real Native Compilation)
Згадайте вбудовану компіляцію у Oracle9i Database Release 2; вона робить виконання PL/SQL-програм набагато швидше порівняно з формами, що інтерпретуються. З іншого боку, освоєння було повільним, оскільки багато системних адміністраторів чинили опір установці необхідного компілятора Cна виробничому сервері бази даних. Крім того, такі компілятори вимагають встановлення параметра plsql_native_library_dir із директорією для проміжних файлів OS.
У Oracle Database 11g можна виконувати native-compile без компілятора C на сервері та без встановлення параметра. Все, що необхідно зробити, це встановити параметр сесії перед створенням або перекомпіляцією коду, що зберігається:
Вбудована (natively) компіляція виконується довше, ніж інтерпретаційна (interpreted), але оскільки цей процес у Oracle Database 11g відбувається набагато швидше, то різниця може бути непомітною. Найкраще застосовувати інтерпретаційну компіляцію під час звичайного циклу розробки, а вбудовану компіляцію, коли розробка завершена.
Як частина процедури міграції на 11g, я виконав експеримент з реальним кодом життєво важливої програми з дуже великим пакетом з 5827 рядків. Я вбудовано скомпілював його на існуючій базі даних 10g і зробив те саме на 11g, а потім повторив ці дії в режимі компіляції-інтерпретації. Кожна з цих компіляцій була зроблена з параметром plsql_optimize_level, що дорівнює 2. Я виміряв час компіляції для кожного випадку, він показано нижче (у секундах).
10g
11g
Interpreted
Native
Результати кажуть самі за себе. У режимі інтерпретації час компіляції майже однаковий. Однак при вбудованій компіляції час компіляції в 11g менший приблизно на 60% порівняно з 10g, а це суттєве поліпшення. Отже, хоча вбудована компіляція 11g вимагає додаткового часу, вона набагато швидше, ніж вбудована компіляція 10g.
Щоб знайти об'єкти, скомпільовані за допомогою NATIVE, потрібно звернутися до USER_PLSQL_OBJECT_SETTINGS:
Є й схоже уявлення всім об'єктів, DBA_PLSQL_OBJECT_SETTINGS.
Новий тип даних: Simple Integer
Переваги інтегрованої компіляції ще більш очевидні при використанні нового типу даних, simple_integer. Правду кажучи, це несправжній тип даних, а скоріше підтип типу даних pls_integer. Цей підтип створений для покращення машинних обчислень у порівнянні з програмними обчисленнями. Коли simple_integer використовується одночасно з вбудованою компіляцією, продуктивність стає набагато вищою. В експерименті, який буде показано далі, ви побачите, чому це так.
Оскільки simple_integer є підтипом pls_integer, він успадковує його властивості як 32-бітного цілого числа зі знаком і може бути цілим числом від негативного -2,147,483,648 до позитивного 2,147,483,647 значень. Однак він відрізняється від pls_integer наступним: цей тип не допускає значення null, але допускає переповнення, тобто коли значення перевищує максимум, воно скидається, але помилка не з'являється.
Синтаксично цей тип даних можна використовувати у всіх тих випадках, коли використовується pls_integer, але необхідно уважно стежити за різницею; додаткові властивості simple_integer у деяких випадках можуть виявитися недоречними.
Давайте наведемо кілька можливих завдань, де слід замінити pls_integer на simple_integer:
- Ця змінна не може бути null, тому якщо написати не так, як показано нижче:
- а просто:
- то буде отримано помилку компіляції:
- Якщо встановити значення змінної в NULL всередині програми, наприклад, так:
- буде отримано помилку компіляції:
- Уникайте цих повідомлень про помилки, які можуть бути не видно з точки зору точної сутіпомилки. Якщо програма очікує на встановлення змінної в null, то не слід описувати змінну, як simple_integer.
- Інший важливий момент у застосуванні simple_integer полягає в тому, що використовується скидання значень при досягненні максимальної та мінімальної меж. Пам'ятайте, максимальне позитивне значення pls_integer, це 2147483647. Що станеться, якщо спробувати зберегти значення, яке більше? Подивіться демонстраційний приклад:
- Буде отримано помилку:
- Помилка очевидна і цілком зрозуміла; ви спробували перевищити максимальне значення, допустиме для даних. Якщо замість pls_integer використовувати simple_integer:
- Результат буде наступним:
- Зауважте, що значення (-2147483648), це мінімальне значення simple_integer. Коли ви додаєте 1 до максимального значення (2147483647), значення просто скидається до мінімального це особливість simple_integer. Стережіться подібних ситуацій.
Використання Real Native Compilation з Simple Integers
Як бачите, simple_integer не може використовуватись у будь-якому місці; необхідно бути уважним з урахуванням додаткових умов (особливо можливе скидання значень) перед застосуванням. Тому simple_integer створений для вбудованої компіляції. У режимі компиляції, що інтерпретується, ефекту підвищення продуктивності може не бути (але і шкоди теж немає, як буде видно далі). У режимі native compilation продуктивність simple_integer набагато суттєвіша.
Більшість бізнес-додатків на PL/SQL жорстко пов'язані з SQL. Тому ці програми не побачать значної зміни продуктивності при вбудованій компіляції. У минулому житті я розробляв інструмент для планування можливостей бази даних з використанням PL/SQL,що включає багато числових та статистичних обчислень у кілька тисяч рядків коду. Застосування вбудованої компіляції призводить до значного підвищення продуктивності. У той час не були доступні числа типу simple_integer, але якби вони були, це ще більше позначилося б на продуктивності.
Intra-unit Inlining
Intra-unit inlining є підміною виклику підпрограми на копію коду цієї підпрограми. В результаті модифікований код виконується швидше. У Oracle Database 11g компілятор PL/SQL здатний ідентифікувати виклики підпрограми, яку необхідно скопіювати (іншими словами, підмінити на неї) та робить зміни, що покращують продуктивність.
Найкраще пояснити це на прикладі. Код, показаний нижче, змінює таблицю BALANCES, обчисливши значення виходячи з балансу рахунку. Код проходить по всіх записах таблиці, обчислює результат і змінює стовпець таблиці з балансом.
Фактично, обчислення результату є однаковим для всіх типів записів, і я помістив логіку в окрему процедуру calc_int() всередині основної процедури. Це підвищує читання та супроводжуваність коду, але, на жаль, це неефективно.
Однак, якщо замінити виклик calc_int() на код calc_int(), вийде швидше програма, як показано нижче:
Цей перероблений код відрізняється від вихідного тільки в частині коду для обчислення балансу, який тепер всередині циклу, а не процедури calc_int().
Зауважте, що нова версія може бути швидше, але це не дуже добрий приклад практики кодування. Частина коду, що виконує обчислення балансу, виконується один раз для кожної ітерації циклу для місяців, а потім для кожного номера рахунку. Так як ця частина коду повторюється, вона зручніша для розміщення окремо, як показано впопередньої версії upd_int у процедурі (calc_int). Цей підхід робить код модульним, легким у підтримці і реально читаним, але також менш ефективним.
Тому як можна досягти примирення конфліктуючих способів створення коду, зробивши код модульним і водночас швидким? Так, а чи можна написати код, використовуючи модульний підхід (як у першій версії upd_int), а потім дозволити компілятору PL/SQL оптимізувати його, щоб він став виглядати, як у другій версії коду?
Це можна зробити у Oracle Database 11g. Все, що потрібно зробити, - перекомпілювати процедуру з вищим рівнем оптимізації PL/SQL. Цього можна досягти двома способами:
-
Встановити параметр рівня сесії та перекомпілювати процедуру: Команда, наведена вище, інструктує компілятор PL/SQL, щоб він переписав код у вбудований код.
Скомпілювати процедуру безпосередньо з plsql-установкою.
На будь-яку іншу процедуру, що компілюється у цій же сесії, це не вплине. Цей метод краще застосовувати для обробки inline, якщо є багато процедур, які необхідно скомпілювати в одній сесії.
Можна використовувати секцію pragma, яка є директивою компілятора.
Я додав рядок pragma inline (calc_int, 'YES'); для вказівки компілятор підмінити в коді цю процедуру. Так само можна вказати "NO" в цьому ж місці, щоб передати компілятору, що не треба підміняти цю процедуру, навіть якщо plsql_optimizer_level встановлений у значення 3.
Inlining робить виконання коду швидше. Точний ступінь покращення залежатиме, звичайно ж, від кількості підмін, які будуть зроблені компілятором. Наприкінці цієї статті ми розглянемо приклад з використанням inlining та побачимо покращення продуктивності в результаті йогозастосування.
Час компіляції
Звичайно, цей процес оптимізації ускладнює роботу компілятора. Але наскільки?
Щоб відповісти на це питання, я взяв код реального додатка, показаний раніше, та скомпілював його у різних комбінаціях з inlining/без inlining та interpreted/native. Ось час компіляції: