Почему этот запрос выполняет полное сканирование таблицы?

Запрос:

SELECT tbl1.*
   FROM tbl1 
JOIN tbl2
     ON (tbl1.t1_pk  = tbl2.t2_fk_t1_pk
AND tbl2.t2_strt_dt <= sysdate
AND tbl2.t2_end_dt  >= sysdate)
JOIN tbl3 on (tbl3.t3_pk = tbl2.t2_fk_t3_pk
AND tbl3.t3_lkup_1 = 2577304
AND tbl3.t3_lkup_2 = 1220833)
where tbl2.t2_lkup_1   = 1020000002981587;

Факты:

  • Oracle XE
  • tbl1.t1_pk - первичный ключ.
  • tbl2.t2_fk_t1_pk - это внешний ключ для этого столбца t1_pk.
  • tbl2.t2_lkup_1 проиндексирован.
  • tbl3.t3_pk - первичный ключ.
  • tbl2.t2_fk_t3_pk - это внешний ключ для этого столбца t3_pk.

План объяснения базы данных с 11 000 строками в tbl1 и 3500 строками в tbl2 показывает, что выполняется полное сканирование таблицы на tbl1. Мне кажется, что было бы быстрее, если бы он мог выполнять индексный запрос на tbl1.

План объяснения базы данных с 11 000 строками в tbl1 и 3500 строками в tbl2 показывает, что выполняется полное сканирование таблицы на tbl1. Мне кажется, что было бы быстрее, если бы он мог выполнять индексный запрос на tbl1.

Обновление: я попробовал подсказку, которую предложили некоторые из вас, и стоимость объяснения стала намного хуже! Теперь я действительно в замешательстве.

Дальнейшее обновление: я наконец получил доступ к копии производственной базы данных, и «план объяснения» показал ее с использованием индексов и с гораздо более дешевым запросом. Я предполагаю, что наличие большего количества данных (более 100 000 строк в tbl1 и 50 000 строк в tbl2) было тем, что потребовалось, чтобы заставить его решить, что индексы того стоят. Спасибо всем, кто помогал. Я все еще считаю настройку производительности Oracle черным искусством, но я рад, что некоторые из вас это понимают.

Дальнейшее обновление: я обновил вопрос по просьбе моего бывшего работодателя. Им не нравится, что их имена таблиц отображаются в запросах Google. Я должен был знать лучше.

Ответов (8)

Решение

Было бы полезно увидеть оценки количества строк оптимизатора, которых нет в опубликованных вами выходных данных SQL Developer.

Я отмечаю, что он выполняет два поиска по индексу: RANGE SCAN, а не UNIQUE SCAN. Таким образом, его оценки количества возвращаемых строк могут быть очень далекими (вне зависимости от того, актуальна статистика или нет).

Я предполагаю, что его оценка окончательного количества строк из TABLE ACCESS TBL2 довольно высока, поэтому он думает, что найдет большое количество совпадений в TBL1, и поэтому решает выполнить полное сканирование / хеш-соединение, а не вложенное циклическое / индексное сканирование.

Для настоящего развлечения вы можете запустить запрос с включенным событием 10053 и получить трассировку, показывающую вычисления, выполненные оптимизатором.

Вы можете сказать это, только посмотрев на план запроса, созданный оптимизатором / исполнителем SQL. Он будет, по крайней мере, частичным, на основе статистики индекса, которую нельзя предсказать только на основе определения (и, следовательно, может измениться со временем).

Студия управления SQL для SQL Server 2005/2008, анализатор запросов для более ранних версий.

(Не могу вспомнить правильные названия инструментов для Oracle.)

Попробуйте добавить подсказку по индексу.

SELECT /*+ index(tbl1 tbl1_index_name) */ .....

Иногда Oracle просто не знает, какой индекс использовать.

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

Здесь на помощь приходит статистика. Допустим, ваш запрос должен вернуть 10 записей. Оптимизатор может рассчитать, что использование индекса потребует 10 вызовов ввода-вывода. Допустим, ваша таблица, согласно статистике по ней, находится в 6 блоках в файле данных. Oracle будет быстрее выполнить полное сканирование (6 операций ввода-вывода), затем прочитать индекс, прочитать таблицу, прочитать, а затем проиндексировать для следующего совпадающего ключа, прочитать таблицу и так далее.

Так что в вашем случае стол может быть очень маленьким. Статистика может быть отключена.

Я использую следующее, чтобы собирать статистику и настраивать ее под свои нужды:

begin

 DBMS_STATS.GATHER_TABLE_STATS(ownname
=> '&owner' ,tabname => '&table_name', estimate_percent => dbms_stats.AUTO_SAMPLE_SIZE,granularity
=> 'ALL', cascade  => TRUE); 

 -- DBMS_STATS.GATHER_TABLE_STATS(ownname
=> '&owner' ,tabname => '&table_name',partname => '&partion_name',granularity => 'PARTITION', estimate_percent => dbms_stats.AUTO_SAMPLE_SIZE, cascade 
=> TRUE);

 -- DBMS_STATS.GATHER_TABLE_STATS(ownname
=> '&owner' ,tabname => '&table_name',partname => '&partion_name',granularity => 'PARTITION', estimate_percent => dbms_stats.AUTO_SAMPLE_SIZE, cascade 
=> TRUE,method_opt  => 'for all indexed columns size 254');

end;

Простой ответ: поскольку оптимизатор ожидает, что будет найдено больше строк, он действительно найдет.

Проверьте статистику, актуальна ли она? Проверьте ожидаемую мощность в плане объяснения, соответствуют ли они фактическим результатам? Если нет, исправьте статистику, относящуюся к этому шагу.

Могут помочь гистограммы для объединенных столбцов. Oracle будет использовать их для оценки количества элементов в результате соединения.

Конечно, вы всегда можете принудительно использовать индекс с помощью подсказки

По-видимому, этот запрос дает тот же план:

SELECT tbl1.*   
FROM tbl1 
JOIN tbl2 ON (tbl1.t1_pk  = tbl2.t2_fk_t1_pk)
JOIN tbl3 on (tbl3.t3_pk = tbl2.t2_fk_t3_pk)
where tbl2.t2_lkup_1   = 1020000002981587
AND tbl2.t2_strt_dt <= sysdate
AND tbl2.t2_end_dt  >= sysdate
AND tbl3.t3_lkup_1 = 2577304
AND tbl3.t3_lkup_2 = 1220833;

Что произойдет, если вы перепишете этот запрос на:

SELECT tbl1.*    
FROM  tbl1 
,     tbl2
,     tbl3  
where tbl2.t2_lkup_1   = 1020000002981587 
AND   tbl1.t1_pk  = tbl2.t2_fk_t1_pk 
AND   tbl3.t3_pk = tbl2.t2_fk_t3_pk 
AND   tbl2.t2_strt_dt <= sysdate 
AND   tbl2.t2_end_dt  >= sysdate 
AND   tbl3.t3_lkup_1 = 2577304 
AND   tbl3.t3_lkup_2 = 1220833;

Похоже, что индекс для таблицы tbl1 не подбирается. Убедитесь, что у вас есть индекс для столбца t2_lkup_1, и он не должен быть многоколоночным, иначе индекс не применим.

(в дополнение к комментарию Мэтта) Из вашего запроса я считаю, что вы присоединяетесь, потому что хотите отфильтровать записи, чтобы не выполнять JOIN, что может увеличить количество элементов для набора результатов из таблицы tbl1, если есть повторяющиеся совпадения из. См. Комментарий Джеффа Этвуда

Попробуйте это, в котором используется существующая функция и соединение (что очень быстро на оракуле)

Выбрать *
  из табл1 
 где tbl2.t2_lkup_1 = 1020000002981587 и
       существуют (
         Выбрать *
           из табл2, табл3 
          где tbl2.t2_fk_t1_pk = tbl1.t1_pk и
                tbl2.t2_fk_t3_pk = tbl3.t3_pk и
                sysdate между tbl2.t2_strt_dt и tbl2.t2_end_dt и
                tbl3.t3_lkup_1 = 2577304 и
                tbl3.t3_lkup_2 = 1220833);

В зависимости от ожидаемого размера результата вы можете поиграть с некоторыми параметрами сеанса:

SHOW PARAMETER optimizer_index_cost_adj;
[...]
ALTER SESSION SET optimizer_index_cost_adj = 10;

SHOW PARAMETER OPTIMIZER_MODE;
[...]
ALTER SESSION SET OPTIMIZER_MODE=FIRST_ROWS_100;

и не забывайте проверять реальное время выполнения, иногда план не соответствует реальному миру;)