Oracle Database 12c - Adaptive SQL execution plans

THE HOLY BIBLE - King James Version - БИБЛИЯ в Синодальном переводе
"Нас Атакуют!" Изобличи козни лукавого, запрети диаволу

Изменяемые 'адаптивные' планы выполнения SQL запросов в Оракл 12c

Новая версия Oracle database 12c предлагает разработчикам значительно улучшенный "умный" SQL оптимизатор. Множество доработок и новых возможностей позволяют правильно использовать статистику, оценивать действительное число возвращаемых строк и учитывать фактическое распределение данных в столбцах.

При этом все предыдущие версии SQL оптимизатора, основанного на относительной стоимости операций, периодически ошибались, позволяя первому запросу "пойти в разнос" за счёт выбора неудачного плана. Последующие выполнения того же SQL могли пройти более успешно если оптимизатор мог учесть допущенные ошибки. Но первое выполнение всегда могло быть отработано неоптимальным образом, что было особенно ощутимо в системах хранилищ данных, где зачастую каждый запрос уникален и выполняется однократно.

В предлагаемой заметке я привожу пример использования одной из важных принципиально новых возможностей Оракла 12c изменять SQL план первого запроса "на лету" непосредственно во время его исполнения, приводя к значительному сокращению времени обработки данных. Так называемые "адаптивные" планы могут буквально спасти ситуацию, позволяя правильно и эффективно выполнить даже уникальный неповторяющийся SQL.

Прежде чем мы продолжим, я хотел бы привести строки из Евангелия:



......................... == Книга пророка Исаии == ............................
=== Глава 12, Стих 1 ===
1 И скажешь  в  тот  день:  славлю  Тебя,  Господи;  Ты  гневался  на  меня,  но
  отвратил гнев Твой и утешил меня.
2 Вот, Бог - спасение мое: уповаю на Него и не боюсь; ибо Господь  -  сила  моя,
  и пение мое - Господь; и Он был мне во спасение.
3 И   в   радости   будете   почерпать   воду    из    источников      спасения,
4 и скажете в тот день:  славьте  Господа,  призывайте  имя  Его;  возвещайте 
  в  народах дела Его; напоминайте, что велико имя Его;
5 пойте Господу, ибо  Он  соделал  великое,  -  да  знают  это  по  всей  земле.

Лично для вас благая весть - Единородный Сын Божий Иисус Христос любит вас, Он взошёл на крест за ваши грехи, был распят и на третий день воскрес, сел одесную Бога и открыл нам дорогу в Царствие Небесное.

Мы все живём в те самые последние времена, когда брат идёт войной на брата, сердца людские ожесточились и каждый обороняет своё, не внимая голосу рассудка и зову сердца. Сатана запутал нас в бесконечном лабиринте жадности и мирских похотей - и, казалось бы, нет выхода из этого тупика.

Но ведь каждому из нас открыта дорога к спасению от этих окружающих нас повседневных бед - уповай на Господа, прославляй имя Его и ищи в Нём силы для борьбы со злом. Такое вот простое и незатейливое, казалось бы, решение. И тем не менее, уже много тысяч лет Господь спасает взывающих к Нему. И сегодня, на очередном витке нашего с вами безумия именуемого "историей", нам как никогда нужно Божие прощение и милость. Ибо наши страшные душевные раны могут быть омыты только той самой водой из источников спасения - остальным же уготована жажда на пустынной дороге самолюбия и зла, ведущей прямо в ад.

Задумайтесь над этими словами, серьёзно задумайтесь - выбор между спасительной верой и осуждающим для ада неверием неизбежен, и вполне возможно, дорогой брат, что именно сейчас решается ваша судьба - и не на год или десятилетие, а навсегда.

Покайтесь, примите Иисуса как вашего Спасителя, ибо наступают последние времена и время близко - стоит Судья у ворот.

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

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


Вернёмся к нашим техническим деталям.

В заметке рассматриваются несколько видов запросов, приводящих к генерации изменяемого плана:

  • "внутренние" служебные запросы самой БД (и как их распознать)
  • простой пример с двумя таблицами пользователя
  • пример, использующий схему "OE", предлагаемый самим Ораклом
  • Прочитать подробно об изменяемых планах ("adaptive plan") можно в документации Оракла 12 - смотрите "Concept" и "SQL Tuning Guide".

    Я же скажу вкратце так - при первом "разборе" SQL выражения оптимизатор начинает строить план его выполнения, опираясь на имеющиеся данные о количестве записей и числе уникальных значений в каждой колонке таблиц, участвующих в запросе. При определённых обстоятельствах оптимизатор замечает "неясность" в эффективности способа "сведения" таблиц - и вот в этот момент в план вставляется "ветвление" - кусок кода, выполняющийся позже, уже вместе с самим запросом. На данный момент код "ветвления" может сгенерировать только "Hash Join" или "Nested Loops", но я уверен - вскоре появятся новые варианты.

    Непосредственно во время первого выполнения запроса при достижении "ветвления" процесс, исполняющий запрос по динамическому плану, оценивает количество уже обработанных фактически строк и сравнивает с числом строк, "предсказанных" планом. Если число отличается не сильно - ветвление просто игнорируется (и фактически выбрасывается, так что последующие выполнения SQL по этому адаптивному плану уже не будут изменяться - т.е. план превращается из адаптивного в обычный "фиксированный") и запрос "сводит" таблицы способом, предложенным в плане изначально.

    Если же разница в числе строк велика - код "ветвления" выполняется и генерирует другой способ сведения таблиц - например, если изначально оптимизатор предсказал всего 20 строк в каждой таблице и решил использовать "Nested Loops", то теперь, когда вместо 20 строк мы уже обработали 2 тысячи, становится ясным что NL будет работать долго и нудно - и это же самое первое выполнение запроса "на лету" переходит на "Hash Join". И таким же образом, код ветвления удаляется из плана, превращая изменяемый план в его фиксированный эквивалент, но теперь уже содержащий HJ вместо NL.

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

    "Внутренние" служебные запросы самой БД

    Сам по себе, Оракл является одним из основных пользователей адаптивных планов. Сбор статистики, Automated Workload Repository и прочие "maintenance jobs" извлекают значительную выгоду из этой новой особенности продукта. Распознать запросы, выполненные по адаптивному плану, очень легко используя колонку V$SQL.IS_RESOLVED_ADAPTIVE_PLAN:

    .......... == Второе послание к Тимофею святого апостола Павла == ..............
    === Глава 4, Стих 22 ===
    22 Господь  Иисус  Христос  со  духом   твоим.   Благодать   с   вами.    Аминь.
    
    (b+/b-, c+/c-, +/-, *) > 
    
    [oracle@localhost ~]$ sqlplus
    
    SQL*Plus: Release 12.1.0.1.0 Production
    ...
    SQL> l
      1  select sql_id, IS_REOPTIMIZABLE, IS_RESOLVED_ADAPTIVE_PLAN, child_number
      2  from v$sql
      3  where IS_RESOLVED_ADAPTIVE_PLAN is not null
      4* order by sql_id, child_number
    15:27:36 SQL> /
    
    SQL_ID        I I CHILD_NUMBER
    ------------- - - ------------
    ...
    fuqs31uwjmbkz N N            0
    fuqs31uwjmbkz N Y            1
    ...
    

    Мы видим, что запрос с sql_id "fuqs31uwjmbkz" был выполнен дважды - первый раз это происходило по "начальному" плану, который был "адаптирован" и в конце второго выполнения уже превратился в "фиксированный" план. Колонка IS_RESOLVED_ADAPTIVE_PLAN говорит нам именно о том, что код ветвления присутствовал в начальном плане, был выполнен и удалён.

    Всё же возникает сомнение - а действительно ли план выполнения был адаптивным и вообще, не являются ли две строки выше просто двумя записями обработки одного и того же плана разными сессиями?

    Замена Hash Join на Nested Loops

    Я скажу сразу - изменения в версии 12c, связанные с "адаптивными" запросами и статистикой настолько кардинальны, что во многих случаях ответ просто неясен :-) Методом проб и ошибок, выполнением простых запросов на известных наборах данных получается добраться до реальной картины происходящего - но это требует усилий и времени. Попробуем посмотреть внимательнее:

    ....................... == Книга пророка Аввакума == ...........................
    === Глава 2, Стих 19 ===
    18 Что  за   польза   от   истукана,   сделанного   художником,   этого   литого
       лжеучителя,  хотя  ваятель,  делая   немые   кумиры,   полагается   на   свое
       произведение?
    19 Горе  тому,  кто   говорит   дереву:   "встань!"   и   бессловесному   камню:
       "пробудись!" Научит ли он чему-нибудь? Вот, он обложен  золотом  и  серебром,
       но дыхания в нем нет.
    
    (b+/b-, c+/c-, +/-, *) > 
    
    [oracle@localhost ~]$ sqlplus
    
    SQL*Plus: Release 12.1.0.1.0 Production
    ...
    
    15:45:52 SQL> select IS_REOPTIMIZABLE, IS_RESOLVED_ADAPTIVE_PLAN, child_number,
    15:46:12   2  EXECUTIONS, fetches, END_OF_FETCH_COUNT, LAST_ACTIVE_TIME, PLAN_HASH_VALUE
    15:46:46   3  from v$sql
    15:46:49   4  where sql_id = 'fuqs31uwjmbkz';
    
    I I CHILD_NUMBER EXECUTIONS    FETCHES END_OF_FETCH_COUNT LAST_ACTIVE_TIME  PLAN_HASH_VALUE
    - - ------------ ---------- ---------- ------------------ ----------------- ---------------
    N N            0          0          0                  0 17-03-14 14:00;32      4259802813
    N Y            1          1          2                  1 17-03-14 14:00;32      4259802813
    
    2 rows selected.
    

    Совпадение времени окончания выполнения и хэшей планов показывает нам, что наше предположение оказалось верным - мы говорим об адаптивном плане. Что же именно делал SQL запрос 'fuqs31uwjmbkz' и как менялся "адаптивный" план? Посмотрим (и я заранее извиняюсь за выступающий за поля текст плана):

    ......................... == Книга пророка Ионы == .............................
    === Глава 2, Стих 10 ===
    9 Чтущие   суетных   и   ложных   _богов_   оставили    Милосердаго      своего,
    10 а я гласом  хвалы  принесу  Тебе  жертву;  что  обещал,  исполню:  у  Господа
      спасение!
    
    (b+/b-, c+/c-, +/-, *) >
    
    [oracle@localhost ~]$ sqlplus
    
    SQL*Plus: Release 12.1.0.1.0 Production
    ...
    SQL> l
      1* select * from table(dbms_xplan.display_cursor('fuqs31uwjmbkz', 0, 'ALL +ADAPTIVE +NOTE'))
    15:37:04 SQL> /
    
    PLAN_TABLE_OUTPUT
    ----------------------------------------------------------------------------------------------
    SQL_ID  fuqs31uwjmbkz, child number 0
    -------------------------------------
     SELECT  count(*) as cnt ,         a.SQL_ID, a.CON_DBID,
    a.CURRENT_OBJ#,         sum(case when a.event_id in (:e0, :e1, :e2,
    :e3, :e4, :e5, :e6, :e7) then 1 else 0 end) as full_scan  FROM
    WRH$_ACTIVE_SESSION_HISTORY a     ,  WRH$_EVENT_NAME en  WHERE  a.dbid
    = :dbid  AND a.instance_number = :inst  AND a.snap_id > :bid AND
    a.snap_id <= :eid  AND  a.sql_id in (:sqlid0, :sqlid1, :sqlid2)   AND
    en.dbid = :dbid   AND  en.event_id = a.event_id    AND  en.wait_class
    in ('Cluster', 'User I/O')    AND  a.CURRENT_OBJ# > 0  AND a.sql_id is
    NOT NULL  AND a.session_type = 1  GROUP BY a.SQL_ID, a.CON_DBID,
    a.CURRENT_OBJ#  HAVING count(*) >= :rowcnt ORDER BY count(*) DESC,
    a.CURRENT_OBJ# ASC
    
    Plan hash value: 4259802813
    
    -----------------------------------------------------------------------------------------------------------------------------------
    |   Id  | Operation                         | Name                        | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
    -----------------------------------------------------------------------------------------------------------------------------------
    |     0 | SELECT STATEMENT                  |                             |       |       |     7 (100)|          |       |       |
    |     1 |  SORT ORDER BY                    |                             |     1 |    58 |     7  (29)| 00:00:01 |       |       |
    |  *  2 |   FILTER                          |                             |       |       |            |          |       |       |
    |     3 |    HASH GROUP BY                  |                             |     1 |    58 |     7  (29)| 00:00:01 |       |       |
    |  *  4 |     FILTER                        |                             |       |       |            |          |       |       |
    |- *  5 |      HASH JOIN                    |                             |     1 |    58 |     5   (0)| 00:00:01 |       |       |
    |     6 |       NESTED LOOPS                |                             |     1 |    58 |     5   (0)| 00:00:01 |       |       |
    |     7 |        NESTED LOOPS               |                             |     1 |    58 |     5   (0)| 00:00:01 |       |       |
    |-    8 |         STATISTICS COLLECTOR      |                             |       |       |            |          |       |       |
    |     9 |          PARTITION RANGE ITERATOR |                             |     1 |    37 |     3   (0)| 00:00:01 |   KEY |   KEY |
    |  * 10 |           TABLE ACCESS FULL       | WRH$_ACTIVE_SESSION_HISTORY |     1 |    37 |     3   (0)| 00:00:01 |   KEY |   KEY |
    |  * 11 |         INDEX RANGE SCAN          | WRH$_EVENT_NAME_PK          |     1 |       |     1   (0)| 00:00:01 |       |       |
    |  * 12 |        TABLE ACCESS BY INDEX ROWID| WRH$_EVENT_NAME             |     1 |    21 |     2   (0)| 00:00:01 |       |       |
    |- * 13 |       TABLE ACCESS FULL           | WRH$_EVENT_NAME             |     1 |    21 |     2   (0)| 00:00:01 |       |       |
    -----------------------------------------------------------------------------------------------------------------------------------
    
    ...
    
    Note
    -----
       - this is an adaptive plan (rows marked '-' are inactive)
    
    Elapsed: 00:00:00.06
    15:37:04 SQL> 
    

    План для "child 1" будет таким же самым. Действия в плане, помеченные дефисом, не выполнялись, именно как результат работы кода ветвления (обозначенного на плане "STATISTICS COLLECTOR", который тоже был помечен "-" после первого обращения и не является активным для последующих сессий).

    В этом запросе процесс, выполняющий план с хэшем 4259802813, решил не использовать Hash Join и полное сканирование таблицы WRH$_EVENT_NAME, вместо этого был использован индекс WRH$_EVENT_NAME_PK с последующим Nested Loops Join. Также заметьте наличие Bind Variables - без них план вероятнее всего не был бы адаптивным и колонка v$sql.IS_RESOLVED_ADAPTIVE_PLAN содержала бы NULL.

    Замена Nested Loops на Hash Join

    Теперь посмотрим на обратное действие - похожий запрос от того же модуля AWR, только выполнялся он 4 раза, и все разы использовал адаптивный план, в котором вместо NL использовался HJ и полное сканирование таблицы WRH$_EVENT_NAME. Даты и количество END_OF_FETCH_COUNT показывают нам, что Child 0 был принят к обработке, но не закончил выполнения. Child 1 был полностью выполнен 4 раза.

    .............. == Послание к Галатам святого апостола Павла == .................
    === Глава 6, Стих 18 ===
    18 Благодать  Господа  нашего  Иисуса  Христа  со  духом  вашим,  братия. Аминь.
    
    (b+/b-, c+/c-, +/-, *) >
    
    [oracle@localhost ~]$ sqlplus
    
    SQL*Plus: Release 12.1.0.1.0 Production
    ...
    
    16:04:16 SQL> l
      1  select IS_REOPTIMIZABLE, IS_RESOLVED_ADAPTIVE_PLAN, child_number,
      2  EXECUTIONS, fetches, END_OF_FETCH_COUNT, FIRST_LOAD_TIME,
      3   LAST_ACTIVE_TIME, full_PLAN_HASH_VALUE,
      4  PLAN_HASH_VALUE, hash_value, invalidations
      5  from v$sql
      6* where sql_id = '6cp74g22fzahf'
    16:04:19 SQL> /
    
    I I CHILD_NUMBER EXECUTIONS    FETCHES END_OF_FETCH_COUNT FIRST_LOAD_TIME LAST_ACTIVE_TIME
    - - ------------ ---------- ---------- ------------------ --------------- -----------------
    FULL_PLAN_HASH_VALUE PLAN_HASH_VALUE HASH_VALUE INVALIDATIONS
    -------------------- --------------- ---------- -------------
    N N            0          0          0                  0 2014-03-17/13:0 17-03-14 13:00;30
                                                              0:30
              3348789387      3902652512 2230299150             0
    
    N Y            1          4        529                  4 2014-03-17/13:0 17-03-14 16:00;42
                                                              0:30
              3348789387      3902652512 2230299150             0
    
    
    2 rows selected.
    
    Elapsed: 00:00:00.00
    16:04:21 SQL> 
    
    PLAN_TABLE_OUTPUT
    -----------------------------------------------------------------------------
    SQL_ID  6cp74g22fzahf, child number 0
    -------------------------------------
     WITH ash as  (SELECT ash.event_id,  sum(decode(ash.session_type, 1, 1,
    0)) as cnt_fg,  count(*) as cnt_total  FROM
    WRH$_ACTIVE_SESSION_HISTORY ash  WHERE  ash.dbid = :dbid  AND
    ash.instance_number = :inst  AND ash.snap_id > :bid AND ash.snap_id <=
    :eid  AND  ash.wait_time = 0  GROUP BY ash.event_id  ),  evts as
    (SELECT event_id, sum(w_cnt) as w_cnt, sum(to_cnt) as to_cnt,
    sum(w_time) as w_time  ,sum(w_cnt_fg) as w_cnt_fg, sum(to_cnt_fg) as
    to_cnt_fg,  sum(w_time_fg) as w_time_fg  FROM (SELECT es.event_id as
    event_id,  es.total_waits - nvl(bs.total_waits,0) as w_cnt,
    es.TOTAL_TIMEOUTS - nvl(bs.TOTAL_TIMEOUTS,0) as to_cnt,
    es.TIME_WAITED_MICRO - nvl(bs.TIME_WAITED_MICRO,0)  as w_time
    ,es.total_waits_fg - nvl(bs.total_waits_fg,0)  as w_cnt_fg,
    es.TOTAL_TIMEOUTS_FG - nvl(bs.TOTAL_TIMEOUTS_FG,0)  as to_cnt_fg,
    es.TIME_WAITED_MICRO_FG - nvl(bs.TIME_WAITED_MICRO_FG,0)  as w_time_fg
    FROM  WRH$_SYSTEM_EVENT es , WRH$_SYSTEM_EVENT bs  WHERE  es.dbid =
    :dbid  AND es.instance_number = :i
    
    Plan hash value: 3902652512
    
    ------------------------------------------------------------------------------------------------------------------------------------------------------------
    |   Id  | Operation                                                  | Name                        | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
    ------------------------------------------------------------------------------------------------------------------------------------------------------------
    |     0 | SELECT STATEMENT                                           |                             |       |       |    21 (100)|          |       |       |
    |     1 |  SORT ORDER BY                                             |                             |    97 | 19982 |    21  (15)| 00:00:01 |       |       |
    |  *  2 |   FILTER                                                   |                             |       |       |            |          |       |       |
    |  *  3 |    HASH JOIN RIGHT OUTER                                   |                             |    97 | 19982 |    20  (10)| 00:00:01 |       |       |
    |     4 |     FIXED TABLE FULL                                       | X$KEHEVTMAP                 |   105 |  1155 |     0   (0)|          |       |       |
    |  *  5 |     HASH JOIN RIGHT OUTER                                  |                             |    97 | 18915 |    20  (10)| 00:00:01 |       |       |
    |     6 |      VIEW                                                  |                             |     2 |    66 |     4  (25)| 00:00:01 |       |       |
    |     7 |       HASH GROUP BY                                        |                             |     2 |    58 |     4  (25)| 00:00:01 |       |       |
    |  *  8 |        FILTER                                              |                             |       |       |            |          |       |       |
    |     9 |         PARTITION RANGE ITERATOR                           |                             |     2 |    58 |     3   (0)| 00:00:01 |   KEY |   KEY |
    |  * 10 |          TABLE ACCESS FULL                                 | WRH$_ACTIVE_SESSION_HISTORY |     2 |    58 |     3   (0)| 00:00:01 |   KEY |   KEY |
    |  * 11 |      HASH JOIN                                             |                             |    97 | 15714 |    16   (7)| 00:00:01 |       |       |
    |  * 12 |       FIXED TABLE FULL                                     | X$KEHECLMAP                 |    12 |   156 |     0   (0)|          |       |       |
    |  * 13 |       HASH JOIN                                            |                             |   105 | 15645 |    16   (7)| 00:00:01 |       |       |
    |-   14 |        NESTED LOOPS                                        |                             |   105 | 15645 |    16   (7)| 00:00:01 |       |       |
    |-   15 |         NESTED LOOPS                                       |                             |       |       |            |          |       |       |
    |-   16 |          STATISTICS COLLECTOR                              |                             |       |       |            |          |       |       |
    |    17 |           VIEW                                             |                             |   105 |  8925 |     9  (12)| 00:00:01 |       |       |
    |    18 |            HASH GROUP BY                                   |                             |   105 |  8925 |     9  (12)| 00:00:01 |       |       |
    |    19 |             VIEW                                           |                             |   270 | 22950 |     8   (0)| 00:00:01 |       |       |
    |    20 |              UNION-ALL                                     |                             |       |       |            |          |       |       |
    |  * 21 |               HASH JOIN OUTER                              |                             |   165 | 14190 |     8   (0)| 00:00:01 |       |       |
    |-   22 |                NESTED LOOPS OUTER                          |                             |   165 | 14190 |     8   (0)| 00:00:01 |       |       |
    |-   23 |                 STATISTICS COLLECTOR                       |                             |       |       |            |          |       |       |
    |    24 |                  PARTITION RANGE SINGLE                    |                             |    90 |  3870 |     4   (0)| 00:00:01 |   KEY |   KEY |
    |    25 |                   TABLE ACCESS BY LOCAL INDEX ROWID BATCHED| WRH$_SYSTEM_EVENT           |    90 |  3870 |     4   (0)| 00:00:01 |   KEY |   KEY |
    |  * 26 |                    INDEX RANGE SCAN                        | WRH$_SYSTEM_EVENT_PK        |    46 |       |     2   (0)| 00:00:01 |   KEY |   KEY |
    |-   27 |                 PARTITION RANGE SINGLE                     |                             |     2 |    86 |     4   (0)| 00:00:01 |   KEY |   KEY |
    |-   28 |                  TABLE ACCESS BY LOCAL INDEX ROWID BATCHED | WRH$_SYSTEM_EVENT           |     2 |    86 |     4   (0)| 00:00:01 |   KEY |   KEY |
    |- * 29 |                   INDEX RANGE SCAN                         | WRH$_SYSTEM_EVENT_PK        |    46 |       |     2   (0)| 00:00:01 |   KEY |   KEY |
    |    30 |                PARTITION RANGE SINGLE                      |                             |    90 |  3870 |     4   (0)| 00:00:01 |   KEY |   KEY |
    |    31 |                 TABLE ACCESS BY LOCAL INDEX ROWID BATCHED  | WRH$_SYSTEM_EVENT           |    90 |  3870 |     4   (0)| 00:00:01 |   KEY |   KEY |
    |  * 32 |                  INDEX RANGE SCAN                          | WRH$_SYSTEM_EVENT_PK        |    46 |       |     2   (0)| 00:00:01 |   KEY |   KEY |
    |    33 |               FIXED TABLE FULL                             | X$KEHEVTMAP                 |   105 |   735 |     0   (0)|          |       |       |
    |- * 34 |          INDEX RANGE SCAN                                  | WRH$_EVENT_NAME_PK          |       |       |            |          |       |       |
    |- * 35 |         TABLE ACCESS BY INDEX ROWID                        | WRH$_EVENT_NAME             |     1 |    64 |     7   (0)| 00:00:01 |       |       |
    |  * 36 |        TABLE ACCESS FULL                                   | WRH$_EVENT_NAME             |  1501 | 96064 |     7   (0)| 00:00:01 |       |       |
    ------------------------------------------------------------------------------------------------------------------------------------------------------------
    
    
    Note
    -----
       - this is an adaptive plan (rows marked '-' are inactive)
    

    Обратите внимание, что "код ветвления" был вставлен в этот план дважды.

    Простой пример с двумя таблицами пользователя

    Настало время оценить полезность изменяемых планов для нас, пользователей, на наших собственных данных. Сгенерируем простой набор в виде таблиц клиентов и их визитов к нам. Распределение записей по клиентам специально подобрано так, чтобы оптимизатор не мог быть уверен в количестве строк.

    Создание набора данных пользователя

    Используя приведенные ниже команды, создайте в своей базе подобные объекты в какой-нибудь из схем. Используйте "локального" пользователя с минимальным набором привилегий в одной из "подключаемых" PDB баз. Как я говорил в своей заметке про PDB/CDB, для удобства подключения установите переменную окружения среды TWO_TASK.

    ........................ == Книга пророка Иоиля == ............................
    === Глава 2, Стих 21 ===
    20 И пришедшего от севера удалю от вас, и изгоню в  землю  безводную  и  пустую,
       переднее полчище его - в море восточное,  а  заднее  -  в  море  западное,  и
       пойдет от него зловоние, и  поднимется  от  него  смрад,  так  как  он  много
       наделал _зла._
    21 Не бойся, земля: радуйся и  веселись,  ибо  Господь  велик,  чтобы  совершить
       это.
    
    (b+/b-, c+/c-, +/-, *) > 
    
    [oracle@localhost ~]$ sqlplus
    
    SQL*Plus: Release 12.1.0.1.0 Production
    ...
    
    11:53:00 SQL> create table customer (id number, name varchar2(100));
    
    Table created.
    
    Elapsed: 00:00:00.05
    11:53:47 SQL> 
    
    11:54:18 SQL> create table visits (cust_id number, visit_date date);
    
    Table created.
    
    Elapsed: 00:00:00.01
    11:55:01 SQL> 
    
    11:55:31 SQL> insert into customer values (1, 'ClientA');
    
    1 row created.
    
    Elapsed: 00:00:00.09
    11:55:43 SQL> commit;
    
    Commit complete.
    
    Elapsed: 00:00:00.00
    11:55:46 SQL>
    
    11:55:54 SQL> insert into visits values (1, sysdate);
    
    1 row created.
    
    Elapsed: 00:00:00.03
    11:56:09 SQL> /
    
    1 row created.
    
    Elapsed: 00:00:00.00
    11:56:16 SQL> /
    
    1 row created.
    
    Elapsed: 00:00:00.01
    11:56:26 SQL> commit;
    
    Commit complete.
    
    Elapsed: 00:00:00.01
    11:56:29 SQL> 
    
    
    -- Добавим данных для создания неравномерного распределения.
    12:37:35 SQL> insert into customer values(2, 'Client B');
    
    1 row created.
    
    Elapsed: 00:00:00.00
    12:37:57 SQL> insert into customer values(3, 'Client C');
    
    1 row created.
    
    Elapsed: 00:00:00.01
    12:38:12 SQL>  insert into customer values(4, 'Client D');
    
    1 row created.
    
    Elapsed: 00:00:00.00
    12:38:22 SQL> commit;
    
    Commit complete.
    
    Elapsed: 00:00:00.01
    12:38:24 SQL> 
    
    12:38:42 SQL> insert into visits select 2, VISIT_DATE from visits;
    
    3 rows created.
    
    Elapsed: 00:00:00.01
    12:39:19 SQL> c/2/3
      1* insert into visits select 3, VISIT_DATE from visits
    12:39:24 SQL> /
    
    6 rows created.
    
    Elapsed: 00:00:00.02
    12:39:26 SQL> c/3/4
      1* insert into visits select 4, VISIT_DATE from visits
    12:39:30 SQL> /
    
    12 rows created.
    
    Elapsed: 00:00:00.01
    12:39:31 SQL> commit;
    
    Commit complete.
    
    Elapsed: 00:00:00.02
    12:39:35 SQL> 
    
    
    
    12:39:48 SQL> select count(*) from customer;
    
      COUNT(*)
    ----------
             4
    
    1 row selected.
    
    Elapsed: 00:00:00.01
    12:39:58 SQL> select count(*) from visits;
    
      COUNT(*)
    ----------
            24
    
    1 row selected.
    
    Elapsed: 00:00:00.02
    12:40:09 SQL> 
    
    
    -- Теперь "раздуем" таблицу, копируя её саму в себя:
    insert into visits select * from visits;
    ...
    13:48:46 SQL> select count(*) from visits;
    
      COUNT(*)
    ----------
      25165824
    
    1 row selected.
    
    Elapsed: 00:00:00.28
    13:48:59 SQL> 
    
    
    -- Создадим отдельного клиента с минимумом записей
    10:32:21 SQL> insert into customer values (0, 'Small Client');
    
    1 row created.
    
    Elapsed: 00:00:00.00
    10:32:57 SQL> insert into visits values (0, sysdate);
    
    1 row created.
    
    Elapsed: 00:00:00.01
    10:33:19 SQL> /
    
    1 row created.
    
    Elapsed: 00:00:00.00
    10:33:20 SQL> /
    
    1 row created.
    
    Elapsed: 00:00:00.00
    10:33:20 SQL> commit;
    
    Commit complete.
    
    Elapsed: 00:00:00.01
    10:33:22 SQL> 
    
    
    
    -- Создадим необходимые связи между таблицами
    12:02:40 SQL> alter table customer add constraint PK_CUS_ID primary key (id);
    
    Table altered.
    
    Elapsed: 00:00:00.09
    12:03:05 SQL> alter table visits add constraint FK_VIS_CUS -
    > foreign key (cust_id) references customer(id);
    
    Table altered.
    
    Elapsed: 00:00:00.01
    12:03:12 SQL> 
    
    
    create index IDX_VIS_CUSID  on visits(cust_id) nologging;
    
    Index created.
    
    Elapsed: 00:00:00.08
    
    
    -- Соберём статистику "по умолчанию" для всех объектов схемы
    10:10:24 SQL> exec dbms_stats.gather_schema_stats(user);
    
    PL/SQL procedure successfully completed.
    
    Elapsed: 00:00:02.68
    10:28:02 SQL>
    
    
    10:28:53 SQL> l
      1  select table_name, degree, last_analyzed, num_rows
      2* from user_tables
    10:28:55 SQL> /
    
    10:34:16 SQL> select table_name, degree, last_analyzed, num_rows
    10:34:25   2  from user_tables;
    
    TABLE_NAME           DEGREE     LAST_ANALYZED       NUM_ROWS
    -------------------- ---------- ----------------- ----------
    VISITS                        1 18-03-14 10:34:16   25165827
    CUSTOMER                      1 18-03-14 10:34:14          5
    
    2 rows selected.
    
    Elapsed: 00:00:00.01
    
    -- Which customer has most records?
      1  select cust_id, count(*)
      2  from visits
      3  group by cust_id
      4* order by 2
    10:35:37 SQL> /
    
       CUST_ID   COUNT(*)
    ---------- ----------
             0          3
             2    3145728
             1    3145728
             3    6291456
             4   12582912
    
    5 rows selected.
    
    Elapsed: 00:00:00.66
    10:35:40 SQL> 
    
    

    После сбора статистики Оракл построил для нас частотную гистограмму для колонки CUST_ID таблицы VISITS:

    .......... == Первое соборное послание святого апостола Иоанна == ..............
    === Глава 2, Стих 22 ===
    22 Кто  лжец,  если  не  тот,  кто  отвергает,  что  Иисус  есть  Христос?   Это
       антихрист, отвергающий Отца и Сына.
    
    (b+/b-, c+/c-, +/-, *) > 
    
    [oracle@localhost ~]$ sqlplus
    
    SQL*Plus: Release 12.1.0.1.0 Production
    ...
    
    SQL> l
      1  select TABLE_NAME, COLUMN_NAME, ENDPOINT_NUMBER, ENDPOINT_VALUE, 
      2  ENDPOINT_REPEAT_COUNT from user_histograms
      3* order by TABLE_NAME, COLUMN_NAME, ENDPOINT_NUMBER
    10:38:29 SQL> /
    
    TABLE_NAME           COLUMN_NAM ENDPOINT_NUMBER ENDPOINT_VALUE ENDPOINT_REPEAT_COUNT
    -------------------- ---------- --------------- -------------- ---------------------
    CUSTOMER             ID                       0              0                     0
    CUSTOMER             ID                       1              4                     0
    CUSTOMER             NAME                     0     3.5008E+35                     0
    CUSTOMER             NAME                     1     4.3318E+35                     0
    VISITS               CUST_ID                  3              0                     0
    VISITS               CUST_ID            3145731              1                     0
    VISITS               CUST_ID            6291459              2                     0
    VISITS               CUST_ID           12582915              3                     0
    VISITS               CUST_ID           25165827              4                     0
    VISITS               VISIT_DATE               0      2456734.5                     0
    VISITS               VISIT_DATE               1     2456735.44                     0
    
    11 rows selected.
    
    Elapsed: 00:00:00.08
    10:38:31 SQL> 
    
      1  select TABLE_NAME, COLUMN_NAME, NUM_DISTINCT, NUM_BUCKETS, HISTOGRAM
      2  from user_tab_col_statistics
      3* where HISTOGRAM != 'NONE'
    10:47:38 SQL> /
    
    TABLE_NAME           COLUMN_NAM NUM_DISTINCT NUM_BUCKETS HISTOGRAM
    -------------------- ---------- ------------ ----------- ---------------
    VISITS               CUST_ID               5           5 FREQUENCY
    
    1 row selected.
    
    Elapsed: 00:00:00.48
    10:47:39 SQL> 
    

    Существуют параметры, определяющие эффект адаптивных планов, по умолчанию оптимизатор запросов будет их создавать и использовать:

    12:23:06 SQL> show parameter adaptive
    
    NAME                                 TYPE        VALUE
    ------------------------------------ ----------- ------------------------------
    optimizer_adaptive_features          boolean     TRUE
    optimizer_adaptive_reporting_only    boolean     FALSE
    parallel_adaptive_multi_user         boolean     TRUE
    12:30:02 SQL>
    

    Итак, данные созданы - пора перейти к запросу.

    Запрос пользователя и начальный план его выполнения

    Запрос очень простой и предсказуемый, посмотрим какой план Оракл нам предложит до его исполнения:

      1  explain plan for
      2  select /*+ gather_plan_statistics Test1 */ c.name, v.visit_date
      3  from customer c, visits v
      4  where c.id = v.cust_id
      5  and c.id > :custid
      6  and v.visit_date < sysdate -1
      7* order by v.visit_date
    11:55:21 SQL> /
    
    Explained.
    
    Elapsed: 00:00:00.03
    11:55:21 SQL> 
    11:55:22 SQL> 
    11:55:22 SQL> 
    11:55:22 SQL> 
    11:55:22 SQL> select * from table(dbms_xplan.display(format=>'ADAPTIVE NOTE'));
    
    PLAN_TABLE_OUTPUT
    ------------------------------------------------------------------------------
    Plan hash value: 3793304633
    
    -----------------------------------------------------------------------------------------------------
    |   Id  | Operation                             | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
    -----------------------------------------------------------------------------------------------------
    |     0 | SELECT STATEMENT                      |           |     1 |    24 |     6   (0)| 00:00:01 |
    |     1 |  SORT ORDER BY                        |           |     1 |    24 |     6   (0)| 00:00:01 |
    |- *  2 |   HASH JOIN                           |           |     1 |    24 |     6   (0)| 00:00:01 |
    |     3 |    NESTED LOOPS                       |           |       |       |            |          |
    |     4 |     NESTED LOOPS                      |           |     1 |    24 |     6   (0)| 00:00:01 |
    |-    5 |      STATISTICS COLLECTOR             |           |       |       |            |          |
    |  *  6 |       TABLE ACCESS FULL               | VISITS    |     1 |    11 |     5   (0)| 00:00:01 |
    |  *  7 |      INDEX UNIQUE SCAN                | PK_CUS_ID |     1 |       |     0   (0)| 00:00:01 |
    |     8 |     TABLE ACCESS BY INDEX ROWID       | CUSTOMER  |     1 |    13 |     1   (0)| 00:00:01 |
    |-    9 |    TABLE ACCESS BY INDEX ROWID BATCHED| CUSTOMER  |     1 |    13 |     1   (0)| 00:00:01 |
    |- * 10 |     INDEX RANGE SCAN                  | PK_CUS_ID |     1 |       |     0   (0)| 00:00:01 |
    -----------------------------------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
    
       2 - access("C"."ID"="V"."CUST_ID")
       6 - filter("V"."CUST_ID">TO_NUMBER(:CUSTID) AND "V"."VISIT_DATE"TO_NUMBER(:CUSTID))
      10 - access("C"."ID">TO_NUMBER(:CUSTID))
    
    Note
    -----
       - this is an adaptive plan (rows marked '-' are inactive)
    
    30 rows selected.
    
    Elapsed: 00:00:00.17
    

    За исключением создания адаптивного плана, ничего выдающегося. Оптимизатор решил не использовать Hash Join ввиду малого числа ожидаемых строк (всего одна).

    Hint "gather_plan_statistics" нам понадобится во всех тестах, наравне с другим полезным хинтом "monitor". Почитайте в документации про них. Не забудьте проверить ваше лицензирование вообще, в том числе если используете "management pack".

    Запускаем запрос и проверяем действительный план выполнения

    Вы наверняка помните, что "explain plan" более не является полностью надежной утилитой - он может показать одно, а на самом деле запрос будет выполнен иначе. Запустим SQL и после его завершения проверим план по "sql_id".

    11:56:01 SQL> print :custid
    
        CUSTID
    ----------
             2
    
    11:58:21 SQL> 
    
      1  select /*+ gather_plan_statistics Test1 */ c.name, v.visit_date
      2  from customer c, visits v
      3  where c.id = v.cust_id
      4  and c.id > :custid
      5  and v.visit_date < sysdate -1
      6* order by v.visit_date
    11:58:53 SQL> /
    
    no rows selected
    
    Elapsed: 00:00:00.01
    11:58:53 SQL> 
    

    Итак, оптимизатор был неправ - наш запрос не вернул строк вообще. Определим sql_id запроса и проверим как же он был выполнен.

      1  select LAST_ACTIVE_TIME, sql_id, sql_text, child_number, IS_REOPTIMIZABLE,
      2  IS_RESOLVED_ADAPTIVE_PLAN, fetches, executions, end_of_fetch_count, 
      3  rows_processed, plan_hash_value from v$sql
      4* where sql_text like '%Test1%'
    12:00:55 SQL> /
    
    LAST_ACTIVE_TIME  SQL_ID
    ----------------- -------------
    SQL_TEXT
    ----------------------------------------------------------------------------------------
    CHILD_NUMBER I I    FETCHES EXECUTIONS END_OF_FETCH_COUNT ROWS_PROCESSED PLAN_HASH_VALUE
    ------------ - - ---------- ---------- ------------------ -------------- ---------------
    
    24-03-14 11:58:53 7spzkn428fjdu
    select /*+ gather_plan_statistics Test1 */ c.name, v.visit_date from customer c, visits v 
    where c.id = v.cust_id and c.id > :custid and 
    v.visit_date < sysdate -1 order by v.visit_date
               0 N Y          1          1                  1              0       865602956
    
    
    SQL> select * from table(dbms_xplan.display_cursor('7spzkn428fjdu', 0, -
    > 'ADAPTIVE NOTE ALLSTATS'))
    13:45:52 SQL> /
    
    PLAN_TABLE_OUTPUT
    ----------------------------------------------------------------------------------------
    SQL_ID  7spzkn428fjdu, child number 0
    -------------------------------------
    select /*+ gather_plan_statistics Test1 */ c.name, v.visit_date from
    customer c, visits v where c.id = v.cust_id and c.id > :custid and
    v.visit_date < sysdate -1 order by v.visit_date
    
    Plan hash value: 865602956
    
    ----------------------------------------------------------------------------------------------------------------------
    |   Id  | Operation                                | Name          | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
    ----------------------------------------------------------------------------------------------------------------------
    |     0 | SELECT STATEMENT                         |               |      1 |        |      0 |00:00:00.01 |       2 |
    |     1 |  SORT ORDER BY                           |               |      1 |      1 |      0 |00:00:00.01 |       2 |
    |- *  2 |   HASH JOIN                              |               |      1 |      1 |      0 |00:00:00.01 |       2 |
    |     3 |    NESTED LOOPS                          |               |      1 |        |      0 |00:00:00.01 |       2 |
    |     4 |     NESTED LOOPS                         |               |      1 |      1 |      0 |00:00:00.01 |       2 |
    |-    5 |      STATISTICS COLLECTOR                |               |      1 |        |      0 |00:00:00.01 |       2 |
    |  *  6 |       TABLE ACCESS BY INDEX ROWID BATCHED| VISITS        |      1 |      1 |      0 |00:00:00.01 |       2 |
    |  *  7 |        INDEX RANGE SCAN                  | IDX_VIS_CUSID |      1 |      2 |      0 |00:00:00.01 |       2 |
    |  *  8 |      INDEX UNIQUE SCAN                   | PK_CUS_ID     |      0 |      1 |      0 |00:00:00.01 |       0 |
    |     9 |     TABLE ACCESS BY INDEX ROWID          | CUSTOMER      |      0 |      1 |      0 |00:00:00.01 |       0 |
    |-   10 |    TABLE ACCESS BY INDEX ROWID BATCHED   | CUSTOMER      |      0 |      1 |      0 |00:00:00.01 |       0 |
    |- * 11 |     INDEX RANGE SCAN                     | PK_CUS_ID     |      0 |      1 |      0 |00:00:00.01 |       0 |
    ----------------------------------------------------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
    
       2 - access("C"."ID"="V"."CUST_ID")
       6 - filter("V"."VISIT_DATE":CUSTID)
       8 - access("C"."ID"="V"."CUST_ID")
           filter("C"."ID">:CUSTID)
      11 - access("C"."ID">:CUSTID)
    
    Note
    -----
       - this is an adaptive plan (rows marked '-' are inactive)
    

    Итак, при выполнении использовался слегка другой план (сравните hash плана в предыдущем подразделе и этого) - оптимизатор решил использовать индексы для обеих таблиц. Но этот план тоже был адаптивным, при этом оптимизатор не был уверен надо ли делать Hash Join или Nested Loops до тех пор, пока не начал получать (отсутствующие) результаты.

    Обратили ли вы внимание на привязку запроса к текущей дате? С тем же самым значением переменной :custid (равному 2) через 24 часа этот же запрос вернёт миллионы строк и оптимизатор решит "переключиться" во время исполнения на Hash Join. Правда, при этом необходимо очистить library cache или дождаться естественного устаревания и исчезновения уже ставшего постоянным плана с хэшем 865602956 (об этом свидетельствует IS_RESOLVED_ADAPTIVE_PLAN=Y в результате запроса вверху). Так что, по-прежнему, ещё есть возможность "обмануть" оптимизатор. Надо отметить, что другие средства оптимизации могут заставить Оракл сгенерировать новый фиксированный план даже при уже имеющемся адаптивном плане (например, обновление или устаревание статистики) - но в нашем случае привязки к дате вполне вероятна ситуация, когда миллионы строк будут возвращаться через Nested Loops. Я оставляю этот случай для вас в качестве упражнения.

    Скрипт для тестирования

    Используйте этот скрипт для тестирования разных условий и планов. Результаты должны показать, что внесение потенциальной "неясности" сразу же приводит к генерации изменяемого плана. Соответственно, ясно выраженные условия, при наличии гистограммы и правильной статистики, приводят к использованию простого фиксированного постоянного плана с самого начала.

    var custid number
    exec :custid:=2;
    print :custid;
    
    explain plan for
    select /*+ gather_plan_statistics Test1 */ c.name, v.visit_date from
    customer c, visits v where c.id = v.cust_id 
    -- Откомментируйте следующие строки по одной
    --and c.id = :custid     -- fixed plan
    --and c.id > :custid     -- adaptive plan
    --and v.visit_date < sysdate -1  -- adaptive plan
    --and v.visit_date = :custid  -- fixed plan
    order by v.visit_date
    /
    select * from table(dbms_xplan.display(format=>'ADAPTIVE NOTE'))
    /
    rollback
    /
    

    В конце тестирования, измените степень параллельного выполнения на таблицах и повторите тесты в приведенном выше скрипте. Изменятся ли полученные планы?

    12:05:11 SQL> alter table VISITS parallel 8;
    
    Table altered.
    
    Elapsed: 00:00:00.01
    12:05:19 SQL>  alter table CUSTOMER parallel 8;
    
    Table altered.
    

    Смогли ли вы добиться адаптивных планов при параллельном выполнении запросов?

    Моё тестирование показало, что в текущей версии Оракла 12.1.0.1 адаптивные планы используются только когда возможна ошибка оптимизации. Они используются почти всегда при наличии условий с неравенством и однопоточного выполнения запроса. В случае параллельного выполнения адаптивные планы почти никогда не генерируются. Также для создания изменяемых планов используйте PDB, подключаясь к ней как локальный пользователь - во многих случаях оптимизатор не генерирует адаптивные планы при подключении к контейнеру CDB$ROOT или не-CDB базе данных.

    Пример, использующий схему "OE"

    Поставляемая с Оракл схема Order Entry (загрузите и установите "Oracle Database 12c Release 1 Examples") тоже генерирует адаптивные планы. Используем простой запрос:

    SQL> l
      1  explain plan for
      2  select product_name
      3  from oe.order_items o, oe.product_information p
      4  where o.unit_price = 15
      5  and o.quantity >1
      6* and p.product_id = o.product_id
    09:49:55 SQL>
    

    Какой план нам "предскажет" оптимизатор до выполнения запроса?

      1* select * from table(dbms_xplan.display(format=>'ADAPTIVE NOTE ALLSTATS'))
    09:50:54 SQL> /
    
    PLAN_TABLE_OUTPUT
    ------------------------------
    Plan hash value: 1553478007
    
    ---------------------------------------------------------------------------
    |   Id  | Operation                     | Name                   | E-Rows |
    ---------------------------------------------------------------------------
    |     0 | SELECT STATEMENT              |                        |     13 |
    |  *  1 |  HASH JOIN                    |                        |     13 |
    |-    2 |   NESTED LOOPS                |                        |        |
    |-    3 |    NESTED LOOPS               |                        |     13 |
    |-    4 |     STATISTICS COLLECTOR      |                        |        |
    |  *  5 |      TABLE ACCESS FULL        | ORDER_ITEMS            |     13 |
    |- *  6 |     INDEX UNIQUE SCAN         | PRODUCT_INFORMATION_PK |        |
    |-    7 |    TABLE ACCESS BY INDEX ROWID| PRODUCT_INFORMATION    |      1 |
    |     8 |   TABLE ACCESS FULL           | PRODUCT_INFORMATION    |    288 |
    ---------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
    
       1 - access("P"."PRODUCT_ID"="O"."PRODUCT_ID")
       5 - filter("O"."UNIT_PRICE"=15 AND "O"."QUANTITY">1)
       6 - access("P"."PRODUCT_ID"="O"."PRODUCT_ID")
    
    Note
    -----
       - this is an adaptive plan (rows marked '-' are inactive)
       - Warning: basic plan statistics not available. These are only collected when:
           * hint 'gather_plan_statistics' is used for the statement or
           * parameter 'statistics_level' is set to 'ALL', at session or system level
    
    29 rows selected.
    
    Elapsed: 00:00:00.04
    09:50:54 SQL> 
    

    Выполним запрос:

      1  select product_name
      2   from oe.order_items o, oe.product_information p
      3   where o.unit_price = 15
      4   and o.quantity >1
      5*  and p.product_id = o.product_id
    09:52:17 SQL> /
    
    PRODUCT_NAME
    --------------------------------------------------
    Screws 
    Screws 
    Screws 
    Screws 
    Screws 
    Screws 
    Screws 
    Screws 
    Screws 
    Screws 
    Screws 
    Screws 
    Screws 
    
    13 rows selected.
    
    Elapsed: 00:00:00.01
    

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

    SQL_ID  8bwz2v2542zm4, child number 0
    -------------------------------------
    select /*+ gather_plan_statistics */ product_name from oe.order_items
    o, oe.product_information p where o.unit_price = 15 and o.quantity >1
    and p.product_id = o.product_id
    
    Plan hash value: 1553478007
    
    -----------------------------------------------------------------------------------------------------------------------------------------------
    |   Id  | Operation                     | Name                   | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem |  O/1/M   |
    -----------------------------------------------------------------------------------------------------------------------------------------------
    |     0 | SELECT STATEMENT              |                        |      1 |        |     13 |00:00:00.01 |      24 |       |       |          |
    |  *  1 |  HASH JOIN                    |                        |      1 |     13 |     13 |00:00:00.01 |      24 |  2061K|  2061K|     1/0/0|
    |-    2 |   NESTED LOOPS                |                        |      1 |        |     13 |00:00:00.01 |       7 |       |       |          |
    |-    3 |    NESTED LOOPS               |                        |      1 |     13 |     13 |00:00:00.01 |       7 |       |       |          |
    |-    4 |     STATISTICS COLLECTOR      |                        |      1 |        |     13 |00:00:00.01 |       7 |       |       |          |
    |  *  5 |      TABLE ACCESS FULL        | ORDER_ITEMS            |      1 |     13 |     13 |00:00:00.01 |       7 |       |       |          |
    |- *  6 |     INDEX UNIQUE SCAN         | PRODUCT_INFORMATION_PK |      0 |        |      0 |00:00:00.01 |       0 |       |       |          |
    |-    7 |    TABLE ACCESS BY INDEX ROWID| PRODUCT_INFORMATION    |      0 |      1 |      0 |00:00:00.01 |       0 |       |       |          |
    |     8 |   TABLE ACCESS FULL           | PRODUCT_INFORMATION    |      1 |    288 |    288 |00:00:00.01 |      17 |       |       |          |
    -----------------------------------------------------------------------------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
    
       1 - access("P"."PRODUCT_ID"="O"."PRODUCT_ID")
       5 - filter(("O"."UNIT_PRICE"=15 AND "O"."QUANTITY">1))
       6 - access("P"."PRODUCT_ID"="O"."PRODUCT_ID")
    
    Note
    -----
       - this is an adaptive plan (rows marked '-' are inactive)
    

    Всё оказалось именно так - даже хэш плана до и после выполнения остался неизменным. Что ещё раз показывает исключительную важность правильной статистики для SQL оптимизатора версии Oracle 12c. Кстати, статистика теперь тоже стала адаптивной, как и планы - но это уже другая история.

    Спасибо что зашли,

    Будьте благословенны!
    Денис

    27 Марта 2014 года.