У меня есть следующий запрос:
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 используется для создания, изменения и управления данными в базах данных.
Скорее всего, Oracle выполняет хэш-соединение в одной среде, а не вложенные циклы в другой. Очень часто запрос ведет себя по-разному при выполнении в другой среде. Существует множество факторов, которые оптимизатор учитывает при разработке своего плана, поэтому нельзя ожидать, что каждый раз в двух разных базах данных будет один и тот же план.
Вот пара вещей, которые стоит попробовать:
Вы действительно хотите FETCH NEXT
, а не FETCH FIRST
?
Вероятно, вам вообще не следует использовать этот внутренний блок запросов. Упрощать:
SELECT t.*
FROM transactions t
ORDER BY t.transaction_date DESC, t.id DESC
FETCH FIRST 11 ROWS ONLY
Для максимальной скорости переопределите индекс как (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%. Когда это происходит, это означает, что время выполнения было потрачено на выполнение рекурсивного запроса; запрос, предназначенный для предоставления полезной информации, редко может занять больше времени, чем сам запрос. В частности, в редких случаях запрос динамической выборки может выполняться медленно. Возможно, сбор статистики позволит избежать необходимости выполнять дорогостоящий запрос динамической выборки.
Кроме того, я согласен с идеей Пола В. о переписывании запроса. Замена самосоединения более сложным запросом часто позволяет сэкономить много времени.