Один и тот же запрос с одним и тем же планом выполнения выполняется по-разному в разных средах Oracle

У меня есть следующий запрос:

SELECT t.*
FROM 
    (SELECT t.id, t.transaction_date
     FROM transactions t 
     ORDER BY t.transaction_date DESC, t.id DESC
     FETCH NEXT 11 ROWS ONLY) transactions_table 
JOIN 
    transactions t ON transactions_table.id = t.id
ORDER BY 
    t.transaction_date DESC, t.id DESC;

Столбец ID — это PRIMARY KEY таблицы, у меня есть следующий оператор CREATE INDEX:

CREATE INDEX transaction_date_idx ON transactions (transaction_date DESC, id);

План выполнения следующий:

PLAN_TABLE_OUTPUT
Plan hash value: 3772986339
 
---------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name                              | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |                                   |    11 |   167K|       | 35981   (2)| 00:00:02 |
|   1 |  SORT ORDER BY                |                                   |    11 |   167K|       | 35981   (2)| 00:00:02 |
|   2 |   NESTED LOOPS                |                                   |    11 |   167K|       | 35980   (2)| 00:00:02 |
|   3 |    NESTED LOOPS               |                                   |    11 |   167K|       | 35980   (2)| 00:00:02 |
|*  4 |     VIEW                      |                                   |    11 |   286 |       | 35958   (2)| 00:00:02 |
|*  5 |      WINDOW SORT PUSHED RANK  |                                   |  4345K|   107M|   150M| 35958   (2)| 00:00:02 |
|   6 |       INDEX FAST FULL SCAN    | TRANSACTIONS_TRANSACTION_DATE_IDX |  4345K|   107M|       |  3593   (2)| 00:00:01 |
|*  7 |     INDEX UNIQUE SCAN         | PK_TRANSACTIONS                   |     1 |       |       |     1   (0)| 00:00:01 |
|   8 |    TABLE ACCESS BY INDEX ROWID| TRANSACTIONS                      |     1 | 15582 |       |     2   (0)| 00:00:01 |
---------------------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
"   4 - filter(""from$_subquery$_003"".""rowlimit_$$_rownumber""<=11)"
"   5 - filter(ROW_NUMBER() OVER ( ORDER BY SYS_OP_DESCEND(""TRANSACTION_DATE""),INTERNAL_FUNCTION(""T"".""ID"") DESC "
              )<=11)
"   7 - access(""T2"".""ID"" = ""from$_subquery$_003"".""ID"")"
 
Note
-----
   - dynamic statistics used: dynamic sampling (level=2)

У меня есть две среды с одинаковыми данными (~ 4 миллиона строк в таблице transactions). Первая среда, в которой запрос выполняется как положено (~ 500 мс). Во второй среде это занимает около 30 секунд! План выполнения для обеих сред абсолютно одинаков, за исключением:

Note
-----
   - dynamic statistics used: dynamic sampling (level=2)

не печатаются на быстровозвратной среде. Когда я сравниваю внутренние запросы:

SELECT t.id, t.transaction_date
FROM transactions t 
ORDER BY t.transaction_date DESC, t.id DESC
FETCH NEXT 11 ROWS ONLY

он работает почти одинаково в обеих средах (обе быстро). Я считаю, что когда я запускаю часть JOIN, дела идут медленно. Почему?

🤔 А знаете ли вы, что...
SQL используется для создания, изменения и управления данными в базах данных.


2
88
2

Ответы:

Скорее всего, Oracle выполняет хэш-соединение в одной среде, а не вложенные циклы в другой. Очень часто запрос ведет себя по-разному при выполнении в другой среде. Существует множество факторов, которые оптимизатор учитывает при разработке своего плана, поэтому нельзя ожидать, что каждый раз в двух разных базах данных будет один и тот же план.

Вот пара вещей, которые стоит попробовать:

  1. Вы действительно хотите FETCH NEXT, а не FETCH FIRST?

  2. Вероятно, вам вообще не следует использовать этот внутренний блок запросов. Упрощать:

    SELECT t.*
    FROM transactions t
    ORDER BY t.transaction_date DESC, t.id DESC
    FETCH FIRST 11 ROWS ONLY
    
  3. Для максимальной скорости переопределите индекс как (transaction_date,id). Это означает удаление ключевого слова DESC из определения индекса. Тогда намекните:

    SELECT /*+ INDEX_DESC(t) */ t.*
    FROM transactions t
    ORDER BY t.transaction_date DESC, t.id DESC
    FETCH FIRST 11 ROWS ONLY
    

Проверьте свой план и убедитесь, что там указано INDEX FULL SCAN DESCENDING (не INDEX FAST FULL SCAN), а над ним вы видите операцию WINDOW NOSORT STOPKEY (не WINDOW SORT PUSHED RANK). Вы пытаетесь избежать подобного.

Если вам нужно сделать это во вложенном блоке запроса, поскольку вам требуется присоединение к другим таблицам в родительском блоке, попробуйте что-то вроде этого:

SELECT /*+ USE_NL(t t2 t3) */ t.*,t2.*,t3.*
  FROM (SELECT /*+ NO_MERGE INDEX_DESC(t) */ ROWID row_id, t.id, t.transaction_date
         FROM transaction t 
        ORDER BY t.transaction_date DESC, t.id DESC
        FETCH FIRST 11 ROWS ONLY) transactions_table
       JOIN transaction t ON transactions_table.row_id = t.ROWID -- if self-joining
       LEFT OUTER JOIN transaction t2 ON t2.id = transactions_table.id -- other table join
       LEFT OUTER JOIN transaction t3 ON t3.id = transactions_table.id -- other table join
 ORDER BY transactions_table.transaction_date DESC, transactions_table.id DESC    

Обратите внимание на подсказки USE_NL и NO_MERGE. NO_MERGE предотвращает слияние представлений Oracle, которое сделало бы недействительным ваш INDEX_DESC-подсказку, а USE_NL обещает Oracle, что действительно лучше использовать вложенные циклы, а не хеш-соединение для этих объединений с другими таблицами. Обратите внимание, что я также дополнительно использовал ROWID для самосоединения. Это позволяет избежать повторного обращения к индексу, хотя вполне вероятно, что индекс был тем же самым, который использовался во внутреннем запросе, поэтому конечные блоки кэшируются, и выгода от его пропуска на втором проходе может быть незаметной.

Самообъединение действительно не требуется, поскольку вы можете вернуть все нужные столбцы из внутреннего запроса. Единственная ситуация, в которой я считаю полезным такое самостоятельное присоединение, — это когда я могу дисквалифицировать строки на основе критериев из других таблиц и хочу избежать затрат на чтение блоков из сегмента таблицы, пока я этого не сделаю. Ограничение внутреннего запроса ссылкой только на столбцы, являющиеся частью индекса, — вот что позволит реализовать этот трюк с производительностью.


Решено

Примечание dynamic statistics used: dynamic sampling (level=2) подразумевает, что в таблице отсутствует статистика по одному из ваших сред. Соберите статистику с помощью такого блока PL/SQL:

begin
    dbms_stats.gather_table_stats(ownname => user, tabname => 'TRANSACTIONS');
end;
/

Если это решит проблему, вам следует выяснить, почему статистика не собиралась автоматически. По умолчанию база данных Oracle каждый день автоматически собирает статистику по таблице, в которой есть новые данные или которая изменилась более чем на 10%. К сожалению, многие организации предпочитают отключать автозадачу по умолчанию и внедрять собственное решение.

Если это не решит проблему или вам интересно, почему это решило проблему, сверьте план выполнения с фактическими цифрами, а не с использованием догадок плана объяснения. Соберите данные, используя select dbms_sqltune.report_sql_monitor(sql_id => 'your SQL_ID') from dual;, и добавьте результаты к своему вопросу. Отчет о мониторинге SQL сообщит вам, какая операция занимает больше всего времени и каковы ожидания.

Я предполагаю, что для этой проблемы ваша «Активность (%)» не составит в сумме 100%. Когда это происходит, это означает, что время выполнения было потрачено на выполнение рекурсивного запроса; запрос, предназначенный для предоставления полезной информации, редко может занять больше времени, чем сам запрос. В частности, в редких случаях запрос динамической выборки может выполняться медленно. Возможно, сбор статистики позволит избежать необходимости выполнять дорогостоящий запрос динамической выборки.

Кроме того, я согласен с идеей Пола В. о переписывании запроса. Замена самосоединения более сложным запросом часто позволяет сэкономить много времени.