#oracle #indexing #to-date
#Oracle #индексирование #на сегодняшний день
Вопрос:
Я использую индексированный столбец, используемый в качестве фильтра, помещая его «между» двумя литеральными значениями. (Столбец находится на второй позиции индекса и фактически замедляет выполнение; я разберусь с этим позже).
Что меня смущает, так это то, что Oracle (11.2.0.3.0) использует или игнорирует указанный индекс в зависимости от формата строк значения и формата, предоставленных to_date:
При этом игнорируется индекс:
SQL> SELECT *
2 FROM gprs_history_import gh
3 WHERE start_call_date_time BETWEEN
4 to_date('20140610 000000','yyyymmdd hh24miss') AND
5 to_date('20140610 235959','yyyymmdd hh24miss')
6 /
Execution Plan
----------------------------------------------------------
Plan hash value: 990804809
--------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop |
--------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 350 | 219K| 242K (1)| 00:56:42 | | |
| 1 | PARTITION RANGE SINGLE| | 350 | 219K| 242K (1)| 00:56:42 | 74 | 74 |
| 2 | PARTITION LIST ALL | | 350 | 219K| 242K (1)| 00:56:42 | 1 | 3 |
|* 3 | TABLE ACCESS FULL | GPRS_HISTORY_IMPORT | 350 | 219K| 242K (1)| 00:56:42 | 220 | 222 |
--------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - filter("START_CALL_DATE_TIME"<=TO_DATE(' 2014-06-10 23:59:59', 'syyyy-mm-dd hh24:mi:ss'))
В этом используется индекс (обратите внимание на пробел после части даты в строке 4):
SQL> SELECT *
2 FROM gprs_history_import gh
3 WHERE start_call_date_time BETWEEN
4 to_date('20140610 ','yyyymmdd ') AND
5 to_date('20140610 235959','yyyymmdd hh24miss')
6 /
Execution Plan
----------------------------------------------------------
Plan hash value: 464458373
---------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop |
---------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 350 | 219K| 2795K (1)| 10:52:15 | | |
|* 1 | FILTER | | | | | | | |
| 2 | PARTITION RANGE ITERATOR | | 350 | 219K| 2795K (1)| 10:52:15 | KEY | 74 |
| 3 | PARTITION LIST ALL | | 350 | 219K| 2795K (1)| 10:52:15 | 1 | 3 |
| 4 | TABLE ACCESS BY LOCAL INDEX ROWID| GPRS_HISTORY_IMPORT | 350 | 219K| 2795K (1)| 10:52:15 | KEY | 222 |
|* 5 | INDEX SKIP SCAN | GPRS_HISTORY_IMPORT_IDX1 | 1 | | 2795K (1)| 10:52:15 | KEY | 222 |
---------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(TO_DATE('20140610 ','yyyymmdd ')<=TO_DATE(' 2014-06-10 23:59:59', 'syyyy-mm-dd hh24:mi:ss'))
5 - access("START_CALL_DATE_TIME">=TO_DATE('20140610 ','yyyymmdd ') AND "START_CALL_DATE_TIME"<=TO_DATE(' 2014-06-10
23:59:59', 'syyyy-mm-dd hh24:mi:ss'))
filter("START_CALL_DATE_TIME">=TO_DATE('20140610 ','yyyymmdd ') AND "START_CALL_DATE_TIME"<=TO_DATE(' 2014-06-10
23:59:59', 'syyyy-mm-dd hh24:mi:ss'))
(Фильтр в (1) кажется немного глупым, как будто Oracle не поняла выражение)
Опять же, это не так (я удалил конечный пробел):
SQL> SELECT *
2 FROM gprs_history_import gh
3 WHERE start_call_date_time BETWEEN
4 to_date('20140610','yyyymmdd') AND
5 to_date('20140610 235959','yyyymmdd hh24miss')
6 /
Execution Plan
----------------------------------------------------------
Plan hash value: 990804809
--------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop |
--------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 350 | 219K| 242K (1)| 00:56:42 | | |
| 1 | PARTITION RANGE SINGLE| | 350 | 219K| 242K (1)| 00:56:42 | 74 | 74 |
| 2 | PARTITION LIST ALL | | 350 | 219K| 242K (1)| 00:56:42 | 1 | 3 |
|* 3 | TABLE ACCESS FULL | GPRS_HISTORY_IMPORT | 350 | 219K| 242K (1)| 00:56:42 | 220 | 222 |
--------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - filter("START_CALL_DATE_TIME"<=TO_DATE(' 2014-06-10 23:59:59', 'syyyy-mm-dd hh24:mi:ss'))
Помещение кавычек вокруг пробела исключает возможность использования индекса.
Что дает?
Комментарии:
1. dba.stackexchange.com был бы лучший сайт для рассмотрения этой темы, где вы могли бы получить подробное и более точное решение. Пометка для переноса этой проблемы.
2. @Rachcha Когда новый пользователь задает интересный вопрос, который хорошо подходит для этого сайта, мы все должны радоваться, а не пытаться переместить его куда-то еще. Кроме того, очень немногие администраторы баз данных знают, как решать подобные проблемы.
3. @jonearles: Возможно, вы правы. Я просто подумал, что OP получит лучший ответ, просто мое восприятие.
4. На что разделена таблица? Предположительно диапазон дат, но какая степень детализации?
5. Если таблица содержит CDR, я предполагаю, что у нее может быть высокий оборот, и записи за определенный день могут отсутствовать недолго, но и статистика может не успевать? Интересно, что индексная версия имеет более высокую стоимость, а неиндексная не фильтрует нижнюю границу даты. Были ли они впервые запущены в разное время и были ли записи в 2014-06-10 и ранее, когда каждая из них подвергалась жесткому анализу? Интересно, может ли фактический формат, который вы используете, быть неактуальным, это просто разные запросы, анализируемые с разными данными / статистикой, и каждый из них выглядел оптимальным на тот момент времени?
Ответ №1:
Хорошо — я попробую, в основном это вычет из доступной информации:
Почему Oracle выбирает другой план выполнения?
Похоже, что в вашем втором запросе с необычным форматом даты оптимизатор понятия не имеет, каково значение результирующей даты. Вы видите предикат фильтра:
1 - filter(TO_DATE('20140610 ','yyyymmdd ')<=TO_DATE(' 2014-06-10 23:59:59', 'syyyy-mm-dd hh24:mi:ss'))
Это означает, что оптимизатор даже не уверен, что первая дата меньше второй! Это означает, что оптимизатор не имеет представления о количестве возвращаемых строк и будет просто использовать общий план без учета конкретной статистики. Было бы то же самое, если бы у вас была определенная пользователем функция xyt(), которая возвращала бы дату для диапазона. Оптимизатор не имеет возможности узнать, какое значение даты приведет — это означает, что вы получаете общий универсальный план, который должен быть довольно приличным для любого указанного диапазона дат.
В первом и третьем случаях оптимизатор, похоже, понимает дату напрямую и может угадать количество строк, которые находятся в диапазоне дат, используя статистику. Итак, в то время как второй запрос был к оптимизатору BETWEEN X AND 3
, подобный этому запросу BETWEEN 1 AND 3
, он оптимизирует план запроса для прогнозируемого количества возвращаемых строк!
Кажется странным, что оптимизатор запросов имеет такие проблемы со странным форматом даты, что может быть подано как ошибка / запрос на улучшение…
Но важный момент:
- Полное сканирование таблицы не обязательно должно быть ПЛОХИМ планом… А также использование индекса не всегда быстрее!
- Стоимость в плане запроса никоим образом не связана напрямую с фактическим временем выполнения или производительностью — это внутреннее измерение для сравнения разных планов для ОДНОГО и ТОГО ЖЕ ЗАПРОСА (поэтому вы не можете сравнивать стоимость разных запросов, таких как ваши запросы 1,2 и 3)
В принципе, если вы возвращаете большое количество строк из таблицы, полное сканирование таблицы без доступа к индексу во многих случаях будет намного быстрее, особенно при работе с определенными разделами! — Сканирование таблицы будет получать доступ только к заголовку для соответствующего диапазона дат — то есть только для рассматриваемой даты и возвращает все строки из этого раздела. Это намного быстрее, чем запрашивать индекс для каждой отдельной строки, а затем извлекать строку с помощью индексного доступа… Попробуйте профилировать запросы — полное сканирование таблицы в разделе должно быть в 3 раза быстрее с гораздо меньшим вводом-выводом
Комментарии:
1. Я согласен с тем, что Oracle, вероятно, не понимает формат во время оптимизации во 2-м запросе. Возможно, вас смущает формат с буквенным пробелом в конце. Я рад, что моя идея не совсем сумасшедшая. (продолжение)
2. Сканирование с пропуском индекса — ужасный выбор, потому что столбец даты является вторым в индексе после другого столбца с несколькими миллионами значений, и для каждого значения 1-го столбца может быть 5-10 дат. Это занимает как минимум в 100 раз больше, чем сокращенное полное сканирование. Оказывается, что статистика может быть полностью устаревшей (по крайней мере, в соответствии с user_*.last_analyzed ); это также может быть причиной оценки 350 строк для одного раздела вместо десятков миллионов. Я должен обсудить возможную причину с администраторами баз данных — возможно, время ожидания задания сбора? Спасибо за понимание!
3. Для определенного количества строк сканирование индекса может быть быстрее (например, если вам нужна только одна строка из всей таблицы, даже с индексом из нескольких столбцов, это должно быть быстрее, чем FTS) — поэтому, если оптимизатор ожидает, что небольшое количество строк соответствует запросу, он будет использовать индекс. Если он не может распознать значение to_date во время синтаксического анализа, ему придется угадывать — и если он угадывает неправильно, результатом является плохой план… Вы могли бы попробовать добавить подсказку оптимизатора для ожидаемой мощности
Ответ №2:
Ошибки в оптимизаторе или анализаторе приводят к тому, что некоторые форматы дат понижают статическое сокращение разделов до динамического сокращения разделов. Изменения в сокращении разделов приводят к разной мощности и затратам, что затем приводит к значительным изменениям во многих других частях плана.
Этот ответ лишь частично объясняет проблему и содержит некоторые предположения. Надеюсь, это хотя бы немного прольет свет на то, в чем проблема, а в чем нет. Это, по крайней мере, хорошая отправная точка, если вам действительно нужно полное объяснение и вы хотите отправить запрос на обслуживание в Oracle.
Терминология и некоторые справочные сведения
Статическое сокращение разделов — это когда оптимизатор определяет во время компиляции, какой раздел будет использоваться. Статистика для каждого раздела, что приводит к лучшим оценкам мощности, что приводит к лучшим планам. Например, представьте таблицу, разделенную по статусу, где раздел для CANCELLED является крошечным, а раздел для ACTIVE — большим. Знание того, какой раздел используется, может полностью изменить порядок соединения и методы доступа оптимального плана. Pstart
и Pstop
будут числовыми значениями, когда используется статическое сокращение разделов.
Динамическое сокращение раздела — это когда оптимизатор не может определить раздел до выполнения. Данные извлекаются только из требуемых разделов, но план выполнения строится без специальных знаний о том, какой раздел используется. Некоторые оценки статистики разделов будут простым средним значением для всех доступных разделов. В приведенном выше примере таблицы, разделенной по статусу, среднее значение крошечного раздела и большого раздела неточно представляют либо. Либо Pstart
или Pstop
будет включать слово KEY
при использовании динамической обрезки разделов.
Руководство Oracle® Database VLDB и по разделению включает раздел о преобразованиях типов данных, который стоит прочитать. Например, одна соответствующая цитата из руководства:
Только правильно примененная функция TO_DATE гарантирует, что база данных способна однозначно определять значение даты и потенциально использовать его для статической обрезки, что особенно полезно для доступа к одному разделу.
Пример схемы и данных
Этот простой тестовый пример демонстрирует проблему. Это также исключает общие проблемы с производительностью, такие как отсутствие статистики.
Сначала создайте пример таблицы с 2 разделами, одним большим и одним маленьким.
create table gprs_history_import(id number, start_call_date_time date)
partition by range (start_call_date_time)
(
partition p_large values less than (date '2014-06-01'),
partition p_small values less than (date '2014-07-01')
);
insert into gprs_history_import
select level, date '2014-05-01'
from dual connect by level <= 1000;
insert into gprs_history_import
select level, date '2014-06-01'
from dual connect by level <= 10;
begin
dbms_stats.gather_table_stats(user, 'GPRS_HISTORY_IMPORT');
end;
/
select count(*) from gprs_history_import partition (p_large); -- 1000
select count(*) from gprs_history_import partition (p_small); -- 10
Преобразование статического в динамическое приводит к неправильным оценкам мощности
Оценка статической мощности равна 1000. Дополнительное пространство во втором формате даты изменяется Pstop
с 1 на KEY
. План меняется со статического на динамическое сокращение разделов. Динамическая оценка — неточная 505, среднее значение 1000 и 10
Для простоты этот пример показывает только плохую оценку мощности. Нет необходимости показывать, что запрос выполняется медленно, поскольку неверные оценки строк неизбежно приводят к неправильным планам выполнения по многим причинам.
explain plan for select /* static partition pruning */ *
from gprs_history_import
where start_call_date_time < to_date('20140601 000000','yyyymmdd hh24miss');
select * from table(dbms_xplan.display);
Plan hash value: 452971246
--------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop |
--------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1000 | 12000 | 16 (0)| 00:00:01 | | |
| 1 | PARTITION RANGE SINGLE| | 1000 | 12000 | 16 (0)| 00:00:01 | 1 | 1 |
| 2 | TABLE ACCESS FULL | GPRS_HISTORY_IMPORT | 1000 | 12000 | 16 (0)| 00:00:01 | 1 | 1 |
--------------------------------------------------------------------------------------------------------------
explain plan for select /* dybnamic partition pruning */ *
from gprs_history_import
where start_call_date_time < to_date('20140601 ','yyyymmdd ');
select * from table(dbms_xplan.display);
Plan hash value: 2464174375
----------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop |
----------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 505 | 6060 | 29 (0)| 00:00:01 | | |
| 1 | PARTITION RANGE ITERATOR| | 505 | 6060 | 29 (0)| 00:00:01 | 1 | KEY |
|* 2 | TABLE ACCESS FULL | GPRS_HISTORY_IMPORT | 505 | 6060 | 29 (0)| 00:00:01 | 1 | KEY |
----------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter("START_CALL_DATE_TIME"<TO_DATE('20140601 ','yyyymmdd '))
Проблемы с синтаксическим анализом формата даты
Теперь о некоторых предположениях относительно того, почему запрос переходит от статической к динамической обрезке разделов.
Не всегда очевидно, когда оптимизатор может использовать статическое и динамическое разделение. В общем, литералы допускают статическую обрезку, а переменные требуют динамической обрезки.
--#1: Obviously static: It uses an unambiguous ANSI date literal.
select * from gprs_history_import where start_call_date_time = date '2000-11-01';
--#2: Obviously dyanmic: It uses a bind variable.
select * from gprs_history_import where start_call_date_time = :date;
--#3: Probably dynamic: The optimizer cannot always infer the literal value.
select * from gprs_history_import where start_call_date_time =
(select date '2000-11-01' from dual);
--#4: Probably static: FEB is not always valid, but Oracle can figure that out.
select * from gprs_history_import where start_call_date_time =
to_date('01-FEB-2000', 'DD-MON-YYYY');
Если учесть все проблемы с производительностью и интернационализацией в случае № 4, становится ясно, насколько сложно анализировать даты. Значение to_date('01-FEB-2000', 'DD-MON-YYYY')
зависит от нескольких параметров NLS, таких как NLS_DATE_LANGUAGE
. Запрос действителен для английского, но не для немецкого. И если NLS_CALENDAR
не установлено GREGORIAN
значение, то даже полностью числовой формат даты может быть неправильным. to_date
Строка не является значением привязки, но и явно не является литералом.
Разница между литералами с истинной датой и отформатированными строками более очевидна, если учитываются жесткие синтаксические анализы. Запрос № 1 не будет принудительно выполнять жесткий синтаксический анализ, даже если язык изменен, но запрос № 4 будет. Это можно продемонстрировать, запустив несколько вариантов каждого, изменив язык, а затем запустив select value from v$sesstat natural join v$statname where name = 'parse count (hard)' and sid = userenv('SID');
.
У Oracle должна быть где-то переменная для обозначения «это не переменная привязки, но может привести к различным планам на основе настроек NLS». Эта переменная не всегда приводит к динамической обрезке разделов, но где-то должны быть какие-то ошибки, которые иногда нарушают ее.
Комментарии:
1. Руководство по VLDB и разделению Oracle® Database … — Мне это нравится! Я не знал о динамической и статической обрезке, и предупреждение о
to_date
том соответствует моей догадке, что у Oracle не было готового значения выражения to_date перед выполнением, потому что этот конкретный формат отбросил его.2. Конечно же, оценка rows = 350 в моих примерах полностью отсутствует (20 мм ближе к реальности). И план проверки с пропуском просто ужасен, потому что пропущенный столбец имеет несколько миллионов разных значений и несколько дат на узел. Но я только что обнаружил, что у меня могут быть устаревшие статистические данные, даже если они предположительно собираются автоматически; Мне нужно перепроверить в понедельник и обсудить возможные причины с администраторами баз данных. Спасибо за все подсказки — я займусь этим, когда у меня будет время.