Понимание характеристик запроса, для которого индекс имеет существенное значение - PullRequest
0 голосов
/ 13 мая 2018

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

Поскольку я понял, что для индекса лучше иметь большую таблицу, я написал следующий скрипт (с использованием Oracle 11g Express):

CREATE TABLE many_students (
  student_id NUMBER(11),
  city       VARCHAR(20)
);

DECLARE
  nStudents    NUMBER := 1000000;
  nCities      NUMBER := 10000;
  curCity      VARCHAR(20);
BEGIN
  FOR i IN 1 .. nStudents LOOP
    curCity := ROUND(DBMS_RANDOM.VALUE()*nCities, 0) || ' City';
    INSERT INTO many_students
    VALUES (i, curCity);
  END LOOP;
  COMMIT;
END;

Затем я попробовал довольно много запросов, таких как:

select count(*) 
from many_students M 
where M.city = '5467 City'; 

и

select count(*) 
from many_students M1
join many_students M2 using(city);

и несколько других.

Я видел эту запись и думаю, что мои запросы удовлетворяют требованиям, изложенным в ответах там. Однако ни один из запросов, которые я пробовал, не показал существенного улучшения после построения индекса: create index myindex on many_students(city);

Мне не хватает какой-либо характеристики, которая отличает запрос, для которого индекс имеет существенное значение? Что это?

Ответы [ 2 ]

0 голосов
/ 14 мая 2018

Тестовый пример - хорошее начало, но для получения заметной разницы в производительности нужно еще несколько вещей:

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

    Приведенный ниже скрипт удваивает размер таблицы, пока не достигнет 64 миллионов строк. Это займет около 20 минут на моей машине. (Чтобы сделать это быстрее, для больших размеров вы можете сделать таблицу nologging и добавить подсказку /*+ append */ к вставке.

    --Increase the table to 64 million rows.  This took 20 minutes on my machine.
    insert into many_students select * from many_students;
    insert into many_students select * from many_students;
    insert into many_students select * from many_students;
    insert into many_students select * from many_students;
    insert into many_students select * from many_students;
    insert into many_students select * from many_students;
    commit;
    
    --The table has about 1.375GB of data.  The actual size will vary.
    select bytes/1024/1024/1024 gb from dba_segments where segment_name = 'MANY_STUDENTS';
    
  2. Сбор статистики. Всегда собирайте статистику после больших изменений в таблице. Оптимизатор не может хорошо выполнять свою работу, если у него нет статистики таблиц, столбцов и индексов.

    begin
        dbms_stats.gather_table_stats(user, 'MANY_STUDENTS');
    end;
    /
    
  3. Используйте подсказки для навязывания хорошего и плохого плана. Обычно следует избегать подсказок оптимизатора. Но для быстрого сравнения разных планов они могут помочь исправить плохой план.

    Например, это приведет к полному сканированию таблицы:

    select /*+ full(M) */ count(*) from many_students M where M.city = '5467 City';
    

    Но вы также захотите проверить план выполнения:

    explain plan for select /*+ full(M) */ count(*) from many_students M where M.city = '5467 City';
    select * from table(dbms_xplan.display);
    
  4. Очистить кеш. Кэширование, вероятно, является основной причиной индекса и запросов на полное сканирование таблицы, занимающих одинаковое количество времени. Если таблица полностью умещается в памяти, тогда время чтения всех строк может быть слишком маленьким для измерения. Число может быть меньше времени для разбора запроса или для отправки простого результата по сети.

    Эта команда заставит Oracle удалить почти все из буферного кеша. Это поможет вам проверить «холодную» систему. (Возможно, вы не хотите запускать этот оператор в производственной системе.)

    alter system flush buffer_cache;
    

    Однако это не очистит операционную систему или кэш SAN. И, может быть, стол действительно уместился в памяти на производстве. Если вам нужно протестировать быстрый запрос, может потребоваться поместить его в цикл PL / SQL.

  5. Несколько чередующихся прогонов. В фоновом режиме происходит много вещей, таких как кэширование и другие процессы. Так легко получить плохие результаты, потому что что-то не связанное изменилось в системе.

    Может быть, первый запуск занимает слишком много времени, чтобы поместить вещи в кеш. Или, может быть, какая-то огромная работа была начата между запросами. Чтобы избежать этих проблем, поочередно выполните два запроса. Запустите их пять раз, выбросьте максимумы и минимумы и сравните средние значения.

    Например, скопируйте и вставьте приведенные ниже операторы пять раз и запустите их. (При использовании SQL * Plus сначала запустите set timing on.) Я уже сделал это и разместил время, которое я получил, в комментарии перед каждой строкой.

    --Seconds: 0.02, 0.02, 0.03, 0.234, 0.02
    alter system flush buffer_cache;
    select count(*) from many_students M where M.city = '5467 City';
    
    --Seconds: 4.07, 4.21, 4.35, 3.629, 3.54
    alter system flush buffer_cache;
    select /*+ full(M) */ count(*) from many_students M where M.city = '5467 City';
    
  6. Тестирование сложно. Сложно собрать тесты достойной производительности. Приведенные выше правила являются только началом.

    Поначалу это может показаться излишним. Но это сложная тема. И я видел, как многие люди, в том числе и я, тратили много времени на «настройку» чего-либо на основе плохого теста. Лучше потратить дополнительное время сейчас и получить правильный ответ.

0 голосов
/ 14 мая 2018

Индекс действительно светится, когда базе данных не нужно переходить к каждой строке в таблице, чтобы получить ваши результаты.Так что COUNT(*) не лучший пример.Возьмем для примера:

alter session set statistics_level = 'ALL';
create table mytable as select * from all_objects;
select * from mytable where owner = 'SYS' and object_name = 'DUAL';

---------------------------------------------------------------------------------------
| Id  | Operation         | Name    | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
---------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |         |      1 |        |    300 |00:00:00.01 |      12 |
|   1 |  TABLE ACCESS FULL| MYTABLE |      1 |  19721 |    300 |00:00:00.01 |      12 |
---------------------------------------------------------------------------------------

Итак, здесь база данных выполняет полное сканирование таблицы (TABLE ACCESS FULL), что означает, что она должна посещать каждую строку в базе данных, что означает, что она должна загружать всеблок с диска.Много ввода / вывода.Оптимизатор догадался, что он найдет 15000 строк, но я знаю, что есть только одна.

Сравните это с этим:

create index myindex on mytable( owner, object_name );
select * from mytable where owner = 'SYS' and object_name = 'JOB$';
select * from table( dbms_xplan.display_cursor( null, null, 'ALLSTATS LAST' ));

----------------------------------------------------------------------------------------------------------
| Id  | Operation                   | Name    | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
----------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |         |      1 |        |      1 |00:00:00.01 |       3 |      2 |
|   1 |  TABLE ACCESS BY INDEX ROWID| MYTABLE |      1 |      2 |      1 |00:00:00.01 |       3 |      2 |
|*  2 |   INDEX RANGE SCAN          | MYINDEX |      1 |      1 |      1 |00:00:00.01 |       2 |      2 |
----------------------------------------------------------------------------------------------------------

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

   2 - access("OWNER"='SYS' AND "OBJECT_NAME"='JOB$')

Здесь, потому что есть индекс, он делает INDEX RANGE SCAN, чтобы найти значения rowid для таблицы, которые соответствуют нашим критериям.Затем он переходит к самой таблице (TABLE ACCESS BY INDEX ROWID) и ищет только те строки, которые нам нужны, и может сделать это эффективно, потому что у него есть rowid.

И даже лучше, если вам что-то нужно искатьэто полностью в индексе, сканирование даже не должно возвращаться к базовой таблице.Индекса достаточно:

select count(*) from mytable where owner = 'SYS';
select * from table( dbms_xplan.display_cursor( null, null, 'ALLSTATS LAST' ));

------------------------------------------------------------------------------------------------
| Id  | Operation         | Name    | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |         |      1 |        |      1 |00:00:00.01 |      46 |     46 |
|   1 |  SORT AGGREGATE   |         |      1 |      1 |      1 |00:00:00.01 |      46 |     46 |
|*  2 |   INDEX RANGE SCAN| MYINDEX |      1 |   8666 |   9294 |00:00:00.01 |      46 |     46 |
------------------------------------------------------------------------------------------------

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

   2 - access("OWNER"='SYS')

Поскольку мой запрос включал столбец владельца и он содержится в индексе, ему никогда не нужно возвращаться к базовой таблице, чтобы что-то там искать.Таким образом, сканирования индекса достаточно, затем выполняется агрегирование для подсчета строк.Этот сценарий несколько менее идеален, потому что индекс включен (владелец, имя_объекта), а не только владелец, но это определенно лучше, чем полное сканирование таблицы на главной таблице.

...