2013-05-21 2 views
4

Это будет долго, так вот краткое резюме, чтобы привлечь вас: мой топ-N запрос с COUNT STOPKEY и ORDER BY STOPKEY в его плане является еще медленно без уважительной причины.топ-N запрос делает слишком много работы, несмотря на оптимизацию STOPKEY

Теперь детали. Он начинается с медленной функции. В реальной жизни это включает в себя строковые манипуляции с регулярными выражениями. Для демонстрационных целей здесь преднамеренно глупый рекурсивный алгоритм Фибоначчи. Я считаю, что быть довольно быстро для входов примерно до 25, медленно около 30, и смешно на 35.

-- I repeat: Please no advice on how to do Fibonacci correctly. 
-- This is slow on purpose! 
CREATE OR REPLACE FUNCTION tmp_fib (
    n INTEGER 
) 
    RETURN INTEGER 
AS 
BEGIN 
    IF n = 0 OR n = 1 THEN 
    RETURN 1; 
    END IF; 
    RETURN tmp_fib(n-2) + tmp_fib(n-1); 
END; 
/

Теперь некоторые ввода: список имен и номеров.

CREATE TABLE tmp_table (
    name VARCHAR2(20) UNIQUE NOT NULL, 
    num NUMBER(2,0) 
); 
INSERT INTO tmp_table (name,num) 
    SELECT 'Alpha', 10 FROM dual UNION ALL 
    SELECT 'Bravo', 11 FROM dual UNION ALL 
    SELECT 'Charlie', 33 FROM dual; 

Вот пример медленного запроса: использовать медленные функции Фибоначчи выбора строк, чьи Num генерирует число Фибоначчи с удвоенным цифрой.

SELECT p.name, p.num 
FROM tmp_table p 
WHERE REGEXP_LIKE(tmp_fib(p.num), '(.)\1') 
ORDER BY p.name; 

Это верно для 11 и 33, так и BravoCharlie находятся в выходе. Это займет около 5 секунд для запуска, почти все из которых являются медленными расчет tmp_fib(33). Поэтому я хочу сделать более быструю версию медленного запроса , преобразов его в запрос top-N. При N = 1, это выглядит так:

SELECT * FROM (
    SELECT p.name, p.num 
    FROM tmp_table p 
    WHERE REGEXP_LIKE(tmp_fib(p.num), '(.)\1') 
    ORDER BY p.name 
) 
WHERE ROWNUM <= 1; 

И теперь она возвращает верхний результат, Bravo. Но все равно требуется 5 секунд для запуска! Единственное объяснение состоит в том, что он все еще вычисляет tmp_fib(33), хотя результат этого расчета не имеет значения . Он должен был принять решение о выходе Bravo , поэтому нет необходимости проверять условие WHERE для остальной таблицы .

Я думал, что, возможно, оптимизатору просто нужно сказать, что tmp_fib стоит дорого. Так что я пытался сказать ему, что, как это:

ASSOCIATE STATISTICS WITH FUNCTIONS tmp_fib DEFAULT COST (999999999,0,0); 

Это изменяет некоторые цифры затрат в плане, но это не делает вводного запрос быстрее.

Выход SELECT * FROM v$version в случае, если это версия-зависимый:

Oracle Database 11g Enterprise Edition Release 11.2.0.2.0 - 64bit Production 
PL/SQL Release 11.2.0.2.0 - Production 
CORE 11.2.0.2.0  Production 
TNS for 64-bit Windows: Version 11.2.0.2.0 - Production 
NLSRTL Version 11.2.0.2.0 - Production 

И вот автотрассировка запроса топ-1. Похоже, что запрашивает , что запрос занял 1 секунду, но это неверно. Он пробежал около 5 секунд.

NAME      NUM 
-------------------- ---------- 
Bravo      11 


Execution Plan 
---------------------------------------------------------- 
Plan hash value: 548796432 

------------------------------------------------------------------------------------- 
| Id | Operation    | Name  | Rows | Bytes | Cost (%CPU)| Time  | 
------------------------------------------------------------------------------------- 
| 0 | SELECT STATEMENT  |   |  1 | 55 |  4 (25)| 00:00:01 | 
|* 1 | COUNT STOPKEY   |   |  |  |   |   | 
| 2 | VIEW     |   |  1 | 55 |  4 (25)| 00:00:01 | 
|* 3 | SORT ORDER BY STOPKEY|   |  1 | 55 |  4 (25)| 00:00:01 | 
|* 4 |  TABLE ACCESS FULL | TMP_TABLE |  1 | 55 |  3 (0)| 00:00:01 | 
------------------------------------------------------------------------------------- 

Predicate Information (identified by operation id): 
--------------------------------------------------- 

    1 - filter(ROWNUM<=1) 
    3 - filter(ROWNUM<=1) 
    4 - filter(REGEXP_LIKE (TO_CHAR("TMP_FIB"("P"."NUM")),'(.)\1')) 

Note 
----- 
    - dynamic sampling used for this statement (level=2) 


Statistics 
---------------------------------------------------------- 
     27 recursive calls 
      0 db block gets 
     25 consistent gets 
      0 physical reads 
      0 redo size 
     593 bytes sent via SQL*Net to client 
     524 bytes received via SQL*Net from client 
      2 SQL*Net roundtrips to/from client 
      1 sorts (memory) 
      0 sorts (disk) 
      1 rows processed 

ОБНОВЛЯТЬ: Как я уже говорил в комментариях, INDEX подсказка помогает этот запрос много. Было бы достаточно, чтобы вас приняли в качестве правильного ответа, хотя это не очень хорошо отразилось на моем реальном сценарии.И в ироническом твисте, Oracle, похоже, учился на опыте, и теперь выбирает план INDEX по умолчанию; Я должен сказать это NO_INDEX, чтобы воспроизвести оригинальное медленное поведение.

В реальном сценарии я применил более сложное решение, переписывая запрос как функцию PL/SQL. Вот как моя техника выглядит, примененные к fib проблемам:

CREATE OR REPLACE PACKAGE tmp_package IS 
    TYPE t_namenum IS TABLE OF tmp_table%ROWTYPE; 
    FUNCTION get_interesting_names (howmany INTEGER) RETURN t_namenum PIPELINED; 
END; 
/

CREATE OR REPLACE PACKAGE BODY tmp_package IS 
    FUNCTION get_interesting_names (howmany INTEGER) RETURN t_namenum PIPELINED IS 
    CURSOR c IS SELECT name, num FROM tmp_table ORDER BY name; 
    rec c%ROWTYPE; 
    outcount INTEGER; 
    BEGIN 
    OPEN c; 
    outcount := 0; 
    WHILE outcount < howmany LOOP 
     FETCH c INTO rec; 
     EXIT WHEN c%NOTFOUND; 
     IF REGEXP_LIKE(tmp_fib(rec.num), '(.)\1') THEN 
     PIPE ROW(rec); 
     outcount := outcount + 1; 
     END IF; 
    END LOOP; 
    END; 
END; 
/

SELECT * FROM TABLE(tmp_package.get_interesting_names(1)); 

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

+0

Обратите внимание, где встречается 'filter' - _before_. Вы не можете предположить, что '11' - это первая строка, которая будет проверяться на достоверность. – Mat

+0

@ Мат, так вот мой вопрос: как я могу убедить оракул сделать фильтр после сортировки? –

+1

Не оценивая предложение 'where' для каждой строки таблицы, как он узнает, что' Bravo' не будет отфильтрован? –

ответ

2

Последующие комментарии, поскольку это слишком велико. Запуск под 11.2.0.3 (OEL), ваш запрос:

SELECT * FROM (
    SELECT p.name, p.num 
    FROM tmp_table p 
    WHERE REGEXP_LIKE(tmp_fib(p.num), '(.)\1') 
    ORDER BY p.name 
) 
WHERE ROWNUM <= 1; 

NAME      NUM 
-------------------- ---------- 
Bravo      11 

Elapsed: 00:00:00.094 
Plan hash value: 1058933870 

---------------------------------------------------------------------------------- 
| Id | Operation   | Name  | Rows | Bytes | Cost (%CPU)| Time  | 
---------------------------------------------------------------------------------- 
| 0 | SELECT STATEMENT  |   |  1 | 25 |  4 (25)| 00:00:01 | 
|* 1 | COUNT STOPKEY  |   |  |  |   |   | 
|* 2 | VIEW    |   |  3 | 75 |  4 (25)| 00:00:01 | 
| 3 | SORT ORDER BY  |   |  3 | 75 |  4 (25)| 00:00:01 | 
| 4 |  TABLE ACCESS FULL| TMP_TABLE |  3 | 75 |  3 (0)| 00:00:01 | 
---------------------------------------------------------------------------------- 

Predicate Information (identified by operation id): 
--------------------------------------------------- 

    1 - filter(ROWNUM<=1) 
    2 - filter(REGEXP_LIKE (TO_CHAR("TMP_FIB"("NUM")),'(.)\1')) 

Note 
----- 
    - dynamic sampling used for this statement (level=2) 

Обратите внимание на изменение в SORT ORDER BY от того, что вы видели, и соответствующие rows значения. Перемещение заказа-на в суб-отборный больше похожи на твоем:

SELECT * FROM (
    SELECT * FROM (
    SELECT p.name, p.num 
    FROM tmp_table p 
    ORDER BY p.name 
) 
    WHERE REGEXP_LIKE(tmp_fib(num), '(.)\1') 
) 
WHERE ROWNUM <= 1; 

NAME      NUM 
-------------------- ---------- 
Bravo      11 

Elapsed: 00:00:07.894 
Plan hash value: 548796432 

------------------------------------------------------------------------------------- 
| Id | Operation    | Name  | Rows | Bytes | Cost (%CPU)| Time  | 
------------------------------------------------------------------------------------- 
| 0 | SELECT STATEMENT  |   |  1 | 25 | 171 (99)| 00:00:03 | 
|* 1 | COUNT STOPKEY   |   |  |  |   |   | 
| 2 | VIEW     |   |  1 | 25 | 171 (99)| 00:00:03 | 
|* 3 | SORT ORDER BY STOPKEY|   |  1 | 25 | 171 (99)| 00:00:03 | 
|* 4 |  TABLE ACCESS FULL | TMP_TABLE |  1 | 25 | 170 (99)| 00:00:03 | 
------------------------------------------------------------------------------------- 

Predicate Information (identified by operation id): 
--------------------------------------------------- 

    1 - filter(ROWNUM<=1) 
    3 - filter(ROWNUM<=1) 
    4 - filter(REGEXP_LIKE (TO_CHAR("TMP_FIB"("P"."NUM")),'(.)\1')) 

Note 
----- 
    - dynamic sampling used for this statement (level=2) 

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

CREATE INDEX tmp_index ON tmp_table(name, num); 

index TMP_INDEX created. 

SELECT * FROM (
    SELECT p.name, p.num 
    FROM tmp_table p 
    WHERE REGEXP_LIKE(tmp_fib(p.num), '(.)\1') 
    ORDER BY p.name 
) 
WHERE ROWNUM <= 1; 

NAME      NUM 
-------------------- ---------- 
Bravo      11 

Elapsed: 00:00:00.093 
Plan hash value: 1841475998 

------------------------------------------------------------------------------- 
| Id | Operation   | Name  | Rows | Bytes | Cost (%CPU)| Time  | 
------------------------------------------------------------------------------- 
| 0 | SELECT STATEMENT |   |  1 | 25 |  1 (0)| 00:00:01 | 
|* 1 | COUNT STOPKEY |   |  |  |   |   | 
|* 2 | VIEW   |   |  3 | 75 |  1 (0)| 00:00:01 | 
| 3 | INDEX FULL SCAN| TMP_INDEX |  3 | 75 |  1 (0)| 00:00:01 | 
------------------------------------------------------------------------------- 

Predicate Information (identified by operation id): 
--------------------------------------------------- 

    1 - filter(ROWNUM<=1) 
    2 - filter(REGEXP_LIKE (TO_CHAR("TMP_FIB"("NUM")),'(.)\1')) 

Note 
----- 
    - dynamic sampling used for this statement (level=2) 

SELECT * FROM (
    SELECT * FROM (
    SELECT p.name, p.num 
    FROM tmp_table p 
    ORDER BY p.name 
) 
    WHERE REGEXP_LIKE(tmp_fib(num), '(.)\1') 
) 
WHERE ROWNUM <= 1; 

NAME      NUM 
-------------------- ---------- 
Bravo      11 

Elapsed: 00:00:00.093 
Plan hash value: 1841475998 

------------------------------------------------------------------------------- 
| Id | Operation   | Name  | Rows | Bytes | Cost (%CPU)| Time  | 
------------------------------------------------------------------------------- 
| 0 | SELECT STATEMENT |   |  1 | 25 |  1 (0)| 00:00:01 | 
|* 1 | COUNT STOPKEY |   |  |  |   |   | 
| 2 | VIEW   |   |  1 | 25 |  1 (0)| 00:00:01 | 
|* 3 | INDEX FULL SCAN| TMP_INDEX |  1 | 25 |  1 (0)| 00:00:01 | 
------------------------------------------------------------------------------- 

Predicate Information (identified by operation id): 
--------------------------------------------------- 

    1 - filter(ROWNUM<=1) 
    3 - filter(REGEXP_LIKE (TO_CHAR("TMP_FIB"("P"."NUM")),'(.)\1')) 

Note 
----- 
    - dynamic sampling used for this statement (level=2) 

Кстати, aftr я запускаю это бросило несколько раз с каким-либо из rownum варианты В конечном итоге я начинаю получать ORA-01000: maximum open cursors exceeded ошибок. Я бросаю объекты в конце каждого прогона, но остаюсь на связи. Я думаю, что там где-то есть другая ошибка, хотя, вероятно, она не связана с тем, что вы видите, так как это происходит даже при сканировании индекса.

+0

Похоже, вы получаете оптимизацию, которую я хочу, фильтр перемещается вверх над сортировкой. Интересно, есть ли разница между 11.2.0.2 и 11.2.0.3 –

+0

Возможно, оптимизатор настроился на патч-набор, да. Я надеялся, что смогу придумать подсказку, которая заставит мой запрос вести себя как ваш в моем env и, таким образом, может заставить вас вести себя так, как вы хотите, но пока ничего не придумали. –

+0

Я подозреваю, что вы хотите применить это поведение с подсказкой оптимизатора NO_PUSH_PRED. –

1

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

  1. модернизация - новый Oracle, похоже, лучше оптимизирует этот тип запросов.
  2. Используйте подсказку INDEX, чтобы внутренний запрос извлекал строки в уже отсортированном порядке, что позволяет STOPKEY работать правильно.
  3. переписать в PL/SQL, с внутренним запросом в качестве курсора. выберите из курсора, пока не получите достаточное количество совпадений, а затем закройте его.

 Смежные вопросы

  • Нет связанных вопросов^_^