Объясните план: выберите из таблицы 100 000 записей по индексированному полю varchar2, не используя индекс

Я согласен с Томом Китом в том, что полное сканирование таблицы не является злом источник, но только если таблица относительно небольшая. Таким образом, наличие дополнительного индекса такой таблицы избыточно. Тем не менее, таблицу со 100 000 записей не следует считать маленькой, но план объяснения из такой таблицы показывает выполненное полное сканирование таблицы. Итак, я провел небольшой эксперимент на своем ноутбуке с локально установленным Oracle:

1) сначала создал my_table:

CREATE TABLE my_table(
  "ID" NUMBER NOT NULL ENABLE, 
  "INVOICE_NO" VARCHAR2(10), 
  CONSTRAINT "test _PK" PRIMARY KEY ("ID")
)

2) Затем создал индекс для столбца invoice_no (потому что будет фильтровать его):

CREATE INDEX "my_table_index1" ON my_table (invoice_no)

3) Затем вставил 100 тыс. записей:

DECLARE
  mod_val NUMBER;
BEGIN
  FOR i IN 1..100000 LOOP
    mod_val := MOD(i,6);
    IF (mod_val = 0) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-110');
    ELSIF (mod_val = 1) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-111');
    ELSIF (mod_val = 2) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-112');
    ELSIF (mod_val = 3) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-113');
    ELSIF (mod_val = 4) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-114');
    ELSIF (mod_val = 5) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-115');
    END IF; 
  END LOOP;
  COMMIT;
END;

4) Затем обновил одну случайную запись (только для того, чтобы подчеркнуть выбор):

BEGIN
  UPDATE my_table SET INVOICENO = 'exception' WHERE id = 50000;
  COMMIT;
END;

5) Затем выполняется выбор с планом объяснения:

EXPLAIN PLAN FOR
  SELECT * FROM my_table WHERE invoice_no = 'exception';

6) Затем схватил статистику:

 SELECT * FROM TABLE(dbms_xplan.display);

7) и получил результаты:

"PLAN_TABLE_OUTPUT"
"Plan hash value: 3804444429"
" "
"------------------------------------------------------------------------------"
"| Id  | Operation         | Name     | Rows  | Bytes | Cost (%CPU)| Time     |"
"------------------------------------------------------------------------------"
"|   0 | SELECT STATEMENT  |          | 83256 |  1626K|   103   (1)| 00:00:02 |"
"|   1 |  TABLE ACCESS FULL| MY_TABLE | 83256 |  1626K|   103   (1)| 00:00:02 |"
"------------------------------------------------------------------------------"
" "
"Note"
"-----"
"   - dynamic sampling used for this statement (level=2)"

Вывод: Странно и пахнет "волшебством", почему Oracle решил не использовать индекс для поля invoice_no и просканировал 83256 записей? Я согласен, что мой ноутбук не перегружен одновременными пользователями, таблица не очень большого размера (содержат числа и varchars), однако мне не нравится эта магия, и я хотел бы знать причины такого поведения :)

<Сильного> UPDATE : Я просто добавил некоторое значение манекена (см ниже) invoice_no поля для всех записей - просто увеличить размер таблицы, однако таблицы полных остатков сканирования: «aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa»

ОБНОВЛЕНИЕ 2: я также выполнил анализ таблицы, но результат тот же:

ANALYZE TABLE my_table COMPUTE STATISTICS;

UPDATE3: пытался принудительно использовать индекс, но результат тот же (может быть, неправильный синтаксис?):

EXPLAIN PLAN FOR 
  SELECT /*+ INDEX(my_table my_table_index1) */ * FROM my_table t WHERE invoice_no = 'exception'

UPDATE4: Наконец-то удалось «сказать Oracle» использовать индекс — выполнить новую процедуру сбора статистики таблицы:

BEGIN
  DBMS_STATS.GATHER_TABLE_STATS ( OWNNAME=>user
                             , TABNAME=>'my_table');
END;

Вот результат объяснения плана:

"--------------------------------------------------------------------------------------"
"| Id  | Operation                   | Name            | Rows  | Bytes | Cost (%CPU)| Time     |"
"-----------------------------------------------------------------------------------------------"
"|   0 | SELECT STATEMENT            |                 |     1 |   294 |     5   (0)| 00:00:01 |"
"|   1 |  TABLE ACCESS BY INDEX ROWID| MY_TABLE        |     1 |   294 |     5   (0)| 00:00:01 |"
"|*  2 |   INDEX RANGE SCAN          | my_table_index1 |     1 |       |     4   (0)| 00:00:01 |"
"-----------------------------------------------------------------------------------------------"
" "
"Predicate Information (identified by operation id):"
"---------------------------------------------------"
" "
"   2 - access(""INVOICE_NO""='exception')"

Итак, похоже, что Oracle решает использовать какой-то подход к запросам в какой-то момент и не обновляет его, даже если ситуация изменилась. Я согласен с этим, но странно, почему он не выбрал правильный подход для этого тестового примера, когда я только что создал, вставил и выполнил выбор. Должны ли мы всегда выполнять DBMS_STATS.GATHER_TABLE_STATS, по крайней мере, в начале, чтобы указать Oracle использовать лучший подход к запросам?


person Centurion    schedule 14.10.2012    source источник
comment
Небольшое примечание: не используйте ANALYZE TABLE для сбора статистики. Он устарел и больше не должен использоваться. Вместо этого используйте dbms_stats.gather_table_stats().   -  person a_horse_with_no_name    schedule 15.10.2012
comment
В подсказке уберите пробел между /* и +. то есть: SELECT /*+ индекс   -  person cagcowboy    schedule 15.10.2012
comment
Спасибо, исправил, но результат тот же.   -  person Centurion    schedule 15.10.2012


Ответы (3)


Когда вы изначально создаете таблицу, для INVOICE_NO существует только 7 различных значений. Таким образом, по умолчанию Oracle ожидает, что запрос к таблице, который просто указывает предикат на INVOICE_NO, вернет примерно 1 из каждых 7 строк (~ 14,3% строк), что обычно означает, что сканирование таблицы будет более длительным. эффективнее сканирования индекса (точная точка отсечки будет зависеть от ряда различных параметров — вполне возможно, что некоторые системы выберут сканирование индекса, если предполагается получить 15% строк).

Когда вы изначально запускали запрос, в таблице не было статистики, поэтому Oracle был вынужден выполнять динамическую выборку (обратите внимание на комментарий «динамическая выборка, используемая для этого оператора (уровень = 2)» в плане запроса). Это сделано для того, чтобы очень быстро собрать некоторую базовую статистику для оптимизатора. Однако динамическая выборка предназначена для оптимизации скорости, а не точности, поэтому качество статистики, как правило, ниже оптимального. В вашем первом примере Oracle оценивает, что запрос возвращает 83256 строк (83,2% от общего числа), что, вероятно, означает, что он переоценил количество строк в таблице и недооценил количество различных значений в столбце INVOICE_NO.

Если бы вы собирали статистику, используя

BEGIN
  DBMS_STATS.GATHER_TABLE_STATS ( OWNNAME=>user
                             , TABNAME=>'my_table');
END;

после шага 4, но до шага 5, при условии, что вы не изменили ни один из DBMS_STATS параметров по умолчанию, у вас была бы лучшая статистика, но вы все равно (скорее всего) провели бы сканирование таблицы. По оценкам Oracle, это 14286 строк (1 из 7 строк).

SQL> SELECT * FROM TABLE(dbms_xplan.display);

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
Plan hash value: 3804444429

------------------------------------------------------------------------------
| Id  | Operation         | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |          | 14286 |   195K|   104   (2)| 00:00:02 |
|*  1 |  TABLE ACCESS FULL| MY_TABLE | 14286 |   195K|   104   (2)| 00:00:02 |
------------------------------------------------------------------------------

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

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------

   1 - filter("INVOICE_NO"='exception')

Чтобы получить лучший план, вам нужно иметь гистограмму в столбце INVOICE_NO. Это сообщит Oracle, что данные в INVOIE_NO распределены неравномерно, поэтому некоторые значения (например, «исключение») гораздо более избирательны, чем другие столбцы. Когда вы собираете статистику, вы можете указать, что хотите собирать гистограммы по отдельным столбцам, по всем индексированным столбцам, или вы можете указать, что вы хотите, чтобы Oracle автоматически определял, для каких столбцов нужны гистограммы (мы вернемся к этому чуть позже). ). Если вы хотите, чтобы Oracle собирал гистограммы по всем проиндексированным столбцам,

SQL> exec dbms_stats.gather_table_stats( 'SCOTT', 
                                         'MY_TABLE', 
                                          method_opt => 'FOR ALL INDEXED COLUMNS SIZE 254' );

PL/SQL procedure successfully completed.

Предполагая, что существует 255 или меньше различных значений для INVOICE_NO, эта гистограмма позволит Oracle точно отслеживать, насколько часто встречается каждое уникальное значение (если имеется более 255 различных значений, Oracle потребуется объединить соседние значения, что может сделать ваши гистограммы менее точными). ).

В установке Oracle 10.2 или 11.2 по умолчанию параметр method_opt по умолчанию будет "ДЛЯ ВСЕХ РАЗМЕРОВ КОЛОНЦОВ АВТО". Это указывает Oracle собирать гистограммы для любых столбцов, которые, по его мнению, будут подходящими. Для этого Oracle ищет столбцы, в которых распределение данных сильно искажено, и где этот столбец появляется в предикатах. Итак, ранее, когда я говорил о сборе статистики между шагами 4 и 5, Oracle не собирал гистограмму для INVOICE_NO, потому что, хотя он знал, что данные искажены, он не знал, что вы собираетесь запрашивать таблицу на основе этого. столбец.

После шага 7, если вы снова собрали статистику с помощью той же команды

BEGIN
  DBMS_STATS.GATHER_TABLE_STATS ( OWNNAME=>user
                             , TABNAME=>'my_table');
END;

Затем Oracle увидит, что есть запрос к MY_TABLE, который имеет предикат для INVOICE_ID в общем пуле. Это позволит ему понять, что INVOICE_NO соответствует обоим условиям для получения гистограммы, поэтому на этот раз он соберет гистограмму для INVOICE_NO. Это позволяет оптимизатору понять, что ваш запрос возвращает только 1 строку, и понять, что сканирование индекса будет наиболее эффективным планом.

SQL> SELECT * FROM TABLE(dbms_xplan.display);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 3377519735

-----------------------------------------------------------------------------------------------
| Id  | Operation                   | Name            | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |                 |     1 |    14 |     4   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| MY_TABLE        |     1 |    14 |     4   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN          | my_table_index1 |     1 |       |     3   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------

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

   2 - access("INVOICE_NO"='exception')

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

В реальной системе в подавляющем большинстве запросов вы, как правило, будете использовать переменные связывания, а не литералы. Когда вы используете переменные связывания в запросах к столбцам с гистограммами, вы создаете новый набор проблем. Если у вас есть запрос в приложении

 SELECT * 
   FROM my_table 
  WHERE invoice_no = :1;

вам понадобится сканирование таблицы, если вы привязываете значение «5570-110», но вам нужно сканирование индекса, если вы привязываете значение «исключение». В Oracle 10.2 Oracle будет выполнять просмотр переменных связывания, что означает, что когда Oracle выполняет жесткий синтаксический анализ, он будет просматривать значение переменной связывания и генерировать план, который оптимизируется для этого значения связывания. К сожалению, в 10g у вас может быть только один план для каждого запроса, поэтому вы получите оптимальный план только для одного из двух случаев за раз, и какой план вы получите, будет зависеть от того, какое значение привязки было встречено первым. В 11g вы можете получить адаптивное совместное использование курсора, когда Oracle поддерживает несколько планов запросов для разных значений переменных связывания, хотя это вносит дополнительную сложность, о которой вам нужно знать.

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

SELECT /*+ INDEX(t "my_table_index1") */ * 
  FROM my_table t 
 WHERE invoice_no = 'exception'

Это одна из (многих) причин, по которой использование идентификаторов с учетом регистра, как правило, является серьезной проблемой.

person Justin Cave    schedule 15.10.2012

Должны ли мы всегда выполнять DBMS_STATS.GATHER_TABLE_STATS по крайней мере в начале, чтобы указать Oracle использовать лучший подход к запросам?

Нет, не обязательно.

Oracle сделает это автоматически (если вы не отключили эту функцию). Но при установке по умолчанию он будет собирать статистику только один раз в день. Поэтому сразу после большой загрузки статистика не актуальна, хотя я ожидал, что 11.x будет использовать индекс сразу после заполнения таблицы.

Поэтому каждый раз, когда вы изменяете значительную часть данных, рекомендуется запускать dbms_stats.gather_table_stats() или даже dbms_stats.gather_schema_stats(), если вы изменили больше таблиц.

Стратегия «один раз в день» работает достаточно хорошо для большинства рабочих нагрузок, но если у вас быстро меняющиеся условия, вы можете настроить параметры того, как Oracle рассчитывает статистику.

Дополнительные сведения см. в руководстве: http://docs.oracle.com/cd/E11882_01/server.112/e16638/stats.htm#g49431

person a_horse_with_no_name    schedule 14.10.2012

Я получил это с вашим тестом, без каких-либо обновлений статистики: План:

SELECT STATEMENT ALL_ROWS: Стоимость: 1 Байт: 20 Мощность: 1

TABLE ACCESS BY INDEX ROWID TABLE SYS.MY_TABLE: Стоимость: 1 Байт: 20 Мощность: 1

INDEX RANGE SCAN INDEX SYS.my_table_index1: Стоимость: 1 Мощность: 1

person OldProgrammer    schedule 14.10.2012