Технология Клиент-Сервер 2008'2 |
|||||||
|
К( настоящему моменту многие уже наслышаны о том, как связываемые переменные помогают в работе с SQL. Для тех, кто до сих пор о них не знает, я попробую просто разъяснить основные концепции (кроме того, рекомендую посетить сайт Тома Кайта (Tom Kyte) asktom.oracle.com, где можно убедиться, как важно использовать связываемые переменные, чтобы улучшить выполнение SQL-выражений. Там же можно узнать, как работать со связываемыми переменными на некоторых других языках.).
Допустим, у вас есть таблица под названием CUSTOMERS, в которой, помимо прочих, имеется колонка STATE_CODE, где сохраняется место жительства каждого покупателя. Как принято в США, название местности сокращено в двухбуквенные аббревиатуры: CT (Коннектикут), NY (Нью-Йорк) и т.д. Если вам, к примеру, нужно узнать, сколько покупателей совершили более трех покупок и кто из них проживает в штате Коннектикут (CT), вы, вероятнее всего, напишете следующее:
select count(1) from customers where state_code = 'CT' and times_purchased > 3; |
Получив этот запрос, Oracle должен вначале произвести парсинг (разбор полученной строки), в результате чего сгенерируется план выполнения для SQL-выражения, которое вы только что передали. После разбора выражения оно готово к выполнению. Концептуально парсинг похож на компиляцию кода при разработке программного обеспечения. К примеру, когда вы пишете что-то на С++, у вас не выйдет запустить это прямо сразу в операционной системе – сначала придется скомпилировать код и сделать его исполняемым. Выполнение парсинга создает исполняемый код из SQL-выражения.
Теперь допустим, что другой пользователь передает такое выражение:
select count(1) from customers where state_code = 'NY' and times_purchased > 3; |
Оно почти идентично запросу, рассмотренному выше, но код штата (state_code), поиск которого здесь выполняется, – не Коннектикут (CT), а Нью-Йорк (NY). В идеальном варианте разобранный код должен бы быть одинаковым в обоих случаях, а значение букв должно подставляться во время выполнения. Но запросы написаны таким образом, что Oracle интерпретирует их как разные и поэтому разбирает второй запрос заново.
Рассмотрим, что было бы, если бы запрос записали так:
select count(1) from customers where state_code = <StateCode> and times_purchased > 3; |
Первый запрос передал бы NY как значение параметра <StateCode>, а второй запрос – CT как значение этого же параметра, поэтому запрос не пришлось бы разбирать повторно.
В данном примере <StateCode> является «связываемой переменной», то есть местом вставки значений, которые будут подменяться во время выполнения запроса. Связываемые переменные записываются как VariableName (см. ниже):
where state_code = :state_code
|
Если в вашем коде нет связываемых переменных, и он вместо этого содержит кучу ссылок на строковые значения вроде where state_code = 'CT', можно принудительно заменить все строковые значения на связываемые переменные. Для этого необходимо установить параметр инициализации:
cursor_sharing = force |
С помощью этого параметра выражение «where state_code = 'CT'» будет заменяться на where state_code = ":SYS_0001", где SYS_0001 – переменная, сгенерированная системой Oracle. Такой подход сделает вышеприведенные SQL-выражения идентичными.
Если связываемые переменные так хороши, почему бы не использовать их постоянно? Разве cursor_sharing – не волшебное средство, трансформирующее весь плохой код в код с повторно используемыми выражениями? (Те, кто уже знает ответы на эти вопросы и разбирается в концепции считывания значений связываемых переменных (bind-peeking), могут сразу перейти к разделу «Адаптивные курсоры»).
Представьте, что колонка STATE_CODE является индексированной. Ниже приведено количество значений в ней:
select state_code, count(1) from customers group by state_code; ST COUNT(1) -- ---------- NY 994901 CT 5099 |
Как видите, данные крайне ассиметричны: около 5% строк содержат 'CT', тогда как все остальные – 'NY'. Зная разницу в населенности этих штатов, удивляться не приходится. Теперь посмотрим, какой тип плана выполнения сгенерирован для запроса, рассмотренного выше.
SQL> set autot traceonly explain SQL> select * from customers where state_code = 'NY' and times_purchased > 3 2 / Execution Plan ---------------------------------------------------------- Plan hash value: 2008213504 ------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 895K| 26M| 1532 (9)| 00:00:19 | |* 1 | TABLE ACCESS FULL| CUSTOMERS | 895K| 26M| 1532 (9)| 00:00:19 | ------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter("TIMES_PURCHASED">3 AND "STATE_CODE"='NY') |
Запрос использовал полный просмотр таблицы. Это вполне подходящее действие, поскольку искомое в результате оказалось в 95% строк; индексное сканирование здесь было бы накладно. Теперь рассмотрим такой же запрос со значением ‘CT’.
SQL> c/NY/CT 1* select * from customers where state_code = 'CT' and times_purchased > 3 SQL> / Execution Plan ---------------------------------------------------------- Plan hash value: 4876992 ------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost(%CPU)| Time ------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 4589 | 138K| 56 (2)| 00:00:01 |* 1 | TABLE ACCESS | CUSTOMERS | 4589 | 138K| 56 (2)| 00:00:01 | BY INDEX ROWID | | | | | |* 2 | INDEX RANGE | IN_CUST_STATE | 5099 | | 12 (0)| 00:00:01 | SCAN | | | | | ------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter("TIMES_PURCHASED">3) 2 - access("STATE_CODE"='CT') |
Запрос использовал индексирование колонки, что вполне тут подходит. Ведь СТ занимает только 5% от общего числа строк. Так что индексное сканирование здесь очень кстати.
Посмотрим теперь, как ведет себя Oracle, если используется какая-нибудь связываемая переменная; в данном случае – в Oracle Database 10g:
SQL> var state_code varchar2(2) SQL> exec :state_code := 'CT' PL/SQL procedure successfully completed. SQL> select max(times_purchased) from customers where state_code = :state_code 2 / Execution Plan ---------------------------------------------------------- Plan hash value: 296924608 ------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 6 | 1511 (8)| 00:00:19 | 1 | SORT AGGREGATE | | 1 | 6 | | |* 2 | TABLE ACCESS FULL| CUSTOMERS | 500K| 2929K| 1511 (8)| 00:00:19 ------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter("STATE_CODE"=:STATE_CODE) |
Для работы с таблицей CUSTOMERS оптимизатор выбрал полный просмотр таблицы. Но ведь мы искали только ‘СТ’, который занимает всего 5% от общего количества записей. Разве не должен был оптимизатор предпочесть индексирование? Что заставило его задействовать полный просмотр таблицы, а не индексное сканирование?
Ответ – феномен, называемый bind peeking. Вначале, когда мы запустили запрос со связываемой переменной, значение которой равнялось ‘NY’, оптимизатору пришлось произвести длительный разбор. В процессе парсинга он обратился к нашей связываемой переменной, чтобы узнать, какое у нее значение. Значение это было 'NY, и ему соответствовало почти 95% строк, в связи с чем, как и ожидалось, оптимизатор выбрал полный просмотр таблицы. В качестве дополнительного действия оптимизатор «заморозил» план выполнения для нашего запроса. Затем, когда мы запустили такой же запрос, но со значением ‘CT’, оптимизатор использовал этот же план, без пересчитывания, хотя он и не являлся оптимальным способом получения результата. Используй мы в запросе значение типа ‘CT’ (литерную переменную) вместо связываемой переменной, оптимизатор был бы в состоянии подобрать более подходящий план выполнения.
Таким образом, хотя использование связываемых переменных в большинстве случаев довольно удобно, эти переменные неэффективны там, где селективность значений существенно влияет на планы. Именно это произошло в нашем примере: селективность значений 'CT' и 'NY равнялась соответственно 5 и 95% строк. В случаях, когда распределение данных таково, что селективность почти одинакова для всех значений, план выполнения так же останется одинаковым. Поэтому программисты, опытные в SQL, знают, когда можно нарушить основное правило использования связываемых переменных и применить вместо них прямые строковые константы.
Но что же делать, если у вас нет опытных программистов или не хватает времени на переписывание этих выражений? Есть ли в Oracle какие-нибудь толковые альтернативы?
Да, есть. В Oracle Database 11g у курсоров появилась сообразительность. Вместо того, чтобы каждый раз при выполнении запроса тупо использовать закешированный план выполнения, они решают, быть ли плану пересчитанным, когда изменяется значение связываемой переменной. Если в курсоре присутствует связываемая переменная, то база данных некоторое время за ней наблюдает, чтобы определить, какие значения передаются в эту переменную, и нуждается ли план выполнения в пересчете. В случае, когда пересчет плана необходим, курсор маркируется как «чувствительный к связыванию» (Bind-Sensitive).
Пример запроса, который мы тут рассмотрели, в этом смысле идеален. Корректный план оптимизатора будет зависеть от значения связываемой переменной. Вам ничего не нужно делать – все произойдет автоматически.
Представление V$SQL модифицировано добавлением двух колонок: IS_BIND_SENSITIVE и IS_BIND_AWARE. Посмотрим теперь, как они используются.
select is_bind_sensitive, is_bind_aware, sql_id, child_number from v$sql where sql_text = 'select count(1) from customers where state_code = :state_code and times_purchased > 3' I I SQL_ID CHILD_NUMBER - - ------------- ------------ Y Y 7cv5271zx2ttg 0 Y N 7cv5271zx2ttg 1 |
Вначале узнаем, что эти колонки означают. Oracle некоторое время наблюдает за курсорами и видит, как различаются значения. Если разные значения потенциально могут изменить план выполнения, курсор помечается как Bind-Sensitive, и колонка IS_BIND_SENSITIVE показывает "Y". После нескольких выполнений база данных знает о курсорах и значениях уже больше, и решает, заставить ли курсор изменить основанный на этих значениях план. В случае положительного решения курсор маркируется как Bind-Aware, и колонка IS_BIND_AWARE показывает "Y". Вывод: Bind-Sensitive курсоры лишь потенциально могут изменять планы выполнения, в то время как Bind-Aware курсоры делают это в обязательном порядке.
Новое представление V$SQL_CS_HISTOGRAM показывает, сколько раз было выполнено SQL-выражение. Оно разделено на три столбца: по одному на каждый курсор-потомок (см. ниже).
select * from v$sql_cs_histogram where sql_id = '7cv5271zx2ttg' / ADDRESS HASH_VALUE SQL_ID CHILD_NUMBER BUCKET_ID COUNT -------- ---------- ------------- ------------ ---------- ---------- 45C8218C 2144429871 7cv5271zx2ttg 5 0 0 45C8218C 2144429871 7cv5271zx2ttg 5 1 2 45C8218C 2144429871 7cv5271zx2ttg 5 2 0 45C8218C 2144429871 7cv5271zx2ttg 4 0 8 ... and so on ... 45C8218C 2144429871 7cv5271zx2ttg 0 2 0 |
Поскольку механизм совместного использования адаптивных курсоров применяет корректный план, основанный на значении связываемой переменной, база данных должна где-то хранить информацию об этом. Она выдает эту информацию через другое новое представление V$SQL_CS_SELECTIVITY, отображающее селективность разных значений, которые переданы в связываемую переменную.
select * from v$sql_cs_selectivity where sql_id = '7cv5271zx2ttg' / ADDRESS HASH_VALUE SQL_ID CHILD_NUMBE PREDICATE R LOW HIGH -------- ---------- ------------- ----------- ----------- - -------- ---------- 45C8218C 2144429871 7cv5271zx2ttg 5 =STATE_CODE 0 0.895410 1.094391 45C8218C 2144429871 7cv5271zx2ttg 4 =STATE_CODE 0 0.004589 0.005609 45C8218C 2144429871 7cv5271zx2ttg 4 =STATE_CODE 1 0.002295 0.002804 45C8218C 2144429871 7cv5271zx2ttg 3 =STATE_CODE 0 0.002295 0.002804 45C8218C 2144429871 7cv5271zx2ttg 0 =STATE_CODE 0 0.004589 0.005609 |
Это представление предоставляет массу информации. Колонка PREDICATE содержит различные предикаты (условия WHERE), которые применялись пользователем. Значения LOW и HIGH показывают диапазон величин для переданных значений.
И, наконец, третье новое представление, V$SQL_CS_STATISTICS, показывает активность курсоров, которые маркированы как Bind-Aware или Bind-Sensitive.
select child_number, bind_set_hash_value, peeked, executions, rows_processed, buffer_gets, cpu_time from v$sql_cs_statistics where sql_id = '7cv5271zx2ttg'; CHILD_NUM BIND_SET_HASH_VAL P EXECUTIONS ROWS_PROCESSED BUFFER_GETS CPU_TIME ------------ ------------------- - ---------- ---------------------- ---------- 1 22981142 Y 1 9592 3219 0 0 22981142 Y 1 9592 3281 0 |
Это представление отображает статистику выполнения, как она записана базой данных. Колонка EXECUTIONS показывает, сколько раз выполнялся запрос с разными значениями в связываемой переменной. В колонке PEEKED (выглядит как «P») выведено, обращался ли оптимизатор к значению связываемой переменной, чтобы подобрать оптимальный план выполнения.
Все эти представления дают дополнительную информацию, не являющуюся необходимой для понимания принципов работы данной функциональности. Адаптивные курсоры активируются и используются автоматически.
Сколько раз вы наблюдали такую ситуацию: запрос обладает идеальным планом, но его обязательно что-нибудь нарушит? Этим «чем-нибудь» может являться кто-то, проанализировавший таблицу заново, или же изменение существенного параметра (например, star-transformer), оказавшее влияние на оптимизатор, - список возможностей бесконечен. Отчаявшись, вы можете запретить любые изменения в базе данных: никаких сборов статистики, никаких изменений в параметрах, и т.п.
Но это легче сказать, чем сделать. Что происходит, когда изменяется паттерн данных? Возьмем, к примеру, случай, рассмотренный в разделе «Адаптивные курсоры». Таблица CUSTOMERS теперь заполнена покупателями из Нью-Йорка, так что the STATE_CODE, в основном, "NY". И когда выполняетя запрос с предикатом
where state_code = 'NY' |
индексного сканирования не происходит. Вместо этого система выполняет полный просмотр таблицы. Если же предикат такой:
where state_code = 'CT' |
то индексирование используется, потому что возвращено будет всего несколько строк. Но что же случится, если изменится паттерн? Скажем, покупателей из Коннектикута (state_code = 'CT') окажется так много, что количество CT подскочит до 70%? В этом случае запрос для CT должен использовать полный просмотр таблицы. Но поскольку сбор статистики для оптимизатора вами запрещен, оптимизатор не узнает об изменении в паттерне и продолжит сканировать с использованием индекса, а это тут малопродуктивно. Что же можно сделать?
А что, если бы Oracle использовал оптимальный план, но пересмотрел бы его, как только обуславливающие факторы (например, сбор статистики или параметры базы данных) изменились, и применил бы новый план – но только – и только! – в том случае, если новый план был бы лучше? Это было бы здорово, правда? Так вот, в Oracle Databse 11g такое возможно. Давайте посмотрим, как.
Когда ранее определенный оптимизатором план нуждается в обновлении из-за каких-нибудь изменений в основных факторах, это обновление не происходит в Oracle Database 11g немедленно. Вначале Oracle лишь оценивает новый план, а задействует его только в том случае, если он улучшает план, который уже применяется. Вдобавок существуют утилиты и интерфейсы, с помощью которых можно посмотреть историю планирования для каждого запроса, а также узнать, как эти планы соотносятся друг с другом.
Жизненный цикл начинается с того, что Oracle определяет выражение, которое выполнено более одного раза, т.е. является «повторяемым». Как только повторяемое выражение выявлено, его план запоминается и сохраняется в базе данных как SQL Plan Baseline, в логической конструкции, именуемой SQL Management Base (SMB). SMB сохраняет каждый план для запроса: как он был сгенерирован, и т.д.
Планы, однако, не сохраняются в SMB автоматически. Если бы это было так, то в SMB хранились бы планы каждого типа запросов, и она стала бы необъятной. Напротив, вы можете и должны контролировать, сколько запросов попадет в SMB. Существует два способа это сделать: включить автоматическое сохранение в SMB всех повторяемых запросов или же вручную загружать запросы, которые должны включаться в SMB.
Давайте сначала рассмотрим простой случай. Вы можете сделать так, чтобы SQL Plan Management автоматически записывал бы планы выполнения (SQL Plan Baselines) для всех повторяемых запросов. Для этого нужно установить значение TRUE в параметр базы данных optimizer_capture_sql_plan_baselines (по умолчанию этот параметр равен FALSE). По счастью, это динамический параметр.
SQL> alter system optimizer_capture_sql_plan_baselines = true; |
После выполнения данного выражения планы выполнения для всех повторяемых выражений сохраняются в SMB в виде SQL Plan Baselines. SQL Plan Baselines сохраняются в представлении, которое называется DBA_SQL_PLAN_BASELINES. Их также можно увидеть в утилите Enterprise Manager. Для этого следует запустить Enterprise Manager и выбрать закладку «Server». На этой странице нужно выбрать «SQL Plan Control» в разделе «Query Optimizer». Откроется главная страница SPM. Далее выбирается закладка «SQL Plan Baseline». Это и есть главный экран SQL Plan Baseline. В левом верхнем углу находятся конфигурационные параметры. Там показано значение TRUE для параметра Capture SQL Plan Baselines (записывать планы SQL автоматически), которое было включено командой ALTER SYSTEM. Под ним указан параметр Use SQL Plan Baselines, установленный равным TRUE (значение по умолчанию). Этот параметр сообщает, что SQL Plan Baselines будут использованы для запроса, когда таковой появится.
Всякий раз, когда для запроса генерируется новый план, старый план сохраняется в истории в SMB. Но это ведь значит, что SMB будет переполнена историями планов. Параметр, контролирующий количество недель, в течение которых планы хранятся в истории, указан в текстовом поле напротив надписи «Хранение планов (недели)» (Plan Retention (Weeks)). Экран показывает, что установлено значение «53 недели». Это значит, что, если SQL Plan Baseline не будет использован в течение 53 недель, он удалится автоматически.
Посередине экрана находится поле для поиска SQL-выражений. Введите искомую строку, нажмите «Go», и вы увидите SQL-выражения, а также ассоциированные с ними планы, как было показано на рисунке выше. Каждый план содержит обширную информацию о своем состоянии. Давайте проанализируем эти состояния.
Такая же информация, и даже больше, находится и в представлении DBA_SQL_PLAN_BASELINES:
SQL> desc DBA_SQL_PLAN_BASELINES Name Null? Type ----------------------------------------- -------- --------------- SIGNATURE NOT NULL NUMBER SQL_HANDLE NOT NULL VARCHAR2(30) SQL_TEXT NOT NULL CLOB PLAN_NAME NOT NULL VARCHAR2(30) CREATOR VARCHAR2(30) ORIGIN VARCHAR2(14) PARSING_SCHEMA_NAME VARCHAR2(30) DESCRIPTION VARCHAR2(500) VERSION VARCHAR2(64) CREATED NOT NULL TIMESTAMP(6) LAST_MODIFIED TIMESTAMP(6) LAST_EXECUTED TIMESTAMP(6) LAST_VERIFIED TIMESTAMP(6) ENABLED VARCHAR2(3) ACCEPTED VARCHAR2(3) FIXED VARCHAR2(3) AUTOPURGE VARCHAR2(3) OPTIMIZER_COST NUMBER MODULE VARCHAR2(48) ACTION VARCHAR2(32) EXECUTIONS NUMBER ELAPSED_TIME NUMBER CPU_TIME NUMBER BUFFER_GETS NUMBER DISK_READS NUMBER DIRECT_WRITES NUMBER ROWS_PROCESSED NUMBER FETCHES NUMBER END_OF_FETCH_COUNT NUMBER |
Нажав на имя плана, можно увидеть детализацию это плана, как на приведенном ниже рисунке.
В детализацию включается план выполнения запроса вкупе с информацией о том, является ли план принятым (accepted), включенным (enabled), фиксированным (fixed), и другими важными сведениями. Еще один значимый атрибут – «Origin» (источник), который здесь показывает значение AUTO-CAPTURE. Это означает, что план был принят системой автоматически, потому что параметр optimizer_capture_sql_plan_baselines установлен равным TRUE.
Чтобы вернуться к списку планов, который был изображен на предыдущей иллюстрации, выберите «Возврат» (Return). Теперь выберите план, статус которого не «принят», и нажмите на «Evolve» (Раскрыть), чтобы увидеть, следует ли рассматривать данный план как кандидата в потенциально лучшие планы.
Важной деталью на появляющемся в результате этого экране является радиокнопка «Verify Performance» (Проверка производительности). Ее выбирают, если нужно проверить разные планы и сравнить их производительность с планом выполнения SQL, который используется для конкретного запроса. Нажмите OK, и вы увидите отчет о сравнении:
------------------------------------------------------------------------------- Evolve SQL Plan Baseline Report ------------------------------------------------------------------------------- Inputs: ------- PLAN_LIST = SYS_SQL_PLAN_b5429522ee05ab0e SYS_SQL_PLAN_b5429522e53beeec TIME_LIMIT = DBMS_SPM.AUTO_LIMIT VERIFY = YES COMMIT = YES Plan: SYS_SQL_PLAN_b5429522e53beeec ----------------------------------- It is already an accepted plan. Plan: SYS_SQL_PLAN_b5429522ee05ab0e ----------------------------------- Plan was verified: Time used 3.9 seconds. Failed performance criterion: Compound improvement ratio <= 1.4. Baseline Plan Test Plan Improv. Ratio ------------- --------- ------------- Execution Status: COMPLETE COMPLETE Rows Processed: 1 1 Elapsed Time(ms): 3396 440 7.72 CPU Time(ms): 1990 408 4.88 Buffer Gets: 7048 5140 1.37 Disk Reads: 4732 53 89.28 Direct Writes: 0 0 Fetches: 4732 25 189.28 Executions: 1 1 |
Это вполне показательный отчет, демонстрирующий соотношение планов между собой. Если видно, что какой-нибудь из них выполняется лучше, оптимизатор использует именно его. Если же новый план не дает заметного улучшения при выполнении запросов, его не следует принимать и использовать. Управление планом выполнения SQL (SQL Performance Management) позволяет видеть, как сравниваются планы, и задействовать те из них, что дают действительно лучший результат.
Существует возможность изменить статус «принят» плана вручную. Для этого следует выполнить пакет DBMS_SPM:
declare ctr binary_integer; begin ctr := dbms_spm.alter_sql_plan_baseline ( sql_handle => 'SYS_SQL_e0b19f65b5429522', plan_name => 'SYS_SQL_PLAN_b5429522ee05ab0e', attribute_name => 'ACCEPTED', attribute_value => 'NO' ); end; |
Можно отключить SQL Plan Baseline так, чтобы он не использовался оптимизатором. Можно так же включить его заново, и оптимизатор опять будет его использовать. Выключается план следующим образом:
declare ctr binary_integer; begin ctr := dbms_spm.alter_sql_plan_baseline ( sql_handle => 'SYS_SQL_e0b19f65b5429522', plan_name => 'SYS_SQL_PLAN_b5429522ee05ab0e', attribute_name => 'ENABLED', attribute_value => 'NO' ); end; |
Когда план для какого-нибудь SQL-выражения является зафиксированным, это будет показано достаточно ясно. В конце плана вы увидите строку, подтверждающую, что данный план зафиксирован.
<...>
Адаптивные курсоры и SQL Plan Management – всего лишь два примера того, как база данных теперь сама разбирается в запросах, и как она на них реагирует. Такое ее поведение позволяет получить лучший результат: в работе с адаптивными курсорами можно использовать связываемые переменные, не теряя при этом в оптимальности плана выполнения, а применение SQL Plan Management дает не намертво фиксированный план выполнения, а предоставляет возможность улучшать его в течение какого-то времени, сохраняя при этом краткосрочную его стабильность.
Copyright © 1994-2016 ООО "К-Пресс"