Текст
                    ORACLE
основы стоимостной
ОПТИМИЗАЦИИ
Apress
ПИТЕР

С^ППТЕР
Jonathan Lewis Cost-Based Oracle Fundamentals ApressM
Дж. Льюис ORACLE ОСНОВЫ стоимостной ОПТИМИЗАЦИИ [^ППТЕР Москва • Санкт-Петербург • Нижний Новгород • Воронеж Ростов-на-Дону Екатеринбург • Самара Новосибирск Киев Харьков • Минск 2007
ББК 32.973.233 УДК 004.65 Л91 Льюис Дж. Л91 Oracle. Основы стоимостной оптимизации. — СПб.: Питер, 2007. — 528 с.: ил. ISBN 978-5-469-01309-9 5-469-01309-Х Стоимостный оптимизатор — это всего лишь фрагмент кода, содержащий модель обработки данных Oracle. Применяя эту модель к статистике по вашим данным, оптимизатор пытается эффективно преобразовать созданный вами запрос в исполняемый план. К сожалению, модель не может быть идеальной, статистика тоже не всегда безупречна, так что получившийся план исполнения порой оказывается далеким от совершенства. В этой книге Джонатан Льюис — один из крупнейших специалистов в своей области — описывает наиболее часто используемые компоненты модели, рассказывает, что именно оптимизатор делает с предоставленной ему статистикой и почему его работа может разладиться. Имея такую информацию, вы сможете не просто исправить отдельные операторы SQL, но и усовершенствовать проблемные области целиком, отрегулировав модель или создав более надежную статистику. ББК 32.973.233 УДК 004.65 Права на издание получены по соглашению с Apress. Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги. ISBN 1590596366 (англ.) ISBN 978-5-469-01309-9 © Jonathan Lewis, 2006 © Перевод на русский язык ООО «Питер Пресс», 2007 © Издание на русском языке, оформление ООО «Питер Пресс», 2007
Краткое содержание Предисловие.................................. 12 Введение. ..................................... 16 Глава 1. Что понимается под «стоимостью».........24 Глава 2. Табличное сканирование..................33 Глава 3. Селективность для одной таблицы.........68 Глава 4. Простой доступ по бинарному дереву. ....... 89 Глава 5. Фактор кластеризации .............. 115 Глава 6. Вопросы селективности ................143 Глава 7. Гистограммы.......................... 178 Глава 8. Битовые индексы...................... 210 Глава 9. Трансформации запросов............. 238 Глава 10. Кардинальность соединения. . ........ . 298 Глава И. Соединения с использованием вложенных циклов. . 340 Глава 12. Соединения хэширования.............. 353 Глава 13. Соединения сортировки и слияния.......391 Глава 14. Файл трассировки 10053 .............. . 446 Приложение А. Проблемы при обновлении версий....491 Приложение Б. Параметры оптимизатора............505 Алфавитный указатель ........................ 513
Содержание Предисловие.....................................................12 Об авторе.................................................... 13 О технических рецензентах.................................... 14 Благодарности................................................ 15 Введение........................................................16 Зачем беспокоиться?.......................................... 17 Что есть в этой. ............................... ........ ...L7 Чего нет в этой книге........................................ 18 Что будет в последующих книгах................................18 Структура книги.............................................. 19 Обязательное страшное предупреждение..........................20 Теория и практика......................................... 21 Планы выполнения.......................................... 22 Заключение................................................... 22 В память о Дугласе Адамсе (Douglas Adam?).....................23 Тестовые сценарии............................................ 23 От издательства.............................................. 23 Глава 1. Что понимается под «стоимостью».....................24 Параметры оптимизатора....................................... 24 Итак, что же такое «стоимость»?............................. 26 Преобразование и оценка стоимости ............................29 Что видишь, то и получишь (WYSIWYG)? .........................31 Заключение................................................... 32 Тестовые сценарии............................................ 32 Глава 2. Табличное сканирование.................................33 Начало....................................................... 34 Вперед и вверх............................................... 39 Эффекты размеров блоков................................... 39 Оценка стоимости процессорных ресурсов.....................41 Немного операций ввода-вывода .............................44 Немного процессора........................................ 47 Округления................................................ 47 Мощь оценки стоимости процессорных ресурсов...................48 BCHR умер! Да здравствует BCHR! ..............................50 Ошибки V$SEGSTAT в 91..................................... 52 Параллельное выполнение...................................... 54 Параллельные сканирования и чтения в режиме прямого доступа...57
Содержание 7 Быстрое полное индексное сканирование..............................58 Секционирование....................................................61 Заключение.........................................................66 Тестовые сценарии .................................................67 Глава 3. Селективность для одной таблицы........................... . 68 Начало .......................................................... 68 Неопределенные значения............................................71 Использование списков значений.....................................72 Обновления в 10g.................................................76 ДиаИазонные предикаты..............................................77 Изменения в 10g..................................................82 Два предиката . ...................................................83 Проблемы с несколькими предикатами.................................85 Заключение....................................................... 87 Тестовые сценарии..................................................88 Глава 4. Простой доступ по бинарному дереву...........................89 Основы оценки индексного доступа...................................89 Начало......................................................... 91 Эффективная селективность индекса.............................. 94 Эффективная селективность таблицы................................95 Фактор кластеризации (clusteringjactor) .........................95 Соберем все вместе............................................. 97 Расширение алгоритма........................................... 99 Эти три селективности......................................... 106 Оценка стоимости процессорных ресурсов. ........................ 109 Разное............................................................112 Заключение........................................................113 Тестовые сценарии............................................... 113 Глава 5. Фактор кластеризации ............. 115 Базовый пример................................................ 115 Уменьшение конкуренции при доступе к таблице (несколько списков свободных блоков)............................................. 118 Уменьшение конкуренции при доступе к листовым блокам (индексы по инвертированному ключу) ................................... 122 Уменьшение конкуренции при доступе к таблице (ASSM)...............124 Уменьшение конкуренции в RAC (группы списков свободных блоков)....128 Порядок столбцов................................................ 130 Дополнительные столбцы............................................133 Корректировка статистики..........................................135 Использование функций sys_op_countchg().........................135 Неформальные стратегии..........................................140 Разное. . ........................................................141 Заключение........................................................142 Тестовые сценарии............................................... 142
8 Содержание Глава 6. Вопросы селективности.....................................143 Различные типы данных............................................143 Значения типа «дата»..........................................144 Символьные значения...........................................144 Неверные типы данных..........................................147 Лидирующие нули..................................................150 Проблемы со значениями по умолчанию..............................152 Опасность дискретных значений....................................154 Изменения в версии 10g..................................... . 158 Удивительное поведение sysdate...................................159 Результаты использования функций.................................161 Коррелированные столбцы...................................... 162 Динамическая выборка........................................ 165 Профили оптимизатора..........................................168 Переходное замкнутое выражение................................. 169 Предикаты, сгенерированные из ограничений........................172 Заключение................................................... 176 Тестовые сценарии................................................176 Глава 7. Гистограммы...............................................178 Введение.........................................................178 Общие гистограммы.............................................. 184 Гистограммы и переменные связывания...........................185 Когда Oracle игнорирует гистограммы......................... 187 Частотные гистограммы............................................190 Изменение частотных гистограмм................................194 Предупреждение для тех, кто будет изменять частотные диаграммы.... 195 «Сбалансированные по высоте» гистограммы.........................197 Расчеты.......................................................200 Еще раз о проблемах с данными....................................203 Проблемные типы данных........................................203 Проблемы со значениями по умолчанию...........................206 Заключение.......................................................208 Тестовые сценарии.............................................. 209 Глава 8. Битовые индексы......................................... 210 Введение....................................................... 210 Компонент индекса......................................... 215 Табличный компонент ........................................ 217 Комбинации битовых индексов.................................... 219 Невысокое значение кардинальности.............................221 Столбцы со значениями NULL.................................. 225 Оценка стоимости использования ресурсов процессора...............228 Интересные случаи.............................................. 230 Индексы, построенные на нескольких столбцах...................231 Битовые индексы соединения ................................. 231 Трансформации битовых индексов ............................. 233 Заключение................................................. 236
Содержание 9 Тестовые сценарии ............................................... . 237 Глава 9. Трансформации запросов....................................238 Введение . . . . .......................................... 239 Эволюция................................................ 242 фильтрация .................................................. 243 Оптимизация фильтрации ..... ................................ 246 Скалярные подзапросы........................................ 250 Механизм выноса подзапроса ................................. 255 Слияние комплексных представлений........................... 262 Включение предикатов в представления ....................... 263 Общие подзапросы............................................. 265 Параметры подзапроса.......................................... . 267 Категоризация............................................. 269 Полусоединения.............................................. 275 Антисоединения.............................................. 278 Аномалия с антисоединениями............................... 280 Значения Null и Not In . . ................................. 281 Подсказка ordered........................................... 283 Соединения с трансформацией типа «звезда»...................... 285 Соединения типа «звезда»..................................... 291 The Future.................................................. 293 Заключение .................................................. 295 Тестовые сценарии . ........................................... 296 Глава 10. Кардинальность соединения........................... . 298 Базовая кардинальность соединения................................298 Влияние предиката фильтрации только с одной стороны соединения . . . 303 Кардинальность соединения в реальном SQL-коде....................305 Расширения и аномалии.......................................... 308 Соединения по диапазону .................................... 308 Неравенства ..... ........................................... 309 Пересечения диапазонов значений............................. 312 Гистограммы. ............................................... 313 Переходное замкнутое выражение................................317 Три таблицы. .................................................. 321 Значения null....................................................325 Проблемы реализации.......................................... 328 Трудности! ...................................................... . 332 Особенности.................................................. 335 Альтернативная точка зрения.................................. 337 Заключение................................................. 338 Тестовые сценарии ............................................. 339 Глава 11. Соединения с использованием вложенных циклов .............. .......................... 340 Основной механизм............................................ 340 Рабочий пример ................................................ 346
10 Содержание Контроль ошибок.................................................. 348 Заключение ......... ........................ .......... ......... 352 Тестовые сценарии .................................................352 Глава 12. Соединения хэширования................................... 353 Введение..................................................... 354 Оптимальное соединение хэширования........... ..................358 Соединение хэширования в один проход (The Onepass Hash Join)....360 Соединение хэширования в несколько проходов.....................367 Файлы трассировки .................................................372 Событие 10104................................................ 373 Событие 10053 ................................................ 375 Аномалии..................................................... . 376 Традиционная оценка стоимости...................................377 Новая оценка стоимости........................................ 377 Сравнения. ... ............................................ ...... 378 Соединения с множеством таблиц................................. . 386 Заключение....................................................... 389 Тестовые сценарии.............................................. 390 Глава 13. Соединения сортировки и слияния.........................391 Введение......................................................... 392 Использование памяти.......................................... 398 Использование ресурсов процессора...............................399 Параметр sort_area_retained_size............................ 403 Параметр pga_aggregate_target............................. . 405 Реальный ввод-вывод.............................................408 Стоимость сортировки......................................... 411 Трассировка 10053 ............................................ 412 Сравнения. ........................... ...... .................... 417 Соединения слияния. . ......................................... 422 Механизм слияния ...............................................422 Соединение слияния без первой сортировки...................... 427 Соединение слияния с декартовым произведением. .................429 Агрегирование и другие операции.................................. 431 Индексы .................................................... 436 Операции над множествами...................................... 437 Последнее предупреждение ........................................ 442 Заключение................................................... 443 Тестовые сценарии................................................ 444 Глава 14. Файл трассировки 10053 ........................ 446 Запрос. ....................................................... 447 План выполнения ................................................. 448 Среда ....................................................... 449 Файл трассировки...................................................450 Настройки параметров............... ............................450 Блоки запроса...................... .......................... 454
Содержание 11 Хранимая статистика......................................... 454 Одиночные таблицы .......................................... 456 Контроль ошибок............................................... 458 Общие планы выполнения..........................................459 Результаты оценки соединений.......... 488 Тестовые сценарии............................................... 490 Приложение А. Проблемы при обновлении версий . . 491 Пакет dbms_stats................................................ 492 Частотные гистограммы......................................... 493 Оценка стоимости использования ресурсов процессора.............. 493 Ошибки округления........................................... 493 Считывание значений переменных связывания....................... 494 Использование значений null в соединениях. ..................... 495 Конвертация индексов со структурой бинарного дерева в битовые индексы. . 495 Поиск в индексе с пропусками.................................... 495 Механизм AND-Equal................................................496 Индексное соединение хэширования .................................497 Исправление ошибок при обработке входного списка ............. 497 Переходное замкнутое выражение.................................. 498 Исправление расчетов, связанных с sysdate.........................498 Индексирование значений null. .................................. 499 Параметр pga_aggregate_target.....................................499 Сортировка.................................................. 500 Группировка..................................................... 500 Контроль ошибок .............................................. 500 Выход за границы диапазона...................................... 501 Использование неправильных типов данных...........................501 Параметр optimizer_mode ........................................ 501 Упорядоченные по убыванию индексы............................. 502 Слияние комплексных представлений .............................. 502 Уменьшение уровней вложенности запроса......................... . 502 Скалярные подзапросы и подзапросы фильтрации.................... 503 Изменения при выполнении параллельных запросов . .................503 Динамическая выборка............................................ 503 Временные таблицы . ............................................ 504 Статистика в словаре данных .................................. 504 Приложение Б. Параметры оптимизатора.................................505 Параметр optimizer_features_enable.......... ................... 505 Файл трассировки 10053. ....................................... 507 Представление v$sql_optimizer_env .............................. 511 Алфавитный указатель.................... 513
Предисловие Артур Кларк однажды написал, что любая достаточно сложная технология не- отличима от волшебства. Я считаю, что это наблюдение полностью верно. Кто-то другой позднее заметил, что любой специалист с достаточным уровнем знаний неотличим от волшебника. Так что эту книгу можно назвать книгой о волшебстве. Однако за все время, что я знаю автора этой книги Джонатана Льюиса (при- близительно 11 лет, в соответствии с моим исследованием архивов групп ново- стей с помощью Google), он никогда не прибегал к понятию «волшебство». Он хочет знать, почему что-то происходит так, как оно происходит. Так что, по су- ществу, его книга — о понимании'. Понимать гл.; сущ. — понимание. 1. Чувствовать и постигать смысл чего-либо. 2. Быть сведующим, хорошо разбираться в чем-либо. Выражаясь точнее, вся эта книга о понимании стоимостного оптимизатора (cost based optimizer, СВО) Oracle — как он работает и почему он делает то, что делает. Джонатан передает нам свое понимание оптимизатора Oracle посредст- вом практики и примеров, и с этим пониманием, с этим знанием становятся доступными новые возможности и решения. Попросту говоря, стоимостный оптимизатор Oracle — это математическая модель; вы предоставляете ему входную информацию (запросы, статистику), а он создает выходную (планы запросов). Чтобы успешно использовать опти- мизатор, очень важно понимать, что представляет собой эта входная информа- ция и как оптимизатор ее использует. Рассмотрим следующий вопрос, на кото- рый я пытался ответить много раз: каков наилучший способ сбора статистиче- ских данных и какую статистику необходимо собирать? Вопрос кажется доста- точно простым и очень понятным — на него должен быть ответ, и он есть, но он подходит не для всех случаев. Ответ на этот вопрос зависит от среды, распреде- ления данных, запросов, типа системы (транзакционная или хранилище дан- ных) — от массы факторов, и только понимая, как работает оптимизатор и как эти факторы влияют на оптимизатор, вы сможете дать ответ в своем случае. Моя любимая глава в этой книге — седьмая: в ней приведено великолепное обсуждение того, что делают гистограммы, как оптимизатор использует их, и рассказывается о некоторых мифах, которые их окружают (последние пред- ставляют собой часто неправильно истолковываемую входную информацию для оптимизатора). Одна из причин моего особого отношения к этой главе со- стоит в том, что я все еще помню, как впервые ее услышал (не прочитал, а именно услышал). Это было приблизительно три года назад, на встрече NoCOUG (Nor- thern California Oracle User Group). Я посетил семинар Джонатана по гисто- граммам и впервые почувствовал, что на самом деле понимаю, как работают гистограммы в Oracle. Джонатан предоставил практическую информацию, го- товую к повседневному применению, и в результате я оказался в силах отве- тить на заданный в предыдущем абзаце вопрос. В этой книге вы найдете множе-
Об авторе 13 ство тем, описанных в такой же практичной, позволяющей немедленное использование материала манере. Понимание, которое Джонатан вносит в работу стоимостного оптимизатора, позволит администраторам баз данных стать лучшими проектировщиками, а разработчикам лучше разрабатывать код на SQL. Обе эти группы в результате смогут лучше решать проблемы. Обстоятельные, развернутые примеры Джона- тана делают работу оптимизатора понятной всем нам. Не раз доказывалось, что мы можем надежно и эффективно использовать Инструмент, если понимаем, как он работает. Это справедливо и для программ- ного обеспечения, и в значительной степени для всего остального в жизни. В лю- бом случае книга, которую вы держите в руках, позволит вам надежнее и эффек- тивнее использовать такой инструмент, как оптимизатор. Томас Кайт VP (Public Sector), Oracle Corporation Об авторе Джонатан Льюис — квалифицированный преподаватель, получивший степень по математике в Оксфордском университете. Хотя его интерес к компьютерам проявился в нежном возрасте 12 лет — в те дни, когда под высокими техноло- гиями подразумевалось использование клавиатуры, а не устройства для проде- лывания дырок в перфокартах, — неясно было, станет ли он заниматься инфор- мационными технологиями, пока не прошло четыре года после окончания универ- ситета. Не считая года работы неумелым продавцом, он работал на себя в течение всей карьеры в компьютерной индустрии. Первой версией Oracle, с которой он познакомился, была 5.1, работавшая на персональном компьютере, используемом для проектирования и разработки системы управления рисками при добыче нефти для одной из ведущих нефтя- ных компаний. Первоначальный вариант этой системы он написал с помощью dBase III, в которой использовались таблицы и индексы и были сделаны неко- торые шаги в сторону систем управления реляционными базами данных. Вне- запно он узнал, на что должна быть способна правильная система управления реляционными базами данных (СУРБД). Начиная с того дня, Джонатан сосредоточился исключительно на вопросах использования и неправильной эксплуатации СУРБД Oracle. После трех или четырех лет работы по контракту над созданием больших систем он решил из наемных служащих превратиться в консультанты — и теперь распределяет свое время равномерно между краткосрочными работами по консультации, семина- рами и исследованиями. На момент написания книги он является одним из директоров UK Oracle User Group (www.ukoug.com), регулярно публикует свои материалы в ее еже- квартальном журнале и выступает на заседаниях профильных секций (особен- но специализирующихся по СУРБД и UNIX). Всякий раз, когда появляется возможность, он старается найти время для выступлений перед группами поль- зователей вне Великобритании, обычно это короткие встречи в конце рабочего дня. Он также поддерживает веб-сайт (www.jlcomp.demon.co.uk), на котором на- ходятся статьи и заметки по СУРБД Oracle.
14 Предисловие Джонатан около 20 лет женат на Диане (она в настоящее время ведущий преподаватель в школе, а до того многие годы трудилась бухгалтером), у них двое детей: Анна (актриса, играет на трубе) и Саймон (занимается регби, музи- цирует на саксофоне). Если приходится делать выбор — поработать в субботу либо «поболеть» на матче регби или сходить на школьный концерт — школьно- му мероприятию отдается предпочтение. О технических рецензентах Кристиан Антоньини (Christian Antognini) с 1995 года сосредоточился на уясне- нии того, как работает механизм базы данных Oracle. Диапазон его интересов — от логического и физического проектирования баз данных, интеграции баз дан- ных с приложениями на Java до стоимостного оптимизатора и, в принципе, всего остального, что имеет отношение к управлению и настройке производительно- сти. В настоящее время он работает в качестве старшего консультанта и препо- давателя в Trivadis AG (www.trivadis.com) в Цюрихе, Швейцария. Если он в дан- ный момент не помогает одному из клиентов использовать максимум возмож- ностей Oracle — значит, проводит где-то еще лекции по оптимизации или по новым возможностям Oracle для разработчиков. Кристиан живет в Тичино, Швейцария, со своей женой Мишель и двумя детьми, Софией и Элей. Он проводит много времени со своим замечательным семейством, и всякий раз, когда это возможно, читает книги, наслаждается хо- рошим кино или ездит на одном из своих велосипедов В MX. Вольфганг Брейтлинг (Wolfgang Breitling) родился в Штутгарте (Герма- ния), изучал математику, физику и компьютерные науки в тамошнем универси- тете. По окончании учебы Вольфганг устроился в отдел контроля качества не- мецкой лаборатории разработок IBM в Боблингене. Там он стал одним из разработчиков операционной системы для тестирования аппаратного обеспече- ния машин модели /370, разработанных в боблингенской лаборатории. Первой работой Вольфганга в сфере оценки и настройки производительно- сти была разработка программы для тестирования скорости отдельных опера- ций архитектуры /370. После подразделения IBM в Германии он трудился в ка- честве системного программиста с иерархическими базами данных IBM DL/1 и IMS в Швейцарии, а затем перебрался в Канаду, в Калгари, где живет сейчас. Несколько лет проработав системным программистом с базами данных IMS и позднее DB2 на мейнфреймах IBM, Брейтлинг включился в проект по реали- зации системы Peoplesoft на Oracle. В 1996 году он стал независимым консультантом, специализирующимся в администрировании и настройке Peoplesoft на Oracle. В этой роли Вольфганг участвовал в нескольких проектах по установке и обновлению Peoplesoft. Спе- цифические проблемы с настройкой Peoplesoft, зачастую без доступа к SQL, за- ставили его исследовать стоимостный оптимизатор Oracle с целью лучше понять, как он работает, и использовать эти знания при настройке. Он обнародовал свои открытия в документах и презентациях на IOUG, UKOUG, в локальных группах пользователей Oracle и в обзорах новостей на конференциях, посвящен- ных темам, связанным с производительностью Oracle.
благодарное™ 15 Вольфганг более 30 лет женат на Беатрис, у них двое детей — Магнус и Лео- да. Когда Вольфганг не занят расшифровкой оптимизатора, он наслаждается греблей на каноэ и путешествует пешком по канадским прериям и Скалистым горам. Влагодарности Прежде всего я должен поблагодарить мою жену и детей за их лояльность К Моим сухим выражениям, долгому молчанию и привычке, физически присут- ствуя, отсутствовать мысленно. Они стойко выносили мое заклинание «дайте мне еще всего пять минут». Я хотел бы поблагодарить тех экспертов Oracle и исследователей по всему миру, которые сознательно или бессознательно расширяли мое знание баз дан- ных. Особенно я должен поблагодарить Стива Адамса (Steve Adams), Вольф- ганга Брейтлинга (Wolfgang Breitling), Джулиан Дайк (Julian Dyke), К. Гопа- дакришнана (К. Gopalakrishnan), Стефана Хайсли (Stephan Haisley), Анье Колк (Anje Kolk), Тома Кайта (Tom Kyte), Джеймса Морла (James Morle) и Ричмон- да Ши (Richmond Shee) — хотя, понятно, вовсе не только их. Каждый из них в свое время помогал мне постичь что-то новое, а это откры- вало дополнительные области для исследования и освоения. Понимание сути явления куда важнее конкретных ответов на вопросы. Кристиан Антоньини (Christian Antognini) и Вольфганг Брейтлинг (Wolf- gang Breitling) заслуживают отдельной благодарности за вычитку рукописи и за множество предложений по улучшению качества книги. Все существенные ошибки остаются на моей совести. Есть еще много людей, которые заслуживают быть названными в качестве источников идей и информации, но чем больше имен я называю, тем более не- справедлив к тем, кого пропустил. Так что позвольте мне охватить их всех, вы- разив благодарность тем, кто пишет в группах новостей comp.databases.oracle, в почтовой рассылке Oracle-L и на форумах MetaLink корпорации Oracle. И ко- нечно, это члены Oak Table Network, которые начинают интересные и стимули- рующие творческий поиск обсуждения. Я также хотел бы упомянуть слушателей моего семинара «Optimizing Oracle: Performance by Design». В конце каждого из этих трехдневных семинаров все- гда появляются несколько новых вопросов, моменты, которые надо прояснить, и странные случаи, требующие исследования. Самая важная часть моей деятельности связана с рабочими базами данных, поэтому я хочу поблагодарить множество компаний, которые решили пригла- сить меня на несколько дней для решения проблем, проверки архитектур или просто с тем, чтобы я дал некоторые рекомендации. Без непрерывно поступаю- щей информации о проблемах от реальных пользователей, реальных требова- ний и реальных приложений было бы невозможно написать книгу, которая Представляла бы собой практическую ценность для читателей. И наконец, я дол- жен выразить огромную благодарность сотрудникам издательства «Apress», от- ветственным за редактуру и выпуск, которые работали невероятно интенсивно, чтобы успеть к сроку: Тони Дэвису (Tony Davis), Эми Нокс (Ami Knox), Кэти (Стене) (Katie Stence) и Грейс Вонг (Grace Wong).
Введение В предисловии к «Practical Oracle 8i> я сказал, что если вы пишете техниче- скую книгу об Oracle, она устареет к тому времени, как вы ее закончите. Эта книга была издана примерно в то же время, когда Ларри Эллисон (Larry Ellison) объявил об официальном выпуске Oracle 9i. Через три недели после выхода книги я получил первое письмо, в котором меня спрашивали, есть ли у меня планы по написанию версии этой книги для 9i. С того самого дня я сопротивлялся «обновлению», мотивируя это тем, что: во-первых, это много тяжелой работы; во-вторых, я должен несколько лет ис- пользовать Oracle 9i, прежде чем я смогу включить некоторую новую полезную информацию в книгу; в-третьих, это была бы точно такая же книга с нескольки- ми небольшими изменениями. Итак, с чего все началось: я начал писать в сентябре 2003 года (да, правда, 2003-го; чтобы написать этот том, мне понадобилось 22 месяца), ровно через че- тыре года после того, как я решил написать «Practical Oracle 8i». (Помните сен- тябрь 1999-го, когда никто не хотел нанимать на работу специалиста по Oracle, если только он не мог отлаживать код на COBOL?) Конечно, в течение этих че- тырех лет было выпущено несколько обновлений Oracle 8i (которые закончи- лись на 8.1.7.4), две основные версии Oracle 9i, и, когда я начал писать, на Oracle World был представлен Oracle 10g. Я был уже почти готов начать писать «Practical Oracle 9i» — но слишком поздно. На самом деле, как только я закончил писать эту книгу (в июне 2005-го), версия 10g Release 2, портированная под Linux, появилась на OTN! Так что пер- вое, что вы можете сделать, читая ее, — это начать проверять мои тестовые сце- нарии на 10gR2, чтобы посмотреть, как много изменений было внесено. Вместо создания обновления Practical Oracle 8i я принял решение описать все, что я знаю об оптимизации на основе оценки стоимости. Сначала эта затея выглядела очень простой: я могу часами говорить о вещах, которые делает оп- тимизатор, и о том, почему он это делает; все, что мне предстояло, — просто за- писать это. К несчастью, задача оказалась намного сложнее, чем я ожидал. Сыпать сло- вами легко, но создать хорошо структурированную книгу, которая была бы по- лезна, — совсем другое дело. Покажите мне что-то, что сделал оптимизатор, и я смогу объяснить, почему он это сделал, — возможно, после создания и тестиро- вания нескольких теорий. Попросите меня дать кому-то другому достаточно универсальную информацию об оптимизаторе, чтобы он смог сам сделать то же самое, — это уже совсем другая задача. В конечном счете, я сумел структурировать информацию и понял, что дол- жен написать, по крайней мере, три книги: основные принципы, некоторый
gyp есть в этой книге 17 дополнительный материал и все второстепенные вопросы. В книге, которую вы держите в руках, описаны базовые принципы оптимизации, основанной на оценке стоимости. Зачем беспокоиться? Почему мы хотим знать, как работает оптимизатор? Потому что, когда оптими- затор создает очень плохой план выполнения, мы хотим понять, что представ- дает собой эта проблема, чтобы можно было корректно ее решить. Конечно, мы можем решить эту конкретную проблему, добавив подсказки к запросу или как-то хитро переписав запрос, но если мы пойдем таким путем, ням, вероятно, придется делать это снова и снова при появлении данной про- блемы в других местах. Но если мы понимаем основу проблемы, мы можем решить ее один раз И знать, что покончили с ней навсегда. Что есть в этой книге Этот том охватывает основные подробности оптимизации. Он не предназначен быть полным руководством по работе оптимизатора: вы отметите, что до соеди- нений я добрался только в главе 10, и поймете, как много всего надо охватить. Важные умные слова, касающиеся оптимизации: О селективность и кардинальность — какую долю данных определяет преди- кат и во сколько записей эта доля превратится; О путь доступа — должен ли запрос использовать индекс, основанный на В-де- реве, или, возможно, должны быть объединены несколько битовых индек- сов, или надо вообще проигнорировать индексы при обращении к таблице; О порядок соединений — к какой из таблиц запрос должен обратиться в первую очередь и как он должен действовать дальше, чтобы для получения необхо- димого результата проделать наименьший объем работы. Хотя я дам несколько комментариев о некоторых наиболее тонких особен- ностях, которые надо учесть, эта книга не ограничивает вас необходимой горст- кой основных понятий. Как оптимизатор определяет объем данных, который создаст предикат? Как он получает число, которое представляет собой оценку работы по выполнению табличного сканирования, и как он сравнивает эту оценку с оценкой работы, которую надо выполнить при использовании индек- са? Какие значения участвуют в оценке ресурсов, необходимых для выполне- ния сортировки или соединения хэширования? Проанализировав запрос, объекты, используемые в нем, файл трассировки 10053, я обычно могу объяснить, почему одному пути доступа было отдано предпочтение. К сожалению, я не могу сказать вам, как сделать это для каждого файла трассировки, который может вам встретиться, потому что не могу охватить
18 Введение все возможные варианты (я даже еще не видел всех вариантов) — если бы я по- пытался, эта книга была бы очень утомительной. Но даже при том, что я, возможно, не могу дать вам все ответы, я надеюсь, что в моей книге описано достаточно базовых методов, позволяющих определять, что происходит, в большинстве случаев, которые вы будете анализировать. Чего нет в этой книге Неизбежно что-то придется опустить. Некоторые вещи я проигнорировал, по- тому что они второстепенны по отношению к основной работе оптимизатора, или аудитория для них все еще очень мала, или просто из-за недостатка места. Я вообще не упомянул оптимизатор по синтаксису (rule based optimizer, RBO), потому что мы должны стараться избегать его использования. Я не за- тронул ничего из области расширяемого оптимизатора (extensible optimizer) (включая контекстное и пространственное индексирование), потому что он не представляет основное направление. Я не упомянул аналитические функции, model clauses (10g), и OLAP, потому что все они должны получить данные, пре- жде чем выполнять свои собственные сложные расчеты, и получение данных, наверное, представляет собой наиболее критичный с точки зрения времени ас- пект их работы. Я не упомянул объекты — потому что они не существуют, если рассматри- вать оптимизатор. Когда вы создаете объектный тип и сегменты данных объект- ных типов, Oracle превращает их в простые таблицы и индексы — оптимизатор не заботится об объектах. И, наконец, я едва упомянул параллельные запросы, секционированные таб- лицы, распределенные запросы и некоторые более тонкие физические настрой- ки Oracle, такие как кластеры и ЮТ. Их пришлось вычеркнуть по двум причи- нам: во-первых, по причине недостатка места, а во-вторых, чтобы избежать растворения важных моментов в море сопутствующих подробностей. С опти- мизатором связано много всего, трудно держать это все в фокусе и лучше брать за раз понемногу. Что будет в последующих книгах Эта книга первая в серии из трех книг. В последующих томах будут охвачены важные возможности Oracle — в частности, секционированные таблицы, парал- лельное выполнение, индекс-таблицы, динамическая выборка и перезапись за- просов. В них также появится дополнительная информация по материалу, приве- денному в этом томе, например дальнейшие пути доступа для индексов в виде В-деревьев, сравнение кластерного доступа и индексного доступа и более под- робная информация по гистограммам. Заключительная важная часть информации об оптимизации на основе оцен- ки стоимости — это инфраструктура, которая поддерживает ее, позволяет вам
gpyrrypa книги.19 донимать ее. Основные темы в этом вопросе — понимание и интерпретация планов выполнения, понимание значения и использования подсказок и получе- ние лучшего из пакета dbms_stats. Эта книга основывается на версии 9.2 с наблюдениями относительно ее от- личий от 8i и комментариями о том, какие изменения появились в 10g. В буду- щих томах будет очень мало явных ссылок на 8i и больше будет рассказываться <?10g. Структура книги Главы этой книги раскрывают следующие темы в приведенном порядке. О Табличные сканирования. Эта тема позволяет начать с простого и немного рассмотреть оценку стоимости процессорных ресурсов. О Простая селективность. Только одна таблица, но множество важных идей цо выполняемым вычислениям. О Простые индексы на основе В-деревьев. Различие между одноблочными и мно- гоблочными чтениями. О Фактор кластеризации. Возможно, наиболее важная особенность индексов. Q Более тонкая селективность. Введение во многие незначительные измене- ния в базовой теме. О Гистограммы. Почему, возможно, вы нуждаетесь в немногом; разница между OLTP и DSS/DW. О Битовые индексы. Потому что не все индексы одинаковы. О Трансформации. То, что вы видите, это совсем не обязательно то, что вы по- лучите. О Соединения. Четыре полных главы просто о том, как соединить две таблицы. О Трассировка 10053. Рабочий пример. О Проблемы, связанные с обновлением. Набор предупреждений и примечаний, которые собраны из оставшейся части книги. Каждая глава содержит выдержки кода из набора SQL-сценариев, который доступен для загрузки с веб-сайта www.piter.com или www.apress.com. Вы можете Выполнить эти сценарии на своей системе, чтобы воспроизвести и исследовать наблюдения, сделанные в главе. Проанализируйте эти сценарии, так как они со- держат дополнительные комментарии и некоторые дополнительные тесты, не упоминаемые в основном тексте. Также время от времени я буду публиковать эти сценарии на своем веб-сайте (www.jlcomp.demon.co.uk), расширяя их и до- бавляя дальнейшие комментарии. Эти сценарии важны: все меняется, и изменения могут производить серьез- ные эффекты на ваших рабочих системах. Если у вас есть сценарии для тести- рования базовых механизмов, вы можете повторять эти тесты при каждом об- новлении, чтобы видеть, были ли внесены какие-то изменения, о которых вы должны знать.
20 Введение Один важный момент, который надо отметить: выдержки кода, присутст- вующие в тексте, часто включают строки для стандартизации тестовой среды, например: alter session set "_optimizer_system_stats_usage" = false; He используйте команды, подобные этой, на рабочих системах только пото- му, что они приведены в книге. Они приведены не для того, чтобы показать хо- роший пример программирования; они необходимы как попытка избежать по- бочных эффектов, которые появляются, когда одна база данных имеет, напри- мер, полностью отличный от другой набор системных статистических данных. Также в онлайн-хранилище кода вы найдете три файла init.ora и сценарий для создания тестовых табличных пространств в 9i и 10g. Все четыре файла надо отредактировать, чтобы решить проблемы с правилами именования ката- логов; файлы init.ora для 9i и 10g также должны быть скорректированы, чтобы удовлетворить вашим параметрам управления откатами и отменами. Я решил работать с файлами init.ora для 9i и 10g, чтобы избежать случайных изменений в spf lie, но вы можете решить объединить настройки init.ora в spfile. Обязательное страшное предупреждение Всякий раз, когда кто-нибудь просит мой автограф на книге «Practical Ora- cle 8i>, рядом со своей подписью я предлагаю добавить мой девиз: «Никогда не верьте всему, что вы читаете». (Если вы попросите меня подписать экземпляр этой книги, девиз будет таким: То, что это напечатано на бумаге, еще не озна- чает, что так оно и есть на самом деле.) Всегда существуют специальные случаи, различные настройки параметров и ошибки. (Не говоря уже о тысячах неизвестных мне вещей и о вещах, кото- рые, как мне кажется, я знаю, но это не так.) Проведем простой эксперимент (сценарий in-list.sql в онлайн-хранилище кода) — выполним следующий пример на локально управляемом табличном пространстве, используя ручное управление пространством сегментов, с разме- ром блока 8 Кбайт: create table tl as select trunc((rownum-l)/100) nl, rpad('x‘,100) padding from all_objects where rownum <= 1000 -- Здесь соберите статистику, используя dbms_stats set autotrace traceonly explain select * from tl where nl in (1,2)
0$язательное страшное предупреждение 21 «-П1'1 .. Из-за использования функции trunc() столбец nl принимает значения от О’ДО 9, со 100 экземплярами для каждого значения. Так что запрос вернет 200 ЗЙЯИсей. Выполните этот тест под 8.1.7.4, а затем под 9.2.0.6, и проверьте карди- нальность, выдаваемую autotrace. Я получил следующие результаты: выполнения (8.1.7.4, автотрассировка) В SELECT STATEMENT Optimizer=ALL_ROWS (Cost=3 Card=190 Bytes=19570) 1 0 TABLE ACCESS (FULL) OF ’ТГ (Cost=3 Card=190 Bytes=19570) выполнения (9.2.0.6, автотрассировка) g SELECT STATEMENT Optimizer=ALL_ROWS (Cost=4 Card=200 Bytes=20400) 1 0 TABLE ACCESS (FULL) OF ’T1’ (Cost=4 Card=200 Bytes=20400) В 8.1.7.4 оптимизатор дает оценку, равную 190 записям, в качестве кардиналь- ности результирующего множества. В 9.2.0.6 у оптимизатора есть алгоритм, ко- Тбрый дает оценку, равную 200, в качестве кардинальности результирующего даоЖества. В данном случае это правильно — это результат исправления ошиб- ки й оптимизаторе. До выхода 9i мой трехдневный семинар включал наблюдение, что в 8i, похо- жее, возможен выигрыш, который может быть получен за счет смешивания ин- дексных путей доступа и списков значений. Затем вышла версия 9i, я выполнил Тест, который я только что описал, и понял, что это не хитрое вычисление в 8i, а ошибка. (За более подробной информацией обратитесь к главе 3.) Так что, если ваш опыт противоречит моим заявлениям, у вас может быть конфигурация, которой я еще не видел, — за всю свою жизнь я работал только с несколькими сотнями баз данных, возможны миллионы особенностей комби- наций конфигураций и распределений данных, вносящие отличия. В большин- стве случаев для загрузки доступны сценарии, которые позволяют вам воспро- извести тестовые сценарии, представленные в книге, — так что, по крайней мере, вы можете проверить, ведут ли мои тестовые сценарии себя на вашей сис- теме так, как прогнозируется. Какими бы ни были результаты, вы можете полу- чить некоторые основные идеи о причинах отличий ваших наблюдений от моих. В качестве упражнения для читателя оставлено сравнение результатов 9.2.0.6 И (в частности) 10.1.0.4 при изменении предиката в следующем запросе с where nl in (1,2); на where nl in (11, 12); Затем изменяйте константы на увеличивающиеся значения. Результаты весьма удивительны — они будут рассмотрены подробнее в главе 3. Теория и практика Важно напоминать самому себе при прочтении этой книги, что существуют две отдельные стадии выполнения запроса. Сперва оптимизатор определяет, что, как он «думает», должно произойти, а затем механизм времени выполнения де- лает то, что оптимизатор хочет делать.
22 Введение В принципе, оптимизатор должен знать об этом, описывать и основывать свои вычисления согласно деятельности, которую, как предполагается, будет выполнять механизм времени выполнения. На практике оптимизатор и меха- низм времени выполнения иногда имеют различные идеи относительно того, что они должны делать. Планы выполнения В следующей книге из этой серии я опишу методы, которые вы можете исполь- зовать для получения планов выполнения, и приведу различные причины, по которым получающиеся планы могут быть неправильными или, по крайней мере, могут вводить в заблуждение. И поэтому пока позвольте мне просто ска- зать, что в примерах в этой книге для отображения планов выполнения исполь- зуется автотрассировка или пакет dbms_xplan. Однако, когда я на месте исследую реальную проблему и у меня есть ка- кие-либо причины сомневаться в этих аналитических инструментах, я могу ре- шить выполнить проблемные запросы так, чтобы удалось проверить файлы трассировки 10053, получить статистическую информацию из файлов трасси- ровки 10046 и содержимое v$sql_plan. В тех случаях, когда задействованы секционированные объекты или парал- лельное выполнение, я могу использовать некоторые другие трассировочные события, чтобы выяснить подробности, которые иначе недоступны из любой формы плана выполнения. Все инструменты для генерации и исследования планов выполнения имеют некоторые неточности. Не полагайтесь только на автотрассировку или dbms_ xplan только потому, что они, похоже, представляют собой единственные инст- рументы, которые я использовал в этой книге. Заключение Когда вы закончите читать эту книгу (в первый раз), я надеюсь, вы почерпнете из нее три ключевых момента. О Во-первых, обычно хорошее решение вашей проблемы существует где-то среди путей выполнения, доступных оптимизатору. Вам просто надо опреде- лить такой путь и найти способ заставить его появиться. О Во-вторых, кто угодно может создавать хорошие тесты с увеличивающейся сложностью, которые увеличиваются в масштабе и нюансах тогда и только тогда, когда это оправдано сложившейся ситуацией. О И наконец, если вы наблюдаете проблему с производительностью и обнару- живаете, что решаете ее, используя понятия содержимого данных, требова- ний к доступу и стратегий физического хранения, это значит, что вы усвои- ли наиболее важный урок, который есть в этой книге.
В издательства 23 g память о Дугласе Адамсе (Douglas Adams) Существует теория: если кто-то обнаруживает, что точно делает стоимостный оптимизатор и то, как он работает, это немедленно исчезает и заменяется Пем-то еще более причудливым и необъяснимым. Есть еще одна теория, которая гласит, что это уже случилось... дважды. Тестовые сценарии файлы к введению, доступные для загрузки, перечислены в табл. ВЛ. Таблица В.1. Тестовые сценарии к введению Щнарий Комментарии________________________________________________ Hjstsql Демонстрирует изменения в вычислении селективности итератора по списку значений setenv.sql Установка стандартизованного окружения для SQL*Plus От издательства Ваши замечания, предложения и вопросы отправляйте по адресу электронной почты comp@piter.com (издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мнение! Все исходные тексты, приведенные в книге, вы можете найти по адресу http://www.piter.com/download. Подробную информацию о наших книгах вы найдете на веб-сайте издатель- ства http://www.piter.com.
1Что понимается под «стоимостью» Когда вы говорите о стоимости оператора SQL, необходимо проявлять осто- рожность, так как очень легко использовать это слово в двух разных смыслах одновременно. С одной стороны, вы можете думать о магическом'числе, сгене- рированном некоторым инструментом, например explain plan;c другой сторо- ны, можно подразумевать фактическое использование ресурсов при выполне- нии оператора. В теории, конечно, должны быть ясные и простые взаимосвязи между этими вариантами, и не надо так сильно заботиться об их значении. В этой книге слово стоимость (cost) будет всегда означать результат вычис- лений, выполненных оптимизатором. Задача книги — объяснить основы того, как оптимизатор выполняет вычисления, в результате которых получается стои- мость, определяющая план выполнения для оператора SQL. Как побочный эффект этого объяснения, я также опишу некоторые пробле- мы, из-за которых оптимизатор может получить стоимость, не имеющую отно- шения к фактическому использованию ресурсов. Очень надеюсь, что при чтении этой книги будут случаться волшебные мгновения, когда вы будете говорить: «Так вот почему запрос X выполняется таким образом...» Параметры оптимизатора Наиболее общий тип оператора — это оператор select, но, даже несмотря на то что в этой книге основное внимание будет уделено операторам select, следует помнить, что любой другой оператор, будь то запрос, DML (например, update) или DDL (например, перестройка индекса), анализируется стоимостным оп- тимизатором (cost based optimizer, СВ О). В Oracle существуют три варианта работы стоимостного оптимизатора. Эти три варианта имеют различные ограничения, встроенные в их код, но все они следуют одной стратегии, состоящей в том, чтобы найти механизм выполнения, который использует наименьшее количество ресурсов ради получения необхо- димых результатов для данного оператора. Эти варианты определяются сле- дующими установками параметра optimizer_mode. О al l_ rows — оптимизатор попытается найти план выполнения, который полно- стью выполняет оператор (обычно это означает «возвращает все записи») в са- мое короткое время. Нет специальных ограничений, встроенных в этот код.
Параметры оптимизатора 25 О first_rows_N — число N может быть равно 1, 10, 100 или 1000 (для более точного определения также существует подсказка f i rst_rows (n), где число п может быть любым положительным целым числом). Оптимизатор сначала оценивает число записей, которое будет возвращено, полностью анализируя только первый порядок соединения. Это позволяет оценить, какую долю всего набора данных предполагается получить. Затем повторно начинается полный процесс оптимизации; задачей его является поиск плана выполне- ния, который минимизирует ресурсы, необходимые для возвращения этой доли всех данных. Этот вариант появился в версии 9i. 0 fi rst_rows — этот режим не рекомендуется использовать в версиях, начи- ная с 9i, но он поддерживается для обратной совместимости. Оптимизатор Попытается найти план выполнения для возвращения первой записи резуль- тирующего множества так быстро, как это возможно. Существует несколько ограничений высокого уровня, встроенных в этот код. Например, одно из ограничений — избегать соединения слияния и хэширования, если только альтернативой не является использование вложенных циклов с полным Сканированием внутренней (второй) таблицы. Эти правила подталкивают оптимизатор использовать индексные пути доступа, что иногда может быть очень неподходящим. Пример обхода этой конкретной проблемы можно найти В сценарии first_rows.sql в онлайн-хранилище кода для этой главы (см. введе- ние) и на веб-сайте автора (www.jlcomp.demon.co.uk). Существуют два других значения для optimizer_mode (даже в 10g): rule и choose. В этой книге полностью игнорируется оптимизация по синтаксису (rule based optimization, RBO), так как она не рекомендуется к использованию несколько лет и полностью перестала поддерживаться в 10g (даже при том, что Некоторый внутренний SQL-код все еще использует подсказку /*+ rule */). Что касается значения choose, оно дает оптимизатору во время выполнения Выбор между оптимизацией по синтаксису и режимом all_rows. Так как я иг- норирую оптимизацию по синтаксису, больше о режиме choose нечего сказать. Я только упомяну, что в 10g, если вы используете Database Configuration Assis- tant (DBCA) для создания базы данных или вызываете catproc.sql при ручном создании, вы автоматически установите задание (создаваемое сценарием catmwin.sqln видимое в представлении dba_scheduler_jobs), которое будет ав- томатически каждые 24 часа генерировать статистику для всех таблиц с отсут- ствующей или устаревшей (stale) статистикой. Так что, если вы установите optimizer-inode в значение choose, это, вероятно, приведет к оптимизации в режиме all_rows, но вы можете обнаружить, что таблицы, которые пропусти- ли последний проход генерации статистики, будут подпадать под динамическую выборку (dynamic sampling). Это происходит потому, что значение по умолчанию ДЛЯ параметра opt1m1zer_dynamic_sampl1ng в 10g — 2 (которое означает ис- пользование динамической выборки по всем таблицам без статистики), а не 1, как это было в 9i. Другой параметр, связанный с opt1mizer_mode, — это optimizer_goal. По- ЗДке, эти два параметра работают одинаково при любых стратегиях оптимиза-
26 Глава 1. Что понимается под «стоимостью» ции, хотя optim1zer_goal можно устанавливать только динамически в сеансе, и этот параметр недоступен в spfile (init.ora). Итак, что же такое «стоимость»? Наиболее часто задаваемый в Интернете вопрос об оптимизаторе — «Что пред- ставляет собой стоимость?» За этим вопросом обычно следуют разъяснения: «В соответствии с explal п plan, стоимость выполнения соединения хэширова- ния для этого запроса — 7 миллионов, а стоимость вложенного цикла — 42, но хэш-соединение выполняется за 3 секунды, а вложенный цикл — за 14 часов». Ответ прост: стоимость представляет (и всегда представляла) собой наилуч- шую оценку оптимизатором времени, необходимого для выполнения операто- ра. Но как это может быть правдой, если мы видим странности, как в приме- ре с хэш-соединением и соединением с вложенным циклом? Ответ обычно можно найти в старой доброй аббревиатуре «GIGO»: Garbage In, Garbage Out (Мусор на входе — мусор на выходе). Оптимизатор допускает ошибки по шести основным причинам. О Некоторые неподходящие предположения встроены в модель стоимости. О Соответствующая статистика о распределении данных доступна, но непра- вильна. О Соответствующая статистика о распределении данных недоступна. О Параметры производительности аппаратного обеспечения неизвестны. О Текущая рабочая нагрузка неизвестна. О Ошибки в коде. По ходу книги мы рассмотрим эти проблемы и развитие оптимизатора для их решения. Однако, учитывая их влияние, я кратко упомяну о некоторых из- менениях в оптимизаторе, внесенных в последних версиях Oracle (они могут появиться и в будущих версиях), из-за которых намного сложнее объяснить, как оптимизатор дошел до некоторого решения. В 8i оптимизатор просто подсчитывал число запросов к подсистеме вво- да-вывода, которое предполагалось выполнить. Выбирался единственный план выполнения, для которого требуется наименьшее количество запросов. Этот подход не учитывал тот факт, что для сканирования таблицы может потребо- ваться значительно больше ресурсов процессора, чем для индексного доступа. Также не учитывается, что 128-блочное чтение может на самом деле занять больше времени, чем чтение по одному блоку (single-block read). Не учитывает- ся и тот факт, что некоторое 128-блочное чтение может на самом деле превра- титься, скажем, в 25 отдельных запросов на чтение, потому что случайно рас- пределенное подмножество необходимых блоков уже находилось в буфере Oracle. Не учитывается также, что запрос ввода-вывода может быть обработан с помо- щью промежуточного кэша, а не с помощью чтения с физического диска. В 9i в оптимизаторе появилась возможность, называемая оценкой стоимо- сти процессорных ресурсов (CPU costing). Вы можете хранить типичное время ответа для одноблочных и многоблочных запросов ввода-вывода в базе данных наряду с показателем типичного размера многоблочного запроса, и оптимиза-
gtaK, что же такое «стоимость»? 27 дер будет учитывать эти значения при вычислении стоимости. Оптимизатор ^ЙКЖе преобразует количество операций процессора (например, сравнение ’делбца с типом «дата» с константой) в процессорное время и учитывает это 4 вычислениях. Эти тонкости гарантируют, что оптимизатор может лучше оце- стоимость сканирований таблиц и строит более разумные планы выполне- дея. и аномалии, такие как с соединениями хэширования и вложенными цикла- ми, упомянутые ранее в этом разделе, происходят реже. В 10g появился автономный оптимизатор (offline optimizer). Эта возмож- тсть позволяет генерировать и хранить критическую статистическую инфор- мацию (в виде профиля (profile)), которая помогает онлайн-оптимизатору решать проблемы с коррелированными распределениями данных. В действи- тельности вы можете улучшить запрос, добавив подсказку, которая сообщает: Шдвсь получится в 15 раз больше данных, чем ожидается». И 9i, и 10g собира- ет статистику кэширования (cache statistics) на уровне объектов и, заглядывая $ будущее, 10g имеет несколько скрытых параметров (hidden parameters), кото- рые, похоже, обеспечат использование высокоточных вычислений с учетом кэ- ширования. Если (или когда) эта возможность будет внедрена, оптимизатор сможет строить планы, которые лучше отражают фактическое количество необ- ходимых запросов ввода-вывода, на основании последних попаданий в кэш. Более того, и 9i и 10g собирают статистику времени выполнения в представ- лениях v$sql_plan_statisties и v$sql_plan_statistics_all. Эта статисти- ка Теоретически может быть возвращена оптимизатору, чтобы у него появился второй шанс оптимизировать запрос, если фактическая статистика сильно от- личается от предположений, сделанных оптимизатором. В один прекрасный день, возможно, в одной из следующих промежуточных версий, вы сможете взглянуть на стоимость запроса и уверенно преобразовать ее в приблизительное время выполнения, так как оптимизатор будет точно строить правильный план для ваших данных на данной машине в некоторый момент времени. (Конечно, оптимизатор, который будет начинать думать по-разному каждые пять минут из-за непрерывной активности, может быть скорее угрозой, чем преимуществом — я думаю, что предпочел бы предсказуе- мость и стабильность периодически неудачному совершенствованию.) Но почему я так уверен, что стоимость предполагается равной времени? Об- ратимся к разделу 9.2 «Performance Guide and Reference» (Part A96533), c. 9-22. В соответствии с моделью оценки стоимости процессорных ресурсов: Стоимость = ( #SRds * sreadtim + #MRds * mreadtim + #CPUCycles I epuspeed ) / sreadtim ГДе #SRDs — количество одноблочных чтений, #MRDs — количество многоблочных чтений, #CPUCycles — количество циклов процессора, sreadtim — время одноблочного чтения, mreadtim — время многоблочного чтения, Cplispeed — количество циклов процессора в секунду. В переводе на человеческий язык это означает следующее:
28 Глава 1. Что понимается под «стоимостью» Стоимость — это время, потраченное на одноблочные чтения, плюс время, потраченное на многоблочные чтения, плюс необходимое процессорное время, и все это деленное на время, необходимое для выполнения одноблочного чте- ния. Таким образом, стоимость — это суммарное прогнозируемое время выпол- нения оператора, выраженное в единицах времени выполнения одноблочного чтения. ВЫВОД CPUSPEED Хотя в руководстве указывается, что cpuspeed отображается в циклах в секунду, существуют две возможные ошибки. Простая ошибка состоит в предположении, что единица измерения — миллионы циклов в секунду (например, скорость процессора в МГц). И даже несмотря на это, значение, похоже, не всегда оп- равдывает ожидания — в моем случае это значение отличалось на множитель от 5 до 30 на различ- ных машинах, на которых я проводил тестирование. Более сложная ошибка состоит в том, что значение может быть на самом деле мерой миллионов стандартизованных oracle-операций (standardized oracle operations) в секунду, где «standardized oracle operation» — некоторая специальная процедура, спроектированная для моделирования на- грузки на процессор. (Об этом свидетельствует файл трассировки 10053 от 10.2.) Представляет ли собой это значение количество циклов в секунду или операций в секунду, един- ственная разница — в простом коэффициенте масштабирования. Механизм использования cpuspeed не изменился. Почему в Oracle используется такая странная единица измерения времени, а не просто сотые доли секунды? Я думаю, что это сделано только для обратной совместимости. Стоимость в 8i (и в 9i, пока вы не включите полную оценку стоимости процессорных ресурсов) была просто количеством запросов вво- да-вывода, никакого различия между одноблочным и многоблочным вводом- выводом не делалось. Так что, если новый код будет показывать время в едини- цах времени одноблочного чтения, значение стоимости для типичного (просто- го, с использованием индексного доступа) OLTP-запроса не изменится сильно, если вы перейдете с 8i на 9i. Если еще немного подумать над этой формулой, вы поймете, что при вклю- чении оценки стоимости процессорных ресурсов стоимость табличного скани- рования будет увеличиваться на коэффициент, который приблизительно равен (mreadtim / sreadtim). Поэтому 9i с включенной оценкой стоимости процес- сорных ресурсов будет предпочитать индексные пути доступа немного чаще, чем 8i, потому что 9i корректно распознает, что многоблочные чтения могут вы- полняться дольше, чем одноблочные. Если вы планируете переход с 8i на 9i (или с 8i на 10g), убедитесь, что вы включили оценку стоимости процессорных ресурсов с самого начала регрессионного тестирования — вас ждут некоторые сюрпризы. Последнее замечание об этой формуле: в ней нет явного упоминания ка- ких-либо компонентов, имеющих отношение к времени, потраченному на опе- рации ввода-вывода, которые могут возникнуть в результате соединений слия- ния, хэш-соединений или сортировок. Во всех этих трех случаях Oracle исполь- зует прямую (direct path) запись и чтение, с размерами, которые обычно не имеют ничего общего с обычным размером многоблочного чтения — так что ни mreadtim, ни sreadtim полностью не подходят.
•вание и оценка стоимости 29 Преобразование и оценка стоимости Существует важный аспект оптимизации, который часто пропускают и кото- рый может легко привести к путанице, особенно если вы работаете с разными дрелями Oracle. Перед тем как выполнять какие-либо вычисления стоимости, Oracle может трансформировать ваш оператор SQL в эквивалентный опера- фр — возможно, даже в формально недопустимый — и затем рассчитывать роимость для этого эквивалентного оператора. В зависимости от версии Oracle существуют преобразования, которые: 0 не могут быть выполнены; всегда выполняются, если это возможно; О выполняются, оцениваются и отбрасываются. Рассмотрим, например, следующие фрагменты SQL (полный сценарий, VfeW_.merge_01.sql, доступен в онлайн-хранилище кода для этой главы): create or replace view avg_val_view 4S select id_par, avg(val) avg_val_tl from t2 group by id_par select tl.vcl, avg_val_tl from tl. avg_val_view where tl.vc2 = lpad(18,32) and avg_val_view.id_par = tl.id_par Обратите внимание на то, что avg_val_view — это агрегатное представле- ний (aggregate view) по таблице t2. Запрос соединяет tl с t2 по столбцу, по ко- торому выполняется агрегация. В этом случае Oracle может использовать один ИЭ двух механизмов для создания корректного результирующего множества: обработать агрегатное представление и затем соединить представление с табли- цей tl или подключить определение представления к запросу и преобразовать его. Начиная с 9i, существует два возможных плана выполнения. 1. План выполнения (9.2.0.6, обработанное представление): SELECT STATEMENT Optimizer=CHOOSE (Cost=lS Card=l Bytes=95) HASH JOIN (Cost=15 Card=l Bytes=95) TABLE ACCESS (FULL) OF 'Tl' (Cost=2 Card=l Bytes=69) VIEW OF 'AVG_VAL_VIEW' (Cost=12 Card=32 Bytes=832) SORT (GROUP BY) (Cost=12 Card=32 Bytes=224) TABLE ACCESS (FULL) OF ' T2' (Cost=5 Card=1024 Bytes=7168)
30 Глава 1. Что понимается под «стоимостью» 2. План выполнения (9.2.0.6, подключенное представление): SELECT STATEMENT Optimizer=CHOOSE (Cost=14 Card=23 Bytes=1909) SORT (GROUP BY) (Cost=14 Card=23 Bytes=1909) HASH JOIN (Cost=8 Card=32 Bytes=2656) TABLE ACCESS (FULL) OF 'T1 ’ (Cost=2 Card=l Bytes=76) TABLE ACCESS (FULL) OF 'T2' (Cost=5 Card=1024 Bytes=7168) Как видно из этих планов выполнения, мой пример позволяет Oracle выпол- нить агрегацию таблицы t2 и затем соединить ее с 11, но также он позволяет Oracle соединить две таблицы, а затем выполнить агрегацию. «Эквивалентный» код для подключенного представления будет выглядеть подобно следующему: select tl.vcl, avg(t2.val) from tl, t2 where tl.vc2 = lpad(18,32) and t2.id_par = tl.id_par group by tl.vcl, tl,id_par Итак, какой из этих вариантов лучше и какой план выполнения выберет оп- тимизатор? Ответ на первую часть вопроса зависит от распределения данных. О Если существует эффективный способ соединить tl и 12, если есть только несколько записей в 12 для каждой записи в 11 и если дополнительный объ- ем данных, добавляемый к каждой записи таблицей 12, мал, то соединение перед агрегацией, вероятно, будет лучшей идеей. О Если не существует эффективного способа соединить tl и t2, существует много записей в t2 для каждой записи в tl и дополнительный объем дан- ных, добавляемый к каждой записи таблицей 12, велик, то агрегация до со- единения, вероятно, будет предпочтительнее. О Я могу рассказать о крайних случаях, которые делают эти варианты возмож- ными, но не могу дать вам немедленный ответ обо всех случаях, располо- женных между ними. Однако оптимизатор может сделать довольно хоро- шую оценку. Ответ на вторую часть вопроса зависит от вашей версии Oracle. Если вы до сих пор работаете на 8i, то Oracle выполнит агрегацию представления, а затем соединение — не принимая во внимание альтернативный способ. Если вы ис- пользуете 9i, Oracle раскроет представление, выполнит соединение, а затем аг- регацию — не принимая во внимание альтернативный способ. Если вы исполь- зуете 10g, Oracle оценит стоимость обоих альтернативных способов отдельно, а затем выберет более дешевый. Вы можете увидеть это, если перезапустите сценарий view_merge_01.sql, но установите трассировку события 10053 (которое будет рассмотрено в дальнейших главах), чтобы сгенерировать трассировочный файл (trace file) вычислений стоимости оптимизатором.
31 видишь, то и получишь (WYSIWYG)? |£люция КОДА ОПТИМИЗАТОРА Йвй переходе между версиями Oracle вы увидите множество примеров механизмов, которые могут ШПЬ включены или отключены на уровне системы или сеанса с помощью скрытых параметров; так- Существует множество механизмов, которые могут быть включены или отключены с помощью подсказок на уровне SQL. Общий эволюционный путь кода оптимизатора, вероятно, выглядит следующим образом: парамет- ры незадокументированы и отключены в первой версии; молча включены, но не оцениваются во фррой версии; включены и оцениваются в третьей версии. В данном конкретном примере вариант раскрытия агрегатного представле- ния и подключения его к остальному запросу зависит от скрытого параметра complex_view_merging, который по умолчанию имеет значение false в 8i, но И 9i по умолчанию имеет значение true. Вы можете заставить 8i выполнять С^МНие комплексных представлений {complex view merging), изменив значение 9TotO параметра — хотя в некоторых случаях вам также понадобится использо- вать подсказку merge () для выполнения слияния. Вы также можете предотвра- тить слияние сложных представлений в 9i и 10g, изменив значение этого пара- метра, но более разумно выборочно использовать подсказку no_merge() — что Я и сделал, чтобы получить первый из двух планов выполнения, показанных ра- нее. У оптимизатора существует множество путей обработки вашего запроса пе- ред его оптимизацией: включение предикатов в представления (predicate pushing), уменьшение уровней вложенности запросов (subquery unnesting) и трансформа- ция типа «звезда» (star transformation) (это, возможно, пример наиболее суще- ственного преобразования запроса) существуют уже давно. Генерация пре- дикатов с помощью транзитивного замыкания (transitive closure) также су- ществовала много лет, да и генерация предикатов из ограничений целостности (constraints) появилась давно и прошла через версии. Все эти возможности (не учитывая возможности перезаписи запроса (query rewrite)) и некоторые другие, которые я даже еще не отметил, намного усложняют точное определение того, что происходит со сложным оператором SQL, если только вы не исследуете Подробности происходящего очень подробно — с трудом пробираясь по трасси- ровке 10053. К счастью, вам не понадобится часто впадать в такие крайности, поскольку подробностей, предоставляемых explain plan, зачастую достаточно, чтобы понять, что произошло некоторое преобразование. Несколько подсказок или проверка параметров, имеющих отношение к оптимизатору, обычно покажут Вам, является ли преобразование принудительным или опциональным, с оцен- кой стоимости или без и какой уровень контроля вы над ним имеете. Что видишь, то и получишь (WYSIWYG)? Еще одна из проблем, с которой вы можете столкнуться, изо всех сил пытаясь раскрыть подробности работы стоимостного оптимизатора: то, что вы видите, Be всегда представляет собой то, что вы получаете.
32 Глава 1. Что понимается под «стоимостью» Существует три различных уровня сложности при эксплуатации. О Во-первых, план выполнения сообщает вам о том, что, по мнению оптимиза- тора, будет происходить во вре.мя выполнения, и создает стоимостную оцен- ку на основании этой модели. О Во-вторых, начинает работать подсистема выполнения (execution engine) и выполняет модель, продиктованную оптимизатором, — но фактический механизм выполнения не всегда идентичен модели (или, иначе говоря, мо- дель не очень хорошо описывает то, что на самом деле происходит). О И наконец, бывают ситуации, когда ресурсы, необходимые для выполнения модели, значительно отличаются от того, каким образом распределены вход- ные данные. Другими словами, план выполнения оптимизатора может не совпадать в точности с путем выполнения времени выполнения, и на скорость пути вы- полнения времени выполнения может повлиять неудачный выбор данных. Примеры вы увидите в главе 9. Заключение Основное назначение этой очень маленькой главы состоит в том, чтобы преду- предить вас, что не существует моментальных ответов на вопросы, подобные «Почему Oracle делает X?» Перед тем как формулировать ответ, вы должны проверить по меньшей мере три режима работы оптимизатора и две (возможно, три) основные версии Oracle. Oracle может выполнять все виды преобразований SQL перед оптимизаци- ей. Это означает, что план выполнения, который вы ожидали увидеть, может быть устранен преобразованием до того, как оптимизатор начнет выполнять ка- кие-либо оценки стоимости. Хотя, с другой стороны, простая арифметика всегда одна и та же. Хитрые тонкости, небольшие усовершенствования, новые возможности и грязные трюки добавлены, чтобы запутать все дело, — но 95 % всего, что делает оптимизатор, можно довольно точно описать и объяснить всего в нескольких главах. Тестовые сценарии Файлы к этой главе, доступные для загрузки, перечислены в табл. 1.1, Таблица 1.1. Тестовые сценарии к главе 1 Сценарий Комментарии first_.rows.sql Демонстрация проблем оптимизации first_rows view_merge_.01.sql Демонстрация изменений в слиянии сложных представлений setenv.sql Установка стандартного тестового окружения для SQL*Plus
2 Табличное сканирование На первый взгляд может показаться, что о табличном сканировании особо нечего щзать, однако вас ждет сюрприз. Так как вычисление стоимости для сканирова- ния таблиц выполняется просто и красиво, я решил использовать его в одной из Первых глав, чтобы продемонстрировать четыре разные стратегии стоимостной Оптимизации, которые развились за эти годы. Кратко перечислим их. О Традиционная. Простой подсчет запросов на чтение. О Системная статистика. Учитываются размер и время запросов на чтение. О Системная статистика (2). Учитываются стоимость процессорных ресурсов И размер и время запросов на чтение. О Системная статистика (3). Учитываются кэширование, стоимость процес- сорных ресурсов и размер и время запросов на чтение. Традиционный метод появился в Oracle 7, и с тех пор он непрерывно улуч- шался. Некоторые из самых серьезных недостатков, особенно невозможность учета кэширования и переменные стоимости операций ввода-вывода, были час- тично устранены в 8i. Я расскажу больше об этих исправлениях в главе 4. Использование системной статистики (оценка стоимости процессорных ре- сурсов) появилось в 9i и все еще улучшается в 10g. «Обычный» вариант сис- темной статистики — номер 2 в нашем списке — учет стоимости процессорных ресурсов и переменных стоимостей операций ввода-вывода. ' Возможность отключения только процессорного компонента системной ста- ТйСтики доступна с помощью недокументированного параметра, эффект кото- рого меняется в зависимости от версии Oracle. А возможность включения аффектов кэширования появилась в 10g, но должна включаться с помощью Недокументированного параметра, поэтому я делаю предположения о будущих направлениях, но не привожу факты. Как видите, код ядра Oracle был расширен, чтобы более эффективно учитывать среду, в которой он выполняется, и тем самым улучшить возможности по созда- нию наиболее подходящего плана выполнения на момент оптимизации запроса. После того как вы изучите все эти четыре разновидности, которые предлага- ет Oracle, вы можете решить, что знаете все, что можно знать о различных методах
34 Глава 2. Табличное сканирование вычисления стоимости табличного сканирования. Но это только начало: пом- ните, что быстрое полное индексное сканирование (index fast full scan) — на са- мом деле вариант идеи табличного сканирования; помните о том факте, что мо- гут быть некоторые аномалии, связанные с автоматическим управлением про- странством в сегментах (automatic segment space management, ASSM); также имейте в виду, что табличное сканирование может осуществляться с помощью параллельного выполнения (parallel execution), при этом могут использоваться секционированные таблицы (partitioned tables). Начало Перед выполнением любых тестов вы должны проделать небольшую подгото- вительную работу, чтобы можно было исследовать подробности планов выпол- нения, которые оптимизатор собирается создавать. На моей системе 9i я использовал сценарий $ORACLE_HOME/rdbms/admin/ uttxplan.sql для создания plan_table, однако я изменил стандартный сценарий, чтобы таблица создавалась как глобальная временная таблица с параметром (on commi t preserve rows), и в качестве владельца использовалась учетная за- пись system. Затем я дал все разрешения на эту таблицу роли publi с и присво- ил ей публичный синоним (public synonym). (В Oracle 10g также можно приме- нять этот подход.) Oracle предоставляет несколько сценариев ($ORACLE_HOME/rdbms/admin/ uttxplp.sql и utlxpls.sql), что позволяет создать план, который включает все по- следние возможности, однако я также подготовил свои собственные сценарии для вывода планов выполнения: plan_run81.sql и plan_run92.sql, доступные в он- лайн-хранилище кода. В 9i вам, возможно, также понадобится выполнить сценарий dbmsutiLsql, чтобы создать пакет dbms_xplan, который Oracle вызывает для получения от- форматированного плана выполнения. Вы можете ознакомиться со сценариями Oracle просто для лучшего понимания моего изложения. ПРИМЕЧАНИЕ Корпорация Oracle уже давно предоставляет сценарии для стандартизации отображения планов выполнения. В 91 эти сценарии изменились — от сложных операторов SQL до простого вызова пакет- ной процедуры. В 10д пакетная процедура была несколько улучшена, чтобы позволить вам про- сматривать план выполнения единственного оператора, который все еще находится в представле- нии v$sql, или даже просматривать набор планов для всех операторов, возвращенных запросом либо к v$sql, либо к таблицам автоматического репозитория загрузки системы (Automatic Workload Repository, AWR). Обращайте внимание на сценарий dbmsutl.sql и пакет dbms_xplan, если вы не хо- тите отстать от последних разработок. Также при исследовании оптимизатора очень важно работать на стабильной платформе. В моем случае наиболее важные особенности стартового тестового окружения следующие.
начало 35 W**-—— @ Размер блока 8 Кбайт. О db_file_multiblock_read_count = 8. Q Локально управляемые табличные пространства (Locally managed tablespaces). 0 Однородные (uniform) экстенты размером 1 Мбайт. & Управление пространством списка свободных блоков — ручное управление пространством сегментов. 0 optimizer_mode = all_rows. 0 Системная статистика (CPU costing) первоначально отключена. Некоторые параметры я буду менять в различных тестовых сценариях, но Начинаю всегда с приведенных настроек. ч Итак, давайте создадим тестовый сценарий. Для этого эксперимента мы не- надолго вернемся к 8i. Полный текст сценария tablescan_Ol.sql приведен в он- даЙН-хранилище кода: execute dbms_random.seed(0) create table tl pctfree 99 pctused 1 aS select rownum id, trunc(100 * dbms_random.normal) val, rpad('x',100) padding from all_objects where rownum <= 10000 1 С помощью этого сценария я создал таблицу с 10 000 записей, также зани- мающую 10 000 блоков из-за необычного значения, которое я выбрал для pctfree. (Этот фокус я часто использую в тестовых сценариях для заполнения большого пространства без генерации большого количества данных; иногда это вызывает удивление, потому что одна запись на блок — очень необычное гра- ничное условие.) Применение представления all_objects, если вы используе- те 8i с установленной Java, представляет собой удобный способ получить около 80 000 записей в рабочей таблице, не прибегая к хитрым уловкам. ПОВТОРЯЕМЫЕ СЛУЧАЙНЫЕ ДАННЫЕ Корпорация Oracle предлагает множество полезных пакетов, которые редко видят свет. Пакет dbms_random — один из них. Этот пакет содержит несколько процедур для генерации псевдослу- чайных данных, включая процедуру для генерации случайных чисел с равномерным распределени- ем, процедуру для генерации случайных чисел с нормальным распределением и пакет для создания случайных строк указанной длины. Я часто использую пакет dbms_random в качестве быстрого, вос- производимого метода генерации тестовых данных, которые приближаются к некоторой реальной ситуации. После генерации статистики по таблице я просто включаю autotrace и запус- каю простой запрос. Так как на таблице нет индексов, Oracle должен выполнить
36 Глава 2. Табличное сканирование табличное сканирование, и в Oracle 8i вы получите план выполнения, подоб- ный этому: select max(val) from tl План выполнения (8.1.7.4) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=1518 Card=l Bytes=4) 1 0 SORT (AGGREGATE) 2 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=1518 Card=10000 Bytes=40000) От autotrace мы получаем три очень важных значения в большинстве строк плана выполнения — совокупную стоимость строки (Cost=), количество записей, которое будет сгенерировано каждой строкой плана (Card=), и общий объем данных, который будет сгенерирован каждой строкой (Bytes=). В данном случае мы можем видеть, что функция тах() вернет только одну запись из 4 байт, стоимость равна 1518 (результат может слегка отличаться в 9i и 10g); но обратите внимание, что большая (фактически вся) стоимость получа- ется из-за полного сканирования таблицы в строке 2, которая передаст 10 000 записей и 40 000 байт строке sort (aggregate), и sort (aggregate) в строке 1, кажется, ничего не стоит. Перед тем как разбираться с арифметикой происходящего, давайте посмот- рим, как мы можем убедить Oracle переоценить стоимость: давайте изменим размер db_f 1 le_multiblock_read_count. alter session set db_file_multiblock_read_count = 16: При удвоении этого параметра мы увидим, что стоимость табличного скани- рования уменьшится — но не в два раза. Мы можем автоматизировать процесс записи стоимости в зависимости от размера db_f i lejnulti block_read_count; в табл. 2.1 приведено несколько результатов, которые будут получены (исполь- зуется Oracle 8i). Обратите особое внимание на столбец со скорректированным значением dbf_mbrc: я вычислял это значение как (10 000 / стоимость), то есть количество блоков в моей таблице, деленное на вычисленную стоимость таб- личного сканирования. Таблица 2.1. Эффекты от изменения количества многоблочных чтений db_6le_multiblocK_read_<x>unt Стоимость Скорректированное значение dbf_mbrc 4 2396 4,17 8 1518 6,59 16 962 10,40 32 610 16,39 64 387 25,84 128 245 40,82 Значение скорректированного dbf_mbrc существенно, так как небольшое дальнейшее исследование показывает, что стоимость сканирования таблицы при использовании традиционных методов оценки стоимости равна количеству
Шу ° _________________________________________________________________37 j^jKOB ниже отметки максимального уровня заполнения (high water mark), де- *|еЖН°му на скорректированное dbfjnbrc. $ Если вы выполните сканирование таблицы из 23 729 блоков с db_file_ ljytt1block_read_count, равным 32, то стоимость сканирования будет показа- как cei 1(23,729/16.39); если вы выполните сканирование таблицы из 99 |$ЮКОВ с db_f ile_multiblock_read_count, равным 64, то стоимость сканиро- >йдая будет равна cei 1 (99/25.84). ' Конечно, будут небольшие ошибки округления; если вы хотите получить бо- Точные значения показателей, создайте очень большую таблицу (или схит- рите — используйте процедуру dbms_stats. $et_table_stats, чтобы показать, <ГО ваша тестовая таблица состоит из 128 000 000 блоков). , Не имеет значения, чему равен размер вашего стандартного блока (хотя, как gy увидите, вам придется повертеть эту формулу, если вы будете работать с не- стандартными размерами блоков), существует только один набор элементар- зцлх значений, который Oracle использует для вычисления стоимости сканиро- вания таблицы. Онлайн-хранилище кода включает сценарий под названием caLc_.mbrc.sql, который генерирует полный набор значений для скорректирован- ного dbfjnbrc. Если вы выполните этот сценарий, то обнаружите, что сущест- вует точка, после которой значение скорректированного dbf_mbrc не будет ме- няться. При старте Oracle обращается к операционной системе для нахождения наибольшего размера физического чтения (largest physical read size), разрешенно- го вашей операционной системой, и по умолчанию использует это значение для ограничения значений, которые вы устанавливаете для db_fi le_multiblock_ fead_count. ОТМЕТКА МАКСИМАЛЬНОГО УРОВНЯ ЗАПОЛНЕНИЯ Вообще, когда сначала создается сегмент таблицы или индекса, пространство для этого сегмента будет заранее выделено из файла данных, но очень небольшой объем такого пространства будет отформатирован для использования. При появлении данных блоки будут форматироваться неболь- шими порциями. В самом простом варианте установки Oracle будет форматировать «следующие пять» блоков из за- ранее выделенного пространства при возникновении необходимости, и отметка максимального уровня заполнения объекта (high water mark, HWM) будет корректироваться, чтобы показать, сколь- ко блоков было отформатировано и доступно для использования. С появлением ASSM в 91 Oracle форматирует группы смежных блоков (по-видимому, обычно 16) за раз, Отметка максимального уровня заполнения все равно определяет наибольший отформатиро- ванный блок в сегменте, но ASSM делает распределение несколько случайным, так что неотформа- Тированные «дырки» (16 блоков, или п х на это значение) могут появиться в середине объекта. ASSM также распределяет один или два блока битовых карт управления пространством (bitmap space management blocks) на экстент. Так что объект на самом деле больше, и стоимость сканирова- ния таблицы увеличивается; для больших объектов отличие не столь уж велико, но существуют и другие побочные эффекты, которые надо учитывать, как вы увидите в последующих главах. Даже в этом очень простом случае важно понимать, что может существовать разница между вычислениями оптимизатора и действиями во время выполне- ния. Скорректированное dbfjnbrc используется только для вычисления стоимо- сти. Во время выполнения Oracle будет пытаться использовать значение пара- метра db_file_multiblock_read_count для табличного сканирования — хотя
38 Глава 2. Табличное сканирование случайные сбои, такие как уже закэшированные граничные блоки экстентов, которые не будут считываться, обычно означают, что последовательное таблич- ное сканирование выберет довольно случайные размеры чтений, от одного до полного значения db_file_multiblock_read_count. (Это обычное изменение времени выполнения, возможно, представляет собой логическое обоснование того, что скорректированное значение dbf_mbrc становится более пессимистич- ным по мере роста db_file_multiblock_read_count.) Перед тем как продолжать, давайте взглянем на немного более сложный за- прос: select val, count(*) from tl group by val План выполнения (8.1.7.4) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=1540 Card=582 Bytes=2328) 1 0 SORT (GROUP BY) (Cost=1540 Card=582 Bytes=2328) 2 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=1518 Card=10000 Bytes=40000) Обратите внимание, что в этом случае стоимость табличного сканирования не изменилась (1518 на второй строке), но суммарная стоимость запроса — 1540 (строка 1): дополнительные 22 появились из-за стоимости выполнения sort (group by). Корректна эта оценка или нет — еще не известно, но она кажется дороговатой для сортировки всего лишь 40 Кбайт данных. ПРИМЕЧАНИЕ Мы подробно рассмотрим стоимость сортировки в главе 13, но сейчас обратите внимание на один момент: шаг sort (aggregate), показываемый в простом запросе, на самом деле не выполняет ника- кой сортировки, вот почему предельная стоимость в строке sort (aggregate) была равна нулю. Oracle просто проверяет каждую поступающую запись относительно текущего максимума. (Эта концепция была расширена для выполнения hash (group by) в 10g Release 2.) Существует множество вариантов использования механизма выполнения, которые не обязательно точно отражаются в планах выпол- нения. Другая важная информация, которую можно получить из плана, — это ко- нечное количество элементов (cardinality — кардинальность). По оценке опти- мизатора, этот запрос вернет 582 записи. В действительности так и есть. Учи- тывая простоту запроса, оптимизатор может использовать одну из сохраненных статистик для столбца val (user_tab_columns ,num_distinct), чтобы полу- чить правильный ответ. Как только запрос становится немного сложнее, коли- чество элементов начинает «плавать» относительно правильного результата. Как вы увидите в главе 10, очень важно, чтобы оптимизатор получал точную оценку количества элементов на каждом шаге своего пути.
утеред и вверх 39 Вперед и вверх Теперь мы переходим к 9i и к важным усовершенствованиям, внесенным в оп- ^ИМизатор. Однако перед тем как рассматривать наиболее критические усовер- шенствования, необходимо изучить возможность поддержки нескольких разме- ров блоков данных и повторить наши тестовые сценарии с использованием Едоков различных размеров. Эффекты размеров блоков Начнем с повторного тестирования нашего базового уровня на более новых Версиях Oracle — по-прежнему используя табличное пространство с размером блока 8 Кбайт — и отметим, что стоимость возросла с 1518 до 1519. Это незна- ШМтельное изменение в 9i, контролируемое параметром _tablescan_cost_ plus_one, который установлен в Та1зев81ив trueB9i. Табличное сканирова- ние в 9i автоматически немного дороже, чем эквивалентное табличное сканиро- вание в 8i (при прочих равных условиях). Это один из тех видов небольших изменений, которые в Oracle появляются удивительно часто, и поэтому очень трудно создать какую-либо документацию об оптимизаторе, которая была бы и точной, и полной. В этом случае изменение может представлять собой необходимость обязательного доступа к заголовочно- му блоку сегмента таблицы при начале табличного сканирования; либо это мо- жет быть просто одним из трюков, встроенных в оптимизатор, чтобы склонить его К использованию индексных путей доступа на очень маленьких таблицах. После обнаружения плюс 1 в стоимости мы можем попробовать несколько вариантов размеров блока (tablescan_Ola.sql и tablescan_Olb.sql в онлайн-храни- дище кода) и посмотреть, что случится, если зафиксировать значение db_f i le_ iuultiblock_read_count равным 8 и оставить оценку стоимости процессорных ресурсов выключенной. В табл. 2.2 приведены следующие три значения для ка- ждого размера блока. О Стоимость табличного сканирования для фиксированного числа блоков (10 000) при изменении размера блока. О Скорректированное dbf_mbrc (10 000 / (стоимость - 1)) для каждого особого размера блока. О Стоимость табличного сканирования, когда размер (в Мбайт) таблицы фик- сирован и размер блока изменяется. Мы можем сделать два существенных вывода из значений в этой таблице. Во-первых, проверьте значения столбца скорректированного dbfjnbrc — где вы видели эти значения (плюс-минус небольшие ошибки округления) ранее? О Для записи размером 2 Кбайт значение 16,39 — это значение, которое мы по- лучили в первом тесте при db_file_multiblock_read_count = 32. Приме- чание: 2 Кбайт х 32 = 64 Кбайт. Q Для записи размером 4 Кбайт значение 10,40 — это значение, которое мы по- лучили в первом тесте при db_file_multiblock_read_count = 16. Приме- чание: 4 Кбайт х 16 = 64 Кбайт.
40 Глава 2. Табличное сканирование О Для записи размером 16 Кбайт значение 4,17 — это значение, которое мы по- лучили в первом тесте при db_f 1 le_multiblock_read_count = 4. Примеча- ние: 16 Кбайт х 4 = 64 Кбайт. Таблица 2.2. Как размер блока влияет на стоимость Размер блока Стоимость сканирования Скорректированное Стоимость сканирования 80 Мбайт 10 000 блоков значение dbfjnbrc 2 Кбайт 611 16,39 2439 4 Кбайт 963 10,40 1925 8 Кбайт 1519 6,59 1519 вКбайтАЭБМ 1540 1540 16 Кбайт 2397 4,17 1199 Все время появляющееся значение 64 Кбайт существенно — это 8 Кбайт х 8, стандартный размер блока базы данных, умноженный на параметр db_file_ multiblock_read_count. Так что можно сделать вывод, что Oracle обрабатыва- ет нестандартные размеры блоков, изменяя значение db_file_multiblock_ read_count для размера чтения в соответствии со стандартным размером бло- ка. Например, если размер блока по умолчанию 8 Кбайт, а сканируемая таблица находится в табличном пространстве с размером блока 4 Кбайт, оптимизатор фактически удваивает значение db_file_multiblock_read_count, чтобы оста- вить размер чтения постоянным, перед тем как выполнять нормальное вычис- ление стоимости. Более того, если вы сгенерируете расширенные трассировочные файлы {extended trace files) события 10046 при выполнении этого эксперимента с таб- личным пространством с размером блока 4 Кбайт, вы увидите, что эта коррек- тировка происходит и во время выполнения. Если вы установите db_file_ multiblock_read_count равным 8 с размером блока по умолчанию, равным 8 Кбайт, вы увидите многоблочные сканирования по 16 блоков при табличном сканировании в табличном пространстве, использующем 4-килобайтовые блоки. Второе заключение исходит из исследования стоимости табличного скани- рования, когда физический размер сегмента данных оставался постоянным. Об- ратите внимание, что стоимость табличного сканирования уменьшается при увеличении размера блока. Если вы переместите объект из одного табличного пространства в другое с отличным размером блока, стоимость сканирования с использованием многоблочных чтений изменится значительно — вы можете обнаружить, что удивительно много операторов SQL в результате изменят свои планы выполнения, и эти изменения могут быть не в лучшую сторону. НАСТРОЙКА С ПОМОЩЬЮ ИЗМЕНЕНИЯ РАЗМЕРОВ БЛОКА Будьте очень осторожны при использовании различных размеров блоков для различных объек- тов — эта возможность была предоставлена для поддержки переносимых табличных пространств, а не как механизм настройки. В некоторых специальных случаях можно получить положительный эффект от замены одного раз- мера блока объекта на другой. Но чаще всего при этом обнаруживается, что побочные эффекты, возникшие из-за изменения в расчетах оптимизатора, перевешивают выгоду от выбранного вами размера блока.
Вперед и вверх 41 И последнее заключение: я привел оригинальные результаты для блока таб- лицы размером 8 Кбайт, но привел также и стоимости для таблицы, которая на- ходилась в табличном пространстве, использующем ASSM. Обратите внима- ние, что стоимость табличного сканирования увеличилась на 1,5 %. Каждый Жртент в моей таблице имеет несколько блоков, используемых для битовых цйрт управления пространством (space management bitmaps) — 2 блока из 128, как мои экстенты были по 1 Мбайт; дополнительная стоимость появляется И значительной степени из-за влияния этих блоков на максимальный уровень Заполнения таблицы. Если вам советуют переместить объект в табличное про- странство ASSM по причинам, связанным с производительностью (особенно цда того, чтобы избежать конкуренции при вставках), будьте осторожны — это Один из небольших раздражающих побочных эффектов ASSM. Оценка стоимости процессорных ресурсов 0ДНЛМ из самых серьезных дефектов оптимизатора до версии 91 было его пред- положение, что одноблочные и многоблочные чтения имели одинаковую стои- мость. Это предположение ущербно по двум причинам. Во-первых, многоблочные чтения часто выполняются дольше, чем одноблочные (особенно на системах со Слишком малым количеством дисков, сконфигурированных с использованием слишком малого размера полос (stripe)). Во-вторых, табличное сканирование может использовать неожиданно большое количество процессорных ресурсов, так как каждая запись проверяется некоторым предикатом. В 9i оба эти недостатка решены за счет использования системной статисти- ки. Вы можете собирать системную статистику за репрезентативные периоды времени или можете просто откалибровать аппаратное обеспечение (по край- ней мере, подсистему ввода-вывода) для получения абсолютных показателей производительности и затем записать системную статистику в базу данных. Например, вы можете выполнять следующие две команды в 9:00 утра и в обед, соответственно, в понедельник утром: execute dbms_stats,gather_system_stats('start') execute dbms_stats,gather_system_stats('stop’) При использовании параметра 'start' получается начальный снимок раз- личных показателей H3v$filestat (на самом деле из таблиц х$, включая неко- торые столбцы, не показываемые представлением v$filestat) и v$sysstat; ' stop' получает второй снимок, обрабатывает различную статистику об актив- ности дисковой подсистемы и процессора за эти три часа и записывает эту ин- формацию в базу данных. Характер собираемых и хранимых данных зависит от версии — возможно, в версии 10g будут внесены изменения, но в таблице Sys.aux_stats$ вы найдете результаты, которые похожи на следующие из 9i (в 10g содержится несколько дополнительных записей): select pname, pvall from sys.aux_stats$ Where sname = 'SYSSTATS_MAIN'
42 Глава 2. Табличное сканирование PNAME PVAL1 CPUSPEED 559 SREADTIM 1.299 MREADTIM 10.204 MBRC 6 MAXTHR 13938448 SLAVETHR 244736 Доступ к значениям с помощью запроса к базовым таблицам, конечно, не одобряемый метод, и Oracle поддерживает PL/SQL API для запроса, установки и удаления системной статистики. Например, мы можем установить четыре ос- новных параметра системной статистики с помощью следующего кода, который находится в сценарии set_system_stats.sql в онлайн-хранилище кода: begi п dbms_stats.set_system_stats('CPUSPEED’,500); dbms_stats.set_system_stats('SREADTIM',5.0); dbms_stats.set_system_stats('MREADTIM’,30.0); dbms_stats.set_system_stats('MBRC',12); end; I alter system flush shared_pool; Я включил в этот фрагмент кода сброс разделяемого пула {shared pool) для напоминания о том, что когда вы изменяете системную статистику, существую- щие курсоры не становятся недействительными (как это было бы для зависи- мых курсоров {dependent cursors) при сборе статистики по таблице или индек- су). Вы должны сбросить разделяемый пул, если хотите гарантировать, что существующие курсоры будут повторно оптимизированы с использованием но- вой системной статистики. ПРИМЕЧАНИЕ Если для сбора системной статистики вы хотите использовать учетную запись с низкими привиле- гиями, вы должны предоставить роль gather_system_statistlcs этой учетной записи. Эта роль опреде- лена в сценарии $ORACLE_HOME/rdbms/admin/dbmsstat.sql. Во многих версиях 9.2 существовала ошибка, которая приводила к нефатальной ошибке Oracle, если выполнялась попытка изменить системную статистику более одного раза в сеансе с использованием одной и той же учетной за- писи. Значения в моем анонимном блоке PL/SQL сообщают Oracle, что; О единственный процессор в моей системе может выполнять 500 000 000 стан- дартных операций в секунду, О среднее время одноблочного чтения — 5 мс; О среднее время многоблочного чтения — 30 мс; О обычный размер многоблочного чтения — 12 блоков. Значения maxthr и slavethr относятся к производительности подчиненных процессов параллельного выполнения. Я полагаю, что эти значения так или иначе контролируют максимальную степень параллелизма {maximum degree of
щеред и вверх 43 ‘-Л " " ' ' parallelism), какую только могут использовать запросы, записывая максималь- ный уровень, на котором подчиненные процессы могли до этого работать, — од- нако я не смог это проверить. Эти два параметра могут иметь значение -1, но если какие-либо другие параметры будут иметь значение -1, алгоритм оценки стоимости процессорных ресурсов не будет использоваться. (Эта ситуация из- менилась в версии 10g, где существуют два набора статистик, один из кото- рых ~ это набор статистики, полученной без рабочей нагрузки (noworkload Если основная статистика становится недействительной, Oracle будет ис- пользовать статистику без рабочей нагрузки.) СИСТЕМНАЯ СТАТИСТИКА 10.2 Хотя в руководстве заявляется, что значение CPUSPEED представляет собой скорость CPU в циклах Й секунду, вы, вероятно, обнаружите, что это значение всегда отличается от действительной скоро- сти процессора. Кристиан Антоньини (Christian Antognini) предположил, что единица измерения это- го параметра представляет собой количество раз, которое Oracle может выполнить некоторую ка- ЛиЙровочную операцию на вашей платформе в секунду. Файл трассировки 10053 от 10g release 2 (10»2.О.1) включает раздел системной статистики, который подтверждает это; например, предыду- щий набор параметров выводится следующим образом (обратите внимание на потерю точности при выводе времени чтения): Using WORKLOAD Stats CPUSPEED: 559 million instructions/sec 5READTIM: 1 milliseconds MREADTIM: 10 milliseconds MBRC: 6.000000 blocks MAXTHR: 13938448 bytes/sec SLAVETHR: 244736 bytes/sec Конечно, CPUSPEED — это просто число, и оптимизатор лишь выполняет некоторые арифметиче- ские действия с этим числом независимо от того, что оно собой представляет. Но если вы будете иметь четкое представление о том, откуда берется это значение, то, возможно, будете более осто- рожны при его использовании. Итак, как же оптимизатор использует эту статистику? Измените первона- чальный тестовый сценарий, чтобы он включал системную статистику, пока- занную ранее, и запустите его под 9i с параметром db_file_multiblock_ read_count, установленным в 4, 8, 16 и 32 (этот тест доступен в виде сценария tablescan_02.sql в онлайн-хранилище кода). Autotrace показывает стоимость этого запроса равной 5031 в первых трех случаях. К сожалению, стоимость В случае 32-блочного размера чтения была 5032, это небольшое, но неожидан- ное изменение. В 10g стоимость была на одну единицу меньше во всех случаях. Правила округления, усечения и так далее немного отличаются в разных верси- ях — общая проблема, которая усложняет определение того, что же происходит На самом деле. Какие выводы мы можем сделать из этого нового теста? Не считая неболь- шой аномалии с 32-блочными чтениями, стоимость больше не изменяется в за- висимости от значения db_file_multiblock_read_count. Первый момент, который надо учесть, состоит в том, что autotrace не выпол- няет всю работу. Нам необходимо полное описание плана выполнения, так что нам требуется соответствующий сценарий explain plan (см. plan_run92.sql в он- лайн-хранилище кода). Столбцы, которые нам особенно интересны в последней
44 Глава 2. Табличное сканирование версии plan_table, — cpu_cost, io_cost и temp_space (последний будет ис- пользоваться только для сортировок и хэш-таблиц, которые будут вынесены на диск). При выводе более сложного плана выполнения мы увидим следующее: SELECT STATEMENT (all_rows) Cost(5031,1,4) New(5001,72914400,0) SORT (aggregate) TABLE ACCESS (analyzed) Tl (full) Cost(5031,10000,40000) New(5001,72914400,0) Три значения, показанные как Cost( , , ), эквивалентны первоначальной стоимости, количеству элементов и количеству байт, показанными autotrace. Три значения, показанные как New( , , ), представляют собой параметры cpu_cost, io_cost и temp_space нового алгоритма cpu_costing; и заключи- тельная стоимость — io_cost + (коэффициент масштабирования) * cpu_cost. Если мы применим формулу, приведенную в главе 1, мы сможем рекон- струировать арифметику оптимизатора: Cost = ( #SRds * sreadtim + #MRds * mreadtim + #CPUCycles I cpuspeed ) / sreadtim Разделив на sreadtim все элементы уравнения, мы можем перестроить его следующим образом: Cost = ( #SRds + #MRds * mreadtim / sreadtim + #CPUCycles / (cpuspeed * sreadtim) ) Так как мы выполняем табличное сканирование, получим: О 5 Rds = 0 (одноблочных чтений); О MRds = 10 000/12 (помните, что ранее мы установили размер многоблочного чтения равным 12). Немного операций ввода-вывода Мы поговорим о компоненте, связанном с процессором, немного позднее, но так как мы установили значения 30 мс и 5 мс для mreadtim и sreadtim, из фор- мулы получим, что стоимость операций ввода-вывода равна (10 000/12) х х (30/5) = 5000. И, конечно, необходимо помнить, что _tablescan_cost_ plus_one установлена в true, так что в конечном итоге получим 5001. Итак, мы видим, что при активной системной статистике для вычисления стоимости ввода-вывода табличного сканирования используется фактическое значение MBRC, а не скорректированное db_file_multiblock_read_count, что к тому же удовлетворяет разнице в скорости между одноблочными и много- блочными чтениями за счет умножения на отношение записанного времени многоблочного чтения к записанному времени одноблочного чтения. В 10g, если вы не собрали статистику, вы обнаружите, что оптимизатор ис- пользует три других параметра статистики из таблицы aux_stats$:
Жеред и вверх 45 ИАНЕ PVAL1 jg'pOSPEEDNW 913.641 -- скорость в миллионах операций в секунду JO5EEKTIM 10 -- время позиционирования на диске в миллисекундах J0TFRSPEED 4096 -- дисковое время передачи в байтах в секунду Если оптимизатор использует эту статистику без рабочей нагрузки, он берет Предыдущие значения, db_block_size и db_filejnultiblock_read_count, 1 синтезирует некоторые значения для sreadtim, mreadtim и MBRC. ф MBRC устанавливается равным значению db_file_multiblock_read_count. О Sreadtim устанавливается в loseektim + db_block_size/iotrf rspeed. q mreadtim устанавливается в ioseektim + db_file_multiblock_read_count * db_block_size/iotftspeed. Другими словами, для запроса на чтение требуется одно позиционирование и затем достаточно большой интервал для передачи данных с диска. Используя предыдущий пример с размером блока 8 Кбайт и размером многоблочного чте- ния, равным 8, оптимизатор установит sreadtim равным 10 + 8192/4096 = •* 12 мс, a mreadtim равным 10 + 8 х 8192/4096 = 26 мс. После получения этих значений (эти значения не сохраняются назад в табли- цу aux_stats$) дальнейшие вычисления выполняются, как уже было рассказано ранее. Но неизбежно возникают осложнения: например, что случится, если вы из- мените значение db_f i le_multiblock_read_count? В табл. 2.3 приведено сравне- ние статистики без рабочей нагрузки с обычной системной статистикой и тради- ционной оценкой стоимости для различных значений db_file_multiblock_ read_count (см. сценарий tablescan_03.sql в онлайн-хранилище кода). В этом примере используется таблица с 10 000 записей из моего первого примера, с одной записью на блок. Для значений стандартной оценки стоимо- сти процессорных ресурсов я намеренно установил значения MBRC, sreadtim и mreadtim так, чтобы они имитировали значения, полученные из статистики без рабочей загрузки. Таблица 2.3. Эффекты статистики с рабочей нагрузкой в 10g db_file_multiblock_read_count Традиционная Стандартная cpu_costing Cpu.costlng без рабочей нагрузки 4 2397 2717 3758 8 1519 2717 2717 16 963 2717 2196 32 611 2717 1936 64 388 2717 1806 128 246 2717 1740 В первом столбце результатов мы видим обычные значения стоимости: как и предполагалось, стоимость снижается с увеличением размера многоблочного чтения. Во втором столбце мы видим эффект стандартного механизма сри_ costing — стоимость определяется фиксированным значением MBRC и поэтому не изменяется при изменении размера многоблочного чтения. И, наконец, мы
46 Глава 2. Табличное сканирование видим странные изменения при cpu_costing со статистикой без рабочей на- грузки — стоимость изменяется в зависимости от размеров многоблочного чте- ния, хотя и намного менее заметно, по мере того как размер чтения становится очень большим. Самый важный момент, касающийся этого изменения в оценке стоимости в 10g, состоит в том, что вы должны знать об этом, перед тем как переходить на данную версию. Если вы не использовали cpu_costing до 10g, то необходимо протестировать все варианты этого механизма из-за побочных эффектов. Если вы перейдете на 10g, не осознавая, что автоматически будет использоваться cpu_costing (вариант со статистикой без рабочей нагрузки), вам потребуется еще раз выполнить тестирование, когда вы начнете собирать системную стати- стику и переключитесь на обычный механизм cpu_costing. Несколько менее важный вопрос (в данном случае) — почему? Почему стои- мость изменяется в случае статистики без рабочей нагрузки? Вспомните: я от- метил, что Oracle не сохраняет синтезированные значения статистики — они пересоздаются для каждого запроса (или, возможно, для каждого сеанса). В ка- честве примера рассмотрим 4-блочное чтение. О MBRC = 4. О sreadtim = 10 + 2 = 12 мс. О mreadtim = 10 + 4 * 2 = 18 мс. У нас есть стандартная формула, и для того, чтобы получить примерно пра- вильный ответ, рассмотрим многоблочные чтения: Cost = ( #SRds + -- нуль в данном случае #MRds * mreadtim / sreadtim + #CPUCycles / (cpuspeed * sreadtim) -- это пока проигнорируем ) cost = (1000/4) * 18/12 = 2500 * 1,5 = 3750 Итак, размер многоблочного чтения меньше, однако синтезированное время многоблочного чтения также меньше, и мы должны выполнить их больше. Я считаю, что значение для ввода-вывода, равное 3750, которое мы получили, достаточно близко к наблюдаемому значению, равному 3758, которое мы мо- жем получить снова, применив компонент формулы, соответствующий процес- сору. Исследование зависимости между системной статистикой без рабочей на- грузки и использованием блоков различного размера оставим вам в качестве упражнения. (Но если хотите, можете просмотреть сценарий tablescan_04.sql из онлайн-хранилища кода.) Конечно, все еще существует множество подробностей, которые мы упусти- ли — мы можем получить их с помощью различных тестовых сценариев. Но вот несколько ответов. О Когда Oracle выполняет табличное сканирование, сколько блоков он пыта- ется прочитать при многоблочном чтении? Равно ли это значение значению MBRC, значению db_file_multiblock_read_count или чему-то еще?
Вперед и вверх 47 Ответ: Oracle все еще пытается использовать фактическое значение db_ file_multiblock_read_count — пропорционально увеличенное или умень- шенное, если выполняется чтение из табличного пространства с размером блока, отличным от размера по умолчанию. У меня значение db_file_ multiblock_read_count фактически было равно 8, так что было глупо уста- навливать значение MBRC, равным 12, но, производя расчеты, оптимизатор верил мне, и затем механизм выполнения читал таблицу по 8 блоков за раз. О Откуда появилась дополнительная единица в стоимости ввода-вывода, если значение db_f 1 le_multi block_read_count было установлено в 32? Я не знаю. Но я нашел несколько других мест в коде оптимизатора, где изменение зна- чения db_f ile_multiblock_read_count вызывало изменение стоимости, которое очевидно не должно было произойти. В настоящее время я предпо- лагаю, что эта странная единица представляет собой ошибку округления или незначительное отклонение, — до тех пор, пока я не найду пример, в котором различие существенно и заслуживает большего внимания. Немного процессора Следующий важный вопрос — как преобразовать cpu_cost из plan_table, пе- ред тем как добавлять это значение к 1 o_cost для получения конечной стоимо- сти, и как вообще получается cpu_cost? Чтобы ответить на первую часть, давай- те посмотрим на формулу, наши записанные значения и буквальные значения в плане выполнения для cpu_cost: О Из формулы стоимость процессорных ресурсов получается равной: #CPUCycles / (cpuspeed * sreadtim). О CPUSPEED = 500 МГц. О sreadtim = 5 мс - 5000 мкс (нормируем единицы времени). О #CPUCycles (значение cpu_cost в plan_table) = 72 914 400. Собрав все значения вместе, получим: 72 914 400 / (500 х 5000) = 29,16576. И оптимизатор округлит стоимость процессорных ресурсов в версии 9.2 (но, очевидно, не сделает этого в 10g); это даст нам стоимость процессорных ресур- сов — 30, что мы и хотели увидеть. Округления Хотя оптимизатор всегда имел обыкновение округлять стоимость, вы обнару- жите, что в 10g появился новый параметр под названием _optimizer_ceil_ cost со значением по умолчанию true. Но, кажется, в этом случае true означа- ет false, a false означает true; и в любом случае это применяется только к стоимости процессорных ресурсов. Намного сложнее точно определить, откуда взялось первоначальное количе- ство операций, равное 72 914 400. Если вы озаботитесь проведением набора
48 Глава 2. Табличное сканирование чрезвычайно утомительных экспериментов, вы, возможно, сможете это опреде- лить — приблизительно — следующим образом. О Стоимость получения блока = X. О Стоимость поиска записи в блоке = У. О Стоимость получения N-ro (в нашем случае 2-го) столбца в записи = (N - 1) х xZ. О Стоимость сравнения числового столбца с числовой константой = А. И когда вы все это определите, константы, вероятно, в любом случае поме- няются в следующей версии. Однако, чтобы вы поняли, насколько мощной может быть оценка стоимости процессорных ресурсов (и почему некоторые запросы выполняются быстрее без видимых на то причин после перехода на 9i), я хочу представить пример ра- боты оценки стоимости процессорных ресурсов в действии. Мощь оценки стоимости процессорных ресурсов Этот пример увидел свет в качестве рабочей проблемы, перед тем как превра- титься в простой тестовый сценарий. Первоначальный код работал под 8i и по- казывал странную проблему с производительностью, так что я пересоздал дан- ные в базе данных 9i, чтобы посмотреть, исчезнет ли проблема, — и она исчезла. Затем я обнаружил: проблема исчезла из-за того, что оценка стоимости процес- сорных ресурсов позволила оптимизатору в 9i сделать нечто такое, чего опти- мизатор в 8i не мог сделать. create table tl( vl, nl, n2 ) as select to_char(mod(rownum,20)), rownum, mod(rownum,20) from all_obj ects where rownum <= 3000 -- Соберите статистику, используя dbms_stats Это простая таблица с тремя тысячами записей. Данные были созданы так (cpu_costing.sql в онлайн-хранилище кода), чтобы продемонстрировать два мо- мента. Обратите внимание, что в столбцах vl и п2 содержится только 20 раз- личных значений, и если не учитывать тип данных, эти столбцы содержат оди- наковые значения. Столбец nl содержит уникальные значения для всей таблицы.
lb оценки стоимости процессорных ресурсов 49 Теперь мы выполним три отдельных запроса и пропустим их через explain ШйО, чтобы можно было отделить стоимости процессорных ресурсов. В этих yrtpocax я использовал подсказку ordered_predicates, чтобы заставить Oracle ^ИМенять предикаты в том порядке, в котором они появляются в выражении $Мге. В этом тесте у 8i нет возможности сделать что-то другое, но 9i может принять решение переупорядочить предикаты на основании оценки стоимости. ^Обратите внимание: этот тест использует значения для системной статистики, Щедрые я привел ранее в этой главе.) /*+ cpu_costing ordered_predicates */ vl, n2, nl from tl wHte vl = 1 Irttf m = te nl = 998 ч select /*+ cpu_costing ordered_predicates */ vl, n2, nl from tl where nl = 998 and П2 = 18 and vl = 1 select /*+ cpu_costing ordered_predicates */ vl, n2, nl fTPffl tl where vl = ’1’ and n2 = 18 and nl = 998 Как вы, возможно, ожидаете, планом выполнения во всех трех случаях явля- лась полное табличное сканирование, и если вы использует^ autotrace, чтобы оп- ределить, что происходит, вы обнаружите только, что стоимость запроса (во всех •Грех случаях) равна 6. Но если вы используете надлежащий запрос к plan_table, который выведет столбцы cpu_cost и filter_predicates (еще один столбец, появившийся в 9i), то увидите результаты, подытоженные в табл. 2.4. Таблица 2.4. Порядок предикатов может повлиять на стоимость Порядок предикатов Стоимость процессорных ресурсов fitter„predicates Vl - 1 1 070 604 and п2 = 18 and nl = 998 TO_NUMBER("T1"."V1")=1 AND "T1"."N2"=18 AND ”T1”.”N1"=998 продолжение &
50 Глава 2. Табличное сканирование Таблица 2.4 (продолжение) Порядок предикатов Стоимость процессорных ресурсов filter_predicates п1 = 998 and п2 = 18 and vl = 1 762 787 "T1"."N1"=998 AND "T1"."N2"=18 AND TO_NUMBER("T1"."V1")=1 vl = 'Г and п2 = 18 and nl = 998 770 604 "T1".'V1"=,1' AND "T1"."N2"=18 AND "T1"."N1"=998 Столбец fiIter_predicates и сообщает точно, что происходит и почему стоимость процессорных ресурсов может измениться, даже если структура пла- на та же самая. На основании информации, которая есть у оптимизатора о раз- личных столбцах, включая минимальные и максимальные значения, количест- во отличных значений и так далее, оптимизатор в 9i может определить (с достаточной степенью точности), что для первого запроса потребуются сле- дующие операции. О Преобразовать столбец vl к числу 3000 раз и сравнить, чтобы получить 150 записей. О Для этих 150 записей сравнить п2 с числом, чтобы получить 8 (точнее, 7,5) записей. О Для этих 8 записей сравнить п1 с числом. Получается 3000 операций приведения и 3158 числовых сравнений. С другой стороны, для выполнения второго запроса потребуются следующие действия. О Сравнить п 1 с числом 3000 раз, чтобы получить только одну запись (вероятно). О Для этой одной записи сравнить п2 с числом, чтобы получить только одну запись (вероятно). О Для этой одной записи преобразовать vl к числу и сравнить. Получается 3002 числовых сравнения и единственное приведение типа — что очень экономит ресурсы процессора. И если в первом запросе вы уберете подсказку ordered_predicates, оптимизатор автоматически выберет переупо- рядочивание предикатов, чтобы порядок соответствовал порядку второго за- проса. (Значение по умолчанию параметра _pred_move_around дает вам под- сказку, что это может произойти.) Просто в качестве заключительного дополнения, в третьем запросе повторя- ется порядок предикатов первого запроса, но используется корректное сравне- ние строк вместо неявного преобразования (implicit conversion). Устраните эти 3000 преобразований, и стоимость процессорных ресурсов упадет с 1 070 604 до 770 604, разница точно равна 300 000. Это позволяет предположить, что функции to_number () была присвоена стоимость, равная 100 операциям процессора. BCHR умер! Да здравствует BCHR! Существует другое большое изменение, которое еще не запущено в работу, но в 10g присутствуют признаки, что нечто существенное все еще ждет своего часа.
BCHR умер! Да здравствует BCHR! 51 (Кстати, не принимайте заголовок раздела всерьез.) В последние несколько лет дее узнали, что коэффициент попадания в буферный кэш (buffer cache hit ratio, BCHR) не такой уж и полезный показатель производительности, хотя сбор ли- 1Й1Й трендов с регулярных снимков может дать вам важные показатели измене- ний или связанных с производительностью аномалий. Одна из самых больших проблем BCHR состоит в том, что он представляет дабой данные, собираемые со всей системы, и один статистически ненормаль- ной объект (или запрос) может сделать это значение полностью бессмыслен- ном. (Для демонстрации этого факта вы можете посетить веб-сайт www. oradedba.co.uk и скачать утилиту Set Your Hit Ratio.) Но что если мы будем собирать попадания в буферный кэш для каждого от- дельного объекта в кэше, обновляя значения каждые 30 минут, поддерживая Скользящие средние и значения трендов? Можем ли мы сделать что-либо по- лезное с таким достаточно специальным набором данных? Один из дефектов ранних версий оптимизатора состоял в следующем: вы- числения работали на основании того, что каждое посещение блока приводит К блоку данных, который до этого никогда не был посещен, и поэтому такое по- мещение превращается в физическое чтение с диска. В 8i было представлено не- сколько параметров для обхода проблем, которые вызывало это поведение (пара- метр optimizer_index_cost_adj, который вы увидите в главе 4, и параметр opti - ltl1zer_index_caching, который вы также увидите в главе 4 и снова в главе И). Но в 9i Oracle собирает статистику о запросах логических блоков и физиче- ских чтениях с диска для каждого сегмента данных. Фактически, похоже, у Oracle есть несколько способов сбора статистики, связанной с кэшем. В 9i было пред- ставлено динамическое представление данных о производительности (dynamic performance view) v$segstat, а в 10g статистика, содержащаяся в представле- нии, была расширена. Вот, например, запрос для обнаружения деятельности для некоторого сегмента данных (таблица 11 использовалась ранее в примерах этой главы) в базе данных 10g: select * from vSsegstat Where obj# = 52799 r TS# OBJ# DATAOBJ# STATISTIC_NAME STATISTIC# VALUE 4 52799 52799 logical reads 6 21600 4 52799 52799 buffer busy waits 1 0 4 52799 52799 gc buffer busy 2 0 4 52799 52799 db block changes 3 240 4 52799 52799 physical reads 4 23393 4 52799 52799 physical writes 5 10001 4 52799 52799 physical reads direct 6 0 4 52799 52799 physical writes direct 7 10000 4 52799 52799 gc cr blocks received 9 0 4 52799 52799 gc current blocks received 10 0 4 52799 52799 ITL waits 11 0 4 52799 52799 row lock waits 12 0
52 Глава 2. Табличное сканирование 4 52799 52799 space used 14 6 4 52799 52799 space allocated 15 82837504 4 52799 52799 segment scans 16 4 Ценность этой информации для поиска проблем неоценима — особо обрати- те внимание на элементы, показывающие конкуренцию за обладание ресурсами (buffer busy wai ts, gc buffer busy, ITL waits, row lock wai ts) и явно указы- вающие на табличные сканирования и быстрые полные индексные сканирова- ния (segment scans). Попутно, отсутствующие значения 8 и 13 были намеренно исключены опре- делением представления и относятся к меткам времени и служебным ожидани- ям ITL (interested transaction list — список заинтересованных транзакций) со- ответственно; последнее может быть особенно интересно в случае расщеплений блоков индексов. Ошибки V$SEGSTAT в Si Хотя v$segstat — это чрезвычайно ценное представление для мониторинга производительности (конечно, если собирать регулярные снимки через корот- кие интервалы времени), в 9i в нем есть несколько ошибок. В ранних версиях, когда вы усекали (truncate) или перемещали (move) табли- цу, data_object_id (физический идентификатор) сегмента изменялся, но в v$segstat статистика для объекта не очищалась. В более поздних версиях табличная статистика очищается, но не очищаются более не существенные ста- тистические данные для соответствующих индексов. Более того, в текущих версиях (9.2.0.6 и 10.1.0.4, во время написания этой книги) фактически любой запрос к этому представлению или к соответствую- щим таблицам х$, похоже, приводит к невосполнимой утечке памяти размером около 15 Кбайт из SGA (если только запрос не содержит выражение order by), что в конечном итоге может привести к остановке экземпляра с хроническими ошибками ORA-04031. Для целей оптимизатора, тем не менее, интересной статистикой являются логические и физические чтения. Я был немного удивлен тем фактом, что мой пример показал больше физических чтений, чем логических. Хотя статистика логических чтений дискретна, и это может объяснить несоответствие, особенно если учесть, что я использовал этот объект несколько экстремально. (Другой вариант состоит в том, что предложение выборки пакета dbms_stats при оцен- ке статистики таблицы может привести к варианту табличного сканирования механизма времени выполнения, который будет показывать больше физиче- ских операций ввода-вывода, чем логических.) Если у нас есть данные, которые сообщают нам долю логических чтений объекта, которые обычно превращаются в физические чтения, и мы получаем значимые показатели из этой информации, возможно, мы сможем вернуть этот показатель назад в следующий проход при оптимизации запроса к этому объек- ту. Правда, с этим предположением связана одна маленькая проблема. Как вы определите, что Oracle делает (или собирается делать) с этой информацией?
g£HR умер! Да здравствует BCHR! 53 Вокруг разбросаны фрагменты различных доказательств — на самом деле их ^же слишком много. Рассмотрим таблицы sys . tab_stats$ и sys. i nd_stats$. Эти таблицы по- лились в 10g, и они обе включают столбцы под названием obj#Hcachehit — очевидно, некоторый вид коэффициента попадания в буферный кэш на (логи- ческом) уровне объектов. Ни одна из этих таблиц, похоже, не заполняется по умолчанию (пока), хотя они действительно используются в различных рекур- сивных запросах. е Рассмотрим также таблицу sys.cache_stats_l$ со следующими столбца- ми, взятыми из ее описания (особенно обратите внимание на inst_id — раз- личные экземпляры RAC могут иметь различные, локализованные коэффици- енты попадания в буферный кэш). desc cache_stats_l$ Name Null? Type DATAOBJ# NOT NULL NUMBER INST.ID NOT NULL NUMBER CACHED_AVG NUMBER CACHED_SQR_AVG NUMBER CUR AVG NUMBER CHR SQR AVG NUMBER UR SUM NUMBER lgrj-ast NUMBER PHRJ.AST NUMBER Все это очень похоже на попытку хранить некоторый вид скользящих пока- зателей о кэшировании и уровнях попадания в буферный кэш для сегментов — и мы увидим, что для обновления этой таблицы сложный оператор слияния за- пускается через регулярные интервалы процессом mmon (manageability monitor process). Быстрая проверка пакета dbms_stats также показывает, что параметр сбора статистики кэширования был добавлен во многие процедуры для сбора стати- стики. Вы увидите, что эти процедуры выполняют доступ к таблице cache_ stats_l$, если вы выполните нечто подобное этому: execute dbms_stats.gather_table_stats(user,'tl'.stattype => 'cache') И наконец, обратите внимание на два скрытых параметра: О _cache_stats_moni tor (значение по умолчанию TRUE); О _optimizer_cache_stats (значение по умолчанию FALSE). Первый параметр включает сбор статистики кэширования, второй — вклю- чает использование статистики кэширования для оптимизации, и поскольку их можно устанавливать на уровне сеанса, эксперименты с этими параметрами на самом деле не принесут вреда (на тестовой базе данных). set autotrace traceonly explain alter session set "_optimizer_cache_stats" = true; select count(*) from tl; alter session set "_optimizer_cache_stats“ = false;
54 Глава 2. Табличное сканирование select count(*) from tl; set autotrace off В этом конкретном тесте стоимость табличного сканирования с параметром _optimizer_cache_stats, установленным в false, была равна 5030 (как и сле- довало из 2 Кбайт блока в тесте, приведенном ранее). Когда я переключил _optimizer_cache_stats в true, стоимость упала до 5025. К сожалению, я не смог увидеть никаких операторов SQL, выполняющих доступ к cache_stats_l$ в процессе оптимизации, когда я включил статистику кэширования. С другой стороны, оптимизатор всегда выполнял доступ к пус- тым таблицам tab_stats$ и ind_stats$ для оптимизации нового оператора. Я все еще не имею полного представления о том, что же значит весь этот разбросанный набор наблюдений, — но, похоже, в корпорации Oracle есть, по меньшей мере, две группы, которые работают над идеями о включении локали- зованных коэффициентов попадания в буферный кэш в оптимизатор. Если (или когда) эта возможность выйдет в работу, она решит некоторые сущест- вующие проблемы и, конечно, создаст некоторые новые. Как, например, вы оп- ределите, что представлял собой план выполнения, через 24 часа после того, как проблема с производительностью появилась и пропала, когда локализован- ные значения попадания в буферный кэш изменились или если вам разрешено выполнять диагностику только на тестовой системе, значения попадания в бу- ферный кэш которой не имеют никакого отношения к значениям, получаемым с производственной системы? (Вам придется лицензировать AWR (автомати- ческий репозиторий нагрузки), нравится он вам или нет.) Параллельное выполнение Параллельный запрос (parallel query), или параллельное выполнение (parallel execution), как он стал называться в 8i, — это еще одна возможность базы данных Oracle, при которой вроде бы небольшое изменение в стратегии оценки стоимости при- водит к значительным изменениям в вычислениях стоимости. Лучше всего это можно продемонстрировать в нашем замечательйом простом табличном скани- ровании; изменение в этой простой операции настолько существенно, что у вас не останется никаких сомнений в том, насколько большое изменение последует в случае более сложного запроса. Пересоздайте таблицу из нашего первого теста и выполните следующие за- просы к ней с включенной автоматической трассировкой (au tot race). Повто- рите следующие запросы в 8i, 9i и 10g сначала с отключенной системной стати- стикой (сценарий paralleLsql в онлайн-хранилище кода), а затем используя сис- темную статистику, определенную ранее в этой главе (см. сценарий parallel_2.sql в онлайн-хранилище кода): select /*+ parallel(tl,l) select /*+ parallel(tl,2) select /*+ parallel(tl,3) select /*+ parallel(tl,4) select /*+ parallel(tl,5) */ count(*) */ count(*) */ count(*) */ count(*) */ count(*) from tl; from tl; from tl; from tl; from tl;
Параллельное выполнение 55 select /*+ parallel(tl,6) */ count(*) from tl; select /*+ parallel(tl,7) */ count(*) from tl; select /*+ parallel(tl,8) */ count(*) from tl; Предположим, что вы установили параметр paraUel_max_servers по край- ней мере равным 8. Тогда вы должны получить результаты для стоимости за- проса, аналогичные приведенным в табл. 2.5. Таблица 2.5. Эффекты различных степеней параллелизма Степень 81 9i (I/O) 10д (I/O) 9i (CPU) 10g(CPU) Последовательно 1518 1519 1519 5031 5030 7 1518 760 844 2502 2779 3 1518 507 563 1668 1852 4 1518 380 422 1252 1389 5 1518 304 338 1002 1111 6 1518 254 282 835 926 7 1518 217 242 716 794 а 1518 190 211 627 695 Пока проигнорируем последние два столбца, которые показывают результа- ты с включенной оценкой стоимости процессорных ресурсов, и сфокусируемся на первых трех наборах стоимости. Обратите внимание, что значения, получен- ные в 9i и 10g, весьма не похожи друг на друга, но, по крайней мере, они доста- точно близки к значению (стоимость последовательного выполнения / степень параллелизма). Но наиболее очевидная особенность состоит в том, что стои- мость в 8i никогда не изменяется, независимо от степени параллелизма. По существу, 8i оценивает и оптимизирует ваш запрос для наилучшего по- следовательного пути выполнения, а затем выполняет его параллельно. С дру- гой стороны, 9i предполагает, что может запустить полностью свободное от коллизий 100-процентное параллельное выполнение, эффективно выполняя оптимизацию для набора данных, уменьшенного на коэффициент (равный сте- пени параллелизма). С учетом небольших ошибок округления, значения из 10g приводят к пред- положению, что в вычислениях стал использоваться коэффициент эффектив- ности параллельного выполнения (parallel efficiency factor), равный 90 %. Факти- ческие значения предполагают следующую формулу, зависящую от версии, для параллельного табличного сканирования: 81: Стоимость для степени N = стоимость последовательного выполнения. 91: Стоимость для степени N = ceiI(стоимость последовательного выполнения / N). 10g: Стоимость для степени N = ceiI(стоимость последовательного выполнения / (0,9 * N)). Это дает вам три момента для рассмотрения. Во-первых, 8i не выполняет корректной оптимизации параллельных запросов. Во-вторых, при обновлении стоимость параллельного выполнения может значительно измениться, и вы мо- жете оказаться не в состоянии предсказать побочные эффекты. В-третьих, вы- числения, используемые в 9i, предполагают, что между рабочими процессами
56 Глава 2. Табличное сканирование не будет абсолютно никаких взаимных помех — в большинстве случаев это не сильно соответствует действительности. Изменения между 8i и 9i контролируются параметром optimizer_percent_ parallel (этот параметр скрыт в 9i, как и _optimizer_percent_parallel). В 8i значение по умолчанию было нулевое — это соответствует вычислениям для последовательного выполнения. В 9i значение по умолчанию 101, что за- ставляет Oracle выполнять свои вычисления на основании 100-процентного па- раллелизма. Теоретически вы можете присвоить этому параметру любое значение между О и 101. Если вы это сделаете, оптимизатор выполнит линейную интерполяцию между последовательной стоимостью (значения Resc в трассировке 10053) и па- раллельной стоимостью для полного параллелизма (значения Resp в трассиров- ке 10053), а затем выберете точку, соответствующую вашему значению, на пря- мой, проведенной от последовательной стоимости до параллельной. (Тот же самый механизм интерполяции применяется для сортировки и некоторых ас- пектов хэш-соединений, но простое табличное сканирование представляет со- бой лучший способ продемонстрировать эту арифметику.) Например, из таблицы результатов для 9i видно, что стоимость степени па- раллелизма степени 4 равна 380, а стоимость последовательного сканирования равна 1519. Если мы установим параметр _optimizer_percent_parallel в 75, то конечная стоимость будет вычислена как 25 % от 1519 плюс 75 % от 380, что равняется 664,75. Вы можете отметить из значений в таблице, что параллельное выполнение со степенью 4 на 75 % не даст вам такой же стоимости, как парал- лельное выполнение со степенью 3 на 100 %. Еще один момент выходит на сцену в 9i и 10g, если вы установите параметр parallel_adaptive_multi_user в true (это значение по умолчанию в 10g). Когда это значение установлено, выполнять запросы со степенью параллелизма по умолчанию может только ограниченное число пользователей — и на моих тестовых системах это ограничение равнялось единице в 9i и двум в 10g. Так что оптимизатор эффективно оценивает стоимость параллельного выполнения на основании того, что не больше одного или двух параллельных запросов бу- дут выполняться в любой момент времени. Это ограничение устанавливается скрытым параметром _parallel_adaptive_max_users, но не играйте с ним, поскольку это одно из значений параметров, которые используются Oracle при старте для вычисления значения parallel_max_servers, так что изменение даст побочные эффекты. Другие странности происходят при оценке стоимости параллельных таблич- ных сканирований. Если вы не включили системную статистику (оценку стои- мости процессорных ресурсов), в вычислениях используется то же самое значе- ние скорректированного dbf_mbrc, как и при последовательном сканировании. Но параллельное сканирование — это чтения в режиме прямого доступа {direct path reads), другими словами, чтения, которые минуют буфер данных (data buffer), так что им не надо заботиться о побочных эффектах, связанных с бло- ками, которые уже находятся в кэше, и поэтому почти неизбежно их размер бу- дет равен полному значению db_f 1 le_mul ti block_read_count. Вычисление не подходит для этого отличного механизма.
(Параллельные сканирования и чтения в режиме прямого доступа 57 Параллельные сканирования и чтения я режиме прямого доступа Параллельные сканирования используют чтения в режиме прямого доступа для рбхода буфера данных и чтения блоков напрямую в локальную (PGA) память. Это помогает уменьшить воздействие на буфер данных (но в некоторых специ- альных случаях это может означать, что вам потребуется небольшой буфер Oracle и большой буфер файловой системы). ' Но если блок в буфере данных «грязный» (более новый, чем блок на диске), ТО вы можете подумать, что прямое чтение «не увидит» последнюю версию И поэтому может получить неправильный результат. Для решения этой пробле- мы параллельный запрос сначала выполняет контрольную точку сегмента {seg- ment checkpoint), чтобы все «грязные» блоки в сегменте были записаны на диск, Перед тем как запрос будет выполнять чтение. (Стоимость определяется стати- стикой DBWR parallel query checkpoint buffers written в 10g или иначе блокировками с очередями типа ТС.) В некоторых редких случаях, когда смешаны большой буфер данных, нагру- женная OLTP-система и параллельные выполнения отчетов, это может привес- ти к проблемам с производительностью — работа, выполняемая database writer (DBWR), который проходит очередь контрольных точек {checkpoint queue) для Поиска «грязных блоков», может иметь нежелательные эффекты на работе OLTP-системы. Теперь более подробно рассмотрим столбцы 5 и 6 в таблице, представляю- щей набор результатов с включенной системной статистикой (оценкой стоимо- сти процессорных ресурсов). Что видно наиболее точно — это то, что оптимиза- тор в вычислениях использует сохраненное значение для статистики MBRC. И снова мы можем увидеть, что в 10g в вычисления был внесен коэффициент 90 %. Но это приводит к другой проблеме. Oracle снова будет использовать факти- ческое значение параметра db_file_multiblock_read_count для выполнения прямого чтения из таблицы, так что стоимостный оптимизатор, очевидно, ис- пользует неподходящее значение в вычислении стоимости. В этом случае, тем не менее, существует преимущество, перевешивающее недостатки. Если вы за- нимаетесь в основном большими параллельными табличными сканированиями, ТО код, который генерирует MBRC, будет видеть только эффекты ваших много- блочных чтений с прямым доступом, так что значения, собранные dbms_ Stats. gather_system_stats, будут корректны. Обратная сторона медали, ко- нечно, состоит в том, что если вы используете смешанную OLTP/DSS-систему с большим количеством многоблочных чтений, которые выполняются не парал- лельно, то значение MBRC, вероятно, будет неподходящим и для последователь- ных, и для параллельных запросов — оно будет находиться где-то между двумя идеальными значениями. Существует еще одна небольшая хитрая особенность оценки стоимости про- цессорных ресурсов, которую можно наиболее четко увидеть в значениях 9i для параллелизма, равного двум. Стоимость уменьшается с 5031 до 2501 при пере- ходе от последовательного выполнения к параллельному со степенью два, но,
58 Глава 2. Табличное сканирование как мы видели ранее, 5031 — это сумма 5001 (стоимость ввода-вывода) и 30 (стоимость процессорных ресурсов). Когда мы переходим к параллельному выполнению, оптимизатор «теряет» стоимость процессорных ресурсов. Фактически проверка файлов трассировки от 9i и 10g показывает, что присутствует небольшой компонент, связанный со стоимостью процессорных ресурсов. Ошибка ли это? Вероятно, нет. Большая доля стоимости процессорных ресурсов при доступе к записи происходит из стоимости процессорных ресурсов для поиска, применения блокировки (lat- ching) и закрепления буферизованного блока — но параллельные запросы не используют кэш, они выполняют прямое чтение, так что, возможно, большая часть стоимости процессорных ресурсов исчезнет. Быстрое полное индексное сканирование Говоря о быстром полном индексном сканировании, очень важно помнить, что это вероятный план выполнения. Оно не появляется очень часто в текущих версиях оптимизатора, но это путь выполнения, который может появиться без использования подсказок. В действительности для запроса, который ссылается только на набор столб- цов из индекса, Oracle может решить использовать индекс подобно тощей таб- лице с небольшим количеством примешанного «мусора» (например, идентифи- каторы строк (rowids) и бессмысленные в данном случае блоки ветвления (branch blocks)). Это означает, что Oracle может читать сегмент в физическом порядке блоков, используя многоблочные чтения, и по мере чтения отбрасы- вать блоки ветвления. Элементы индекса не будут возвращены в порядке ин- декса, так как Oracle не будет переходить между листовыми блоками (leaf block), используя обычные указатели, но теоретически стоимость любой сорти- ровки, которая может потребоваться после этого, может превысить преимуще- ство более быстрого получения данных с диска. ПРИМЕЧАНИЕ Полные индексные сканирования выполняются точно также, как табличные сканирования. Они ис- пользуют многоблочные чтения и «большие» индексы (определяемые теми же 2 % от количества буферов блоков при старте), которые загружаются в конец списка LRU. И так же они страдают от ошибки, которая влияет на табличные сканирования: значение количества использования буфера не увеличивается (даже для «маленьких» индексов/таблиц), если блок был загружен с помощью полного быстрого индексного сканирования. Эта ошибка была исправлена в 10д. Например, пересоздав первый набор данных, мы можем сделать следующее (см. сценарий indexjfs.sql в онлайн-хранилище кода): create index tl_i on tl(val); execute dbms_stats.gather_table_stats(user,'tl',cascade=>true); set autotrace traceonly explain
быстрое полное индексное сканирование 59 ' "" ' " ' ' " —————— select count(*) from tl where val > 100; ддан выполнения (9.2.0.6) -------------------------------------------------------- I’4 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=5 Card=l Bytes=4) SORT (AGGREGATE) 1 INDEX (FAST FULL SCAN) OF 'Т1_Г (INDEX) (Cost=5 Card=3739 Bytes=14956) Неудивительно, что для вычисления стоимости полного быстрого индексно- jti сканирования используется та же арифметика, что и для стоимости таблич- ного сканирования. Однако результаты часто кажутся немного неправильными, Игр может вызвать у вас вопрос: где оптимизатор находит значения, исполь- зуемые для расчета количества блоков, которое необходимо для формулы таб- личного сканирования? Когда вы генерируете статистику для таблицы, одним из результатов явля- ется количество блоков ниже отметки максимального уровня заполнения. Ко- гда вы генерируете статистику для индекса, вы получаете количество листовых блоков и высоту В-дерева индекса; но вы не получаете никакой информации о блоках ветвления или о максимальном уровне заполнения сегментов индекса. Итак, какое значение оптимизатор использует в качестве основы для вычис- ления стоимости полного быстрого индексного сканирования? Ответом, похо- же, является количество листовых блоков, что довольно разумно, потому что в хорошем, достаточно случайно сгенерированном индексе без катастрофиче- ских обновлений и удалений количество листовых блоков, вероятно, будет рав- ЙО 1 % общего числа блоков ниже максимального уровня заполнения. Удивительно, что, если вы не собрали статистику по индексу, Oracle для по- лучения правильного ответа использует свое знание о максимальном уровне за- полнения из заголовочного блока сегмента индекса. Однако возможно, что в некоторых сценариях вы можете привести индекс д необычное состояние, в котором количество заполненных листовых блоков будет намного меньше, чем количество блоков ниже максимального уровня за- полнения (неудачное или неподходящее использование команды coalesce мо- жет привести к такому эффекту — иногда правильный путь решения проблемы Состоит в перестроении индекса), что приводит к несоответствующей стоимо- сти полного быстрого индексного сканирования, тогда как сканирование диапа- зона было бы намного эффективнее. МЕТОДЫ ИССЛЕДОВАНИЯ Вы можете задаться вопросом, как я пришел к заключению, что стоимость полного быстрого ин- дексного сканирования диктуется статистикой leaf_blocks. Первоначально вместо постоянного пересоздания одного и того же индекса с различными значе- ниями pctfree я менял значение leaf_blocks для индекса с помощью пакетной процедуры dbms_ Stats.set_index_stats, чтобы посмотреть, что случится со стоимостью запроса. Затем я изменял blevel, и так далее. Сценарий hack stats.sql в онлайн-хранилище кода демонстрирует этот метод.
60 Глава 2. Табличное сканирование Этот тип проблемы может вызвать неожиданные побочные эффекты. Если вы «освободили» диапазон листовых блоков, то команда analyze будет показы- вать количество листовых блоков как число листовых блоков, которые в на- стоящее время существуют в структуре индекса, тогда как процедура dbms_ stats.gather_index_stats показывает это количество как число листовых блоков, которые действительно содержат данные. (Когда все записи удалены из листового блока, он временно остается в дереве, но также добавляется к списку свободных блоков сегмента индекса.) Когда вы переключитесь с устаревшей analyze к стратегической dbms_stats, вы можете обнаружить, что некоторые операторы SQL неожиданно начали использовать полные быстрые индексные сканирования «без очевидной на то причины». Эта проблема может появиться только в запросах (или строках плана вы- полнения), которые могут быть полностью выполнены с использованием ин- декса, и в настоящее время это не столь обычное явление. Но вы никогда не знаете, какая захватывающая новая возможность будет представлена в следую- щей версии оптимизатора и может ли проблема неожиданно возникнуть. Уди- вительно, как часто улучшение в оптимизаторе помогает 99 людям из 100 — и вызывает сильные головные боли у одного странного человека. Здесь, напри- мер, представлен план выполнения, который является совершенно разумным, но еще недоступен оптимизатору: План выполнения (? 11.1.0.0 ?) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=30 Card=18 Bytes=144) 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=30 Card=18 Bytes=144) 2 1 SORT (ORDER BY) 3 2 INDEX (FAST FULL SCAN) OF 'Т1_Г (INDEX) (Cost=12 Card=18) Этот план (я должен подчеркнуть, что это полный вымысел) начинается с полного быстрого индексного сканирования для поиска списка идентифика- торов записей на основании неведущих столбцов, выполняет сортировку по идентификатору блока, а затем выполняет доступ к таблице. Если бы у вас был индекс, существенно меньший, чем таблица, на которой он основан, и вам надо было получить только несколько записей, соответствующих критерию, кото- рый может быть протестирован в индексе (но не идентифицируется ведущим набором столбцов индекса), то теоретически этот план выполнения был бы более предпочтителен, чем использование индексного сканирования с пропусками. В настоящее время вы можете подобраться весьма близко к эмуляции этого пути выполнения с SQL-кодом, подобным следующему (также находится в сце- нарии index_ffs.sql в онлайн-хранилище кода): select /*+ ordered no_merge(tb) use_nl(ta) rowid(ta) */ * from ( select /*+ index_ffs(tl) */ rowi d from tl where val > 250 order by rowid
акционирование 61 ) tb, tl ta Where ta.rowid = tb.rowid Диан выполнения (9.2.0.6) @ SELECT STATEMENT Optimizer=ALL_ROWS (Cost=1834 Card=1818 Bytes=209070) | 0 NESTED LOOPS (Cost=1834 Card=1818 Bytes=209070) | 1 VIEW (Cost=16 Card=1818 Bytes=12726) J 2 SORT (ORDER BY) (Cost=16 Card=1818 Bytes=19998) 4 3 INDEX (FAST FULL SCAN) OF 'Т1_Г (INDEX) (Cost=5 Card=1818 Bytes=19998) J 1 TABLE ACCESS (BY USER ROWID) OF 'Tl' (TABLE) (Cost=l Card=l Bytes=108) Если оптимизатор когда-либо получит этот новый путь доступа, некоторые дюди неожиданно получат проблемы при обновлении, потому что они попадут в гу самую группу несчастливых администраторов, имеющих индексы, в кото- рых количество используемых листовых блоков всегда гораздо меньше количе- ства блоков ниже уровня максимального заполнения. Внезапно у некоторых даодей появится серьезное основание для регулярного перестроения несколь- КцХ индексов. Секционирование критическая проблема с секционированными (partitioned) объектами может быть лучше всего продемонстрирована простым примером, в котором мы должны по- смотреть только на кардинальность (оценочное количество записей), а не на стои- мость плана выполнения (см. сценарий partition.sql в онлайн-хранилище кода): create table tl ( part_col not null, id not null, small_vc, padding ) partition by range(part_col) ( partition p0200 values less than ( 200), partition p0400 values less than ( 400), partition p0600 values less than ( 600), partition p0800 values less than ( 800), partition pl000 values less than (1000) nologging as with generator as ( select --+ materialize rownum id from all_objects where rownum <= 5000 ) select trunc(sqrt(rownum-l)), rownum-1,
62 Глава 2. Табличное сканирование lpad(rownum-l,10), грасЦ’х’ ,50) from generator vl, generator v2 where rownum <= 1000000 -- Здесь соберите статистику, используя dbms_stats Я использовал вынос подзапросов (subquery factoring) (специфичную воз- можность 9i) для генерации большой таблицы, в данном случае с подсказкой materialize, чтобы Oracle не переписывал подзапрос с названием generator как вложенное представление и затем выполнял оптимизацию большого запу- танного оператора. Даже минимальная установка базы данных будет иметь бо- лее 3000 записей в представлении all_objects, так что этот фокус подходит для генерации таблицы с количеством записей до 9 000 000 (900 000 000 или больше, возможно, при полной установке Java), с использованием только двух копий вынесенных подзапросов. Генерируя столбец секционирования с помощью функции trunc(sqrtO), я удобно создал секционированную таблицу, в которой количество записей в каждой секции увеличивается при переходе от секции к секции. select partition_name, num_rows from user_tab_partitions order by parti ti on_posi tion PARTITION_NAME NUM_ROWS P0200 40,000 P0400 120,000 P0600 200,000 P0800 280,000 P1000 360,000 После создания таблицы и вызова dbms_stats. gather_table_stats для вычисления статистики таблицы я выполнил три запроса к таблице и тщатель- но проверил их планы выполнения. Первый запрос использовал символьные константы и имел предикат, который ограничивал его некоторой одной секци- ей. Второй запрос также использовал символьные константы, но предикат пере- секал две последовательные секции. Последний запрос был таким же, как и вто- рой, но использовал переменные связывания (bind variables) вместо констант. ПРИМЕЧАНИЕ Использования autotrace обычно достаточно для быстрой проверки того, что делает оптимизатор, но этот метод имеет ограничения. Одно из наиболее серьезных ограничений проявляется в случае секционированных таблиц, когда autotrace оказывается неспособной отобразить три наиболее важ- ные столбца из planjzable, которые помогают вам понять, насколько эффективной была ваша стра- тегия секционирования. Эта проблема была решена в 10g release 2, где autotrace была переписана, чтобы вызывать пакет dbms xplan.
(Акционирование 63 Вот эти запросы — каждый сопровожден планами выполнения, полученны- ми с помощью dbms_xplan из версии 9.2.0.6, с описанием того, как оптимизатор определил столбец кардинальности (Rows): /»+ Query 1 - одна секция */ |elect count(*) from 11 where part_col between 250 and 350 t Id | | Operation I Name | Rows | | Bytes | Cost | Pstart| Pstop | 0 1 | SELECT STATEMENT 1 1 1 1 1 4 | 193 | 1 1 1 | SORT AGGREGATE 1 1 1 1 1 4 | 1 1 1 1 2 | TABLE ACCESS FULL 1 Tl I 61502 | | 240K | 193 | 1 2 | 2 1 I - fiIter("Tl"."PART_COL">=250 AND "Tl".”PART_COL"<=350) Оптимизатор определил секцию 2 как единственную секцию, к которой бу- дет выполнено обращение, — обратите внимание на столбцы pstart и pstop. Вели мы проверим статистику для part_col для этой одной секции, мы уви- ДИМ, что в этой секции 120 000 записей, значения столбца варьируются от 200 до 399 и он содержит 200 уникальных значений. Из всего доступного диапазона данных нам необходим диапазон от 250 до 350’, то есть около 120 000 х (350 - 250)/199 плюс дополнительные 120 000 х х 2/200, которые Oracle добавляет из-за того, что наш диапазон замкнут на обо- их Концах. (Подробно об этом я расскажу в главе 3.) Поэтому суммарное число Записей равно 120 000 х ((100/199) + 1/100) = 61 502 (округлили с 61 501,5). Итак, с единственной известной на момент синтаксического анализа секци- ей Oracle использовал статистику уровня секции. /*+ Query 2 - несколько секций */ Select count(*) from tl where part_col between 150 and 250 | Id | Operation | | Name | | Rows | Bytes | Cost | I Pstart| Pstop| 1 0 1 SELECT STATEMENT | 1 1 1 1 1 4 1 257 | 1 1 1 I I SORT AGGREGATE | 1 1 1 1 1 4 | 1 1 1 1 1 2 | PARTITION RANGE ITERATOR | 1 1 1 1 1 1 1 1 1 2 | 1*3 I TABLE ACCESS FULL | 1 Tl | | 102K | 398K | 257 | 1 1 2 | 3 • filter("Tl"."PART_COL">=150 AND "Tl".”PART_COL"<=250) Мы пересекли границу секции — Oracle отметил тот факт, что мы будем об- ращаться к секциям 1 и 2. Проверив статистику таблицы и столбца, мы найдем, ЧТО секции содержат 160 000 записей, а столбец содержит 400 уникальных зна- чений в диапазоне от 0 до 399, из которого нам необходим диапазон от 150 до 250. Давайте применим ту же формулу, что и ранее: 160 000 х ((250 - 150)/399 + + 2/400 ) = 48 100. Результат не очень-то близок. Мы можем проверить, выполнял ли Oracle два набора вычислений — один Для диапазона 150 <= part_col < 200 и другой для 200 <= part_col <= 250
64 Глава 2. Табличное сканирование с последующим суммированием результатов. Он так не делал — иначе в резуль- тате мы получили бы 40 800. Ответ получается из статистики уровня таблицы. Существует 1 000 000 за- писей с 1000 различных значений и диапазоном от 0 до 999, что дает нам 1 000 000 х ((250 -150) / 999 + 2/1000) = 102 100. В случае с несколькими секциями, известными на момент синтаксического анализа, Oracle использует статистику уровня таблицы. /*+ Query 3 - несколько секций, использование переменных связывания*/ variable vl number variable v2 number execute :vl := 150; :v2 := 250 select count(*) from tl where part_col between :vl and ;v2 I Id | Operation I Name | Rows | Bytes | Cost | Pstart| Pstopl 0 | SELECT STATEMENT | | 1 | 4 | 1599 | | 1 | SORT AGGREGATE | | 1 | 4 | | | * 2 | FILTER III II I 3 | PARTITION RANGE ITERATOR! | | | | KEY | KEY * 4 | TABLE ACCESS FULL | Tl | 2500 | 10000 | 1599 | KEY | KEY 2 - filter(TO_NUMBER(:Z)<=TO_NUMBER(:Z)) 4 - filter("Tl”."PART_COL">=TO_NUMBER(:Z) AND "Tl"."PART_C0L”<=TO_NUMBER(:Z)) Оценка, равная 2500, неудачна, особенно если мы знаем правильный ответ: диапазон содержит 40 000 — так откуда она получилась? Начиная с неполного знания (особо обратите внимание на KEY-KEY для начала и окончания секции в плане), оптимизатор использовал статистику уровня таблицы (например, 1 000 000 записей). В этом случае, тем не менее, нет никакой информации о фактических значениях, так что оптимизатор вернулся к использованию жест- ко закодированных констант, а именно 0,25 % для between :bindl and : bind2; 2 500 записей - это 0,0025 x 1 000 000. ЧРЕЗМЕРНОЕ ИСПОЛЬЗОВАНИЕ ПЕРЕМЕННЫХ СВЯЗЫВАНИЯ Как известно, пользователям Oracle время от времени приходится сталкиваться с различными при- чудами. Одна из самых последних состоит в настоятельной рекомендации использовать связанные переменные, а не константы, из-за дополнительных расходов, которые иначе вы можете получить при синтаксическом анализе и применении блокировок (latching). Энтузиазм при использовании этой стратегии может зайти несколько далеко. В некоторых случаях оптимизатор может работать очень плохо, если не видит константных значений. Как правило, сложные отчеты, которые обраща- ются к большим таблицам за большим количеством данных, вероятно, должны использовать кон- стантные значения—дополнительные затраты на синтаксический анализ, скорее всего, будут неве- лики по сравнению с работой, проделываемой запросом. Даже в OLTP-системах было бы весьма разумно помочь оптимизатору с помощью разумного исполь- зования констант — возможно, вплоть до наличия полудюжины версий одного и того же запроса, которые отличаются только значением одного критичного входного параметра.
Секционирование 65 Осложняет ситуацию то, что 9i и 10g используют считывание переменных связывания (bind variable peeking) в большинстве случаев (но, конечно, не при выполнении explain plan или autotrace). Во время выполнения оптимизатор был бы способен просмотреть переменные связывания и использовать их зна- чения для подсчета количества значений, которое больше соответствовало бы Мому набору значений. К сожалению, при следующем выполнении запроса могут использоваться совершенно другие наборы значений, но оптимизатор не просмотрит их, а про- сто выполнит план, который получился при первой оптимизации. Это может привести к дорогостоящей ошибке, и в данном случае вы не мо- жете сильно на это повлиять — вы можете просто знать об этом и кодировать Соответственно, обходя эту проблему. ВНИМАНИЕ Недавно мне прислали файл трассировки 10053 от 10.2, который, похоже, показывает, что оптими- затор не просматривает переменные связывания в подготовленных операторах, полученных через ТОНКИЙ драйвер JDBC. Замечание MetaLink 273635.1 также содержит ссылку на эту проблему, но только для версии 81 этого драйвера. Вы, должно быть, отметили странный фильтрующий предикат (filter pre- dicate) в строке 2 последнего плана выполнения. Если вы попытаетесь выпол- йИТЬ этот тест в 8i и 9i, вы увидите важное отличие, которое вносит этот преди- кат. Установите значения переменных связывания следующим образом: :vl = 300 и :v2 •= 250 (другими словами, неправильным образом) для 8i, и если оба цредиката получают значения из одной секции, Oracle будет сканировать эту секцию, несмотря на тот факт, что, очевидно, никаких данных получено не бу- дет. Повторите этот тест в 9i, и дополнительный предикат автоматически будет иметь значение false, и Oracle не выполнит следующую строку плана выпол- нения. Вернемся к проблеме статистики: проблема статистики секций и уровня таб- лиц — сложная вещь. Наиболее общее использование секционированных таб- лиц вовлекает процесс, который корпорация Oracle теперь называет Partition Exchange Loading (или rolling partition maintenance); в нем вы заполняете пус- тую таблицу, индексируете ее и собираете по ней статистику, а затем выпол- няете обмен секций (partition exchange), используя SQL-код, подобный сле- дующему: alter table ptl exchange partition p0999 with table load_table including indexes without validation » Проблема состоит в том, что это не обновляет статистику уровня таблицы (фактически было много сообщений в прошлом об исчезновении статистики Уровня таблицы при выполнении обслуживания секций). После выполнения обмена секций вам необходим механизм, который бы обновил статистику уров- ня таблицы — желательно без чрезмерного использования машинных ресурсов.
66 Глава 2. Табличное сканирование Вам действительно необходимо знать свои данные, и чтобы сделать это эффек- тивно, необходимо использовать самую последнюю версию пакета dbms_stats. Проблемы статистики уровня секций и таблиц эхом отражаются на подсек- ции (subpartitions). Если вы хотите запросить точно одну подсекцию одной сек- ции, оптимизатор будет использовать статистику для этой одной подсекции. Если вы хотите запросить несколько подсекций одной секции, оптимизатор пе- реключится на статистику уровня секции для этой одной секции. Если ваш за- прос становится немного беспорядочным в своем выборе, оптимизатор будет использовать статистику уровня таблицы. Заключение Стоимость табличного сканирования в значительной степени определяется пред- полагаемыми многоблочными чтениями. Чтобы вычислить стоимость, оптими- затор делит количество используемых в таблице блоков (ниже уровня макси- мального заполнения) на число, представляющее собой предполагаемый размер многоблочного чтения. (Так было только когда я писал эти строки для заклю- чения. Я думал: насколько хорошо было бы, если бы этот параметр включал слово размер вместо количество и выражался бы в килобайтах.) В 8i это вычисленное количество необходимых многоблочных чтений пред- ставляло собой стоимость. В 9i введение системной статистики позволяет скор- ректировать результат с использованием коэффициентов, представляющих собой обычные размеры и относительные скорости многоблочных чтений, а также добавить стоимость процессорных ресурсов для обращения к блокам и получения данных из каждой записи в блоках. В 10g содержатся некоторые элементы, которые где-то в будущем оптимиза- тор также будет использовать для статистики кэширования, что, скорее всего, приведет к снижению значения стоимости. В случае полных быстрых индексных сканирований количество листовых блоков в индексе, а не количество блоков ниже уровня максимального заполне- ния, похоже, является ведущим значением, используемым в вычислениях. Во многих случаях это даст разумный результат. Однако существует несколько сценариев, в которых количество листовых блоков может быть намного меньше количества блоков ниже уровня максимального заполнения, и это приведет к серьезным просчетам в оценке стоимости полного быстрого индексного ска- нирования. Секционированные таблицы, похоже, представляют собой проблему. Опти- мизатор может использовать статистику одной секции, если секция может быть определена на этапе синтаксического анализа; иначе он использует статистику уровня таблицы. Та же самая стратегия используется с подсекциями одной сек- ции. Во многих случаях вы можете обнаружить, что единственный способ за- ставить оптимизатор создать разумный план состоит в обновлении статистики уровня таблицы при выполнении Partition Exchange Loading. Это может ока- заться дорогим с точки зрения ресурсов подходом.
Тестовые сценарии 67 тестовые сценарии файлы к этой главе, доступные для загрузки, перечислены в табл. 2.6. Таблица 2.6. Тестовые сценарии к главе 2 Сценарий Комментарии jlan_run81.sql plan_run92.sql -ablescan_01.sql Сценарий для вывода всех столбцов plan_table в 8.1 Сценарий для вывода всех столбцов plan_table в 9.2 Создание тестового набора данных для вычисления стоимости табличных сканирований :alc_mbrc.sql tablescan_Ola.sql Сценарий для генерации значений скорректированного dbf_mbrc Влияние размера блока на стоимость для таблицы с фиксированным размером блоков, равным 10,000 ablescan_01b.sql Влияние нескольких размеров блоков на стоимость для таблицы с фиксированным размером в 80МВ set_system_stats.sql ablescan_02.sql Пример кода для установки системной статистики Первоначальный тестовый сценарий, измененный для исследования обычной оценки стоимости процессорных ресурсов tablescan_03.sql Первоначальный тестовый сценарий, измененный для исследования оценки стоимости процессорных ресурсов без рабочей нагрузки tablescan_04.sql Эффекты оценки стоимости процессорных ресурсов без рабочей нагрузки с несколькими размерами блоков Cpu_costing.sql paralleLsql Простая демонстрация перемещения предикатов Повторное использование первого примера табличного сканирования для параллельного тестирования parallel_2.sql tadex_ffs.sql hack_stats.sql partition.sql Версия parallel.sql с включенной системной статистикой Простая демонстрация стоимостей полного сканирования по индексу Универсальный сценарий для модификации статистики объекта Демонстрация оценки стоимости секционированных табличных сканирований setenv.sql Установка стандартного тестового окружения для SQL*Plus
3 Селективность для одной таблицы После главы, посвященной табличным сканированиям, вы, возможно, ждали главу об индексных путях доступа. Но прогнозируемое количество записей (кардинальность), генерируемых операцией, играет критическую роль в выборе первоначального порядка соединений и оптимальных индексов, так что полез- но иметь хорошее представление о том, как оптимизатор оценивает количество записей, которые будут получены на каждом шаге плана. Причина, по которой заголовок этой главы включает термин «селектив- ность», а не «кардинальность», состоит в том, что вычисления, выполняемые оптимизатором для определения кардинальности, основываются на оценке пред- полагаемой доли записей в текущем множестве данных, которые пройдут неко- торый тест. Эта доля и является числом, которое мы называем селективностью. После того как вы получите селективность, кардинальность — это просто селек- тивность, умноженная на количество записей на входе. В зависимости от обстоятельств иногда одно из понятий использовать более удобно, или оно более интуитивно, чем другое, так что я буду достаточно сво- бодно переключаться между ними по ходу изложения. Я сделал несколько комментариев о гистограммах в этой главе, но в боль- шинстве случаев в ней описываются вычисления, которые оптимизатор исполь- зует, когда у него нет никаких гистограмм. Эффекты гистограмм рассматрива- ются в главе 7. Начало На недавней конференции я работал с аудиторией в 1200 человек. Как вы ду- маете, сколько из них родились в декабре? Если вы решили, что около 100, зна- чит, вы только что отлично сымитировали работу оптимизатора. (Между про- чим, я соврал относительно размеров аудитории для простоты вычислений.) о Существует 12 возможных месяцев в году — известный факт. о Даты рождения (возможно) равномерно распределены по году — предполо- жение.
дачало 69 Q Одна двенадцатая всей аудитории родилась в любой один месяц — селектив- ность месяца. О Запрос был для некоторого одного месяца — предикат. Й Запрошенный месяц на самом деле существует в календаре — проверка гра- ничных условий. ф Аудитория состоит из 1200 людей — базовая кардинальность. © Ответ: одна двенадцатая от 1200, что равняется 100, — вычисленная карди- нальность. Давайте превратим этот вопрос в оператор SQL и повторно выполним эти шаги с точки зрения оптимизатора. У нас будет таблица audience со столбцом month_no, в котором используются значения от 1 до 12 для месяцев года. Так <ак мы хорошие администраторы баз данных, мы сгенерировали статистику для данных. Наш запрос выглядит следующим образом: Select count(*) from audience Khere month_no = 12 Позволяя себе небольшую поэтическую вольность, оптимизатор выполнит следующие шаги для столбца month_no. Обратите внимание на то, что эти шаги очень близки к человеческому мышлению, хотя и добавляется одна проверка, о которой мы можем забыть из-за человеческой интуиции. Исследуем значения да Представления user_tab_col_statistics (или user_tab_columns) и про- верим представление user_tab_hi stograms, чтобы выяснить следующие под- робности. О user_tab_col_statistics. num_distinet равняется 12. Q user_tab_histograms показывает только нижнее (1) и верхнее (12) значе- ния, так что предполагаем, что значения распределены равномерно. Q user_tab_col_statisties.density равняется 1/12; один месяц дает одну двенадцатую данных. Q month_no равняется 12, единственный столбец, равенство, так что можно ис- пользовать user_tab_col_stati stics.densi ty. О 12 находится между значениями low_value и high_value из user_tab_ col_stati sti cs. Q Use r_t a b_c о l_s tatistics.nu m_n u 11 s равняется 0 (все когда-нибудь роди- лись — компьютер учел это, даже притом, что для нас это интуитивно понят- но). О user_tables.num_rows равняется 1200. О Ответ равняется одной двенадцатой 1200, то есть 100. Аудитория моделируется в сценарии birth_month_01.sql в онлайн-хранилище КОДа. Запрос декабрьских дней рождений создаст следующий план выполнения: План выполнения (9.2.0.6) — - »• - _ _ _ _ _ — — — — — — _ — _ _ _ _ ~ — _ _ _ _ 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=2 Card=l Bytes=3)
70 Глава 3. Селективность для одной таблицы 1 0 SORT (AGGREGATE) 2 1 TABLE ACCESS (FULL) OF 'AUDIENCE' (Cost=2 Card=100 Bytes=300) Строка 2 как раз нам и интересна — в ней показано Card=100: Oracle пра- вильно вывел, что будет 100 записей в базовой таблице, которые соответствуют нашему тесту. (Card=l в строке 0 отражает тот факт, что конечный результат, который мы получим при подсчете записей, действительно будет представлять собой одну запись.) Вы можете обратить внимание на небольшую причуду, если посмотрите на статистику, хранящуюся в словаре данных, select column_name, num_di sti net, density from user_tab_col_stati sties where table_name = 'AUDIENCE' COLUMN_NAME NUM_DISTINCT DENSITY MONTH_NO 12 .083333333 Похоже, что Oracle хранит одну и ту' же часть информации дважды — в столбце под названием num_distinct (количество различных значений, без неопределенных) и в столбце density (доля данных — без учета записей с не- определенными значениями — которую вернул бы запрос вида столбец = кон- станта). В нашем примере num_disti net равно 12 и densi ty равно 1/12; и в об- щем случае вы, возможно, можете отметить, что density = l/num_distinct. Тогда почему же Oracle хранит одну и ту же информацию дважды? Эти два значения в нашем примере связаны, но это не всегда так. Если вы создадите гистограмму по столбцу, то, скорее всего, обнаружите, что плотность больше не равна l/num_di sti net, и если существуют гистограммы, разные вер- сии Oracle будут вести себя несколько по-разному. Запуская пример в разных средах, вы обнаружите, что оптимизатор в 10g использует столбец num_ distinct для получения результата: cardinality = num_rows/num_distinct. Если бы существовала гистограмма, то оптимизатор использовал бы столбец density: cardinality = num_rows * density. Для подтверждения этой детали я использовал пакетную процедуру dbms_ stats.set_column_stats, чтобы изменить num_distinct и density между двумя выполнениями одного и того же запроса (см. сценарий hack_stats.sql в онлайн-хранилище кода). Эта проверка показала, что 8i всегда использует densi ty, но 9i (как и 10g) использует num_di sti net, если не существует гисто- граммы, и density, если существует гистограмма — хотя 9i не подхватит изме- ненные значения, пока не будет выполнен сброс разделяемого пула (небольшая несуразность, которая не относится к делу, если только вы не реализовали сце- нарии для загрузки свойственной бизнесу специфики напрямую в словаре дан- ных).
Неопределенные значения 71 УСОВЕРШЕНСТВОВАНИЯ И ПРОБЛЕМЫ При обновлении вас поджидает всегда множество небольших ловушек. Вот одна из них, которую нашел с помощью тестовых сценариев, написанных для примера с месяцем дня рождения. При использовании пакетной процедуры dbms_stats.gather_table_stats() параметр method_ppt полу- й|ет значение по умолчанию. В 81 и 91 это значение было for all columns size 1, что значит «Не соби- рать Данные для гистограмм»; в 10g это значение — for all columns size auto. fak как моим стандартом для тестирования кода до выпуска 10g было выполнение dbms_stats. gather_table_state( user, 'tl', cascade=>true), я обнаружил, что некоторые мои тестовые результаты стали «неправильными» после обновления. {^ведение по умолчанию в 10g фактически управляется таблицами. Код в пакете dbms_stats ис- пользует процедуру под названием get_param для поиска значений по умолчанию для некоторых параметров сбора данных. gbl можете изменить поведение по умолчанию, вызвав связанную процедуру dbms_stats.set_param. Я бы поостерегся делать это. Очень просто забыть об этом, что может привести к большой пута- нице при следующем обновлении или при следующей установке. Неопределенные значения Давайте усложним наш пример: представьте, что 10 % членов нашей аудитории це помнят, в каком месяце они родились (и не хотят включать свои наладонни- ки в середине интересной презентации). Как много людей поднимут свои руки за декабрь? Предположим, что амнезия, связанная с днем рождения, распределена слу- чайно; тогда будут существовать 120 людей, которые не смогут ответить на этот вопрос. Это будет представлено равномерным распределением неопределенных значений в столбце month_no в нашей таблице audience. Как это скажется на статистике и вычислениях? Это значения, которые мы увидим, если выполним запрос к словарю данных. О user_tables . num_rows равняется 1200 — без изменений. О user_tab_col_stati sti cs . low_value равняется 1 — без изменений. О user_tab_col_stati sti cs . hi gh_value равняется 12 — без изменений. О user_tab_hi stograms показывает отсутствие гистограммы — без изменений. О user_tab_col_statistics.num_distinct равняется 12 — без изменений. О user_tab_col_statisties.density равняется 1/12 — без изменений. О user_tab_col_stati sti cs. num_nulls равняется 120 — новый элемент данных. Самый важный момент, который надо отметить, это то, что присутствие не- определенных значений не изменяет значения num_distinct (или density); Неопределенные значения просто игнорируются при генерации статистики. Итак, что же мы получим в качестве результата для людей, родившихся в декабре? Человек в этом случае поступил бы как-то так: если 100 людей родились в декабре, но 10 % аудитории не помнят, когда они родились, то, при равномер- ном распределении амнезии, 90 из 100 помнили бы, что они родились в декабре. Вот эквивалентная «аргументация» оптимизатора. О Базовая селективность = 1/12 (из density или из l/num_distinct).
72 Глава 3. Селективность для одной таблицы О num_nulls = 120. о num_rows = 1200. О Скорректированная селективность = Базовая селективность х (num_rows - - num_nulls)/num_rows. о Скорректированная селективность = (1/12) х ((1200 - 120)/1200) = 0,075. о Скорректированная кардинальность == Скорректированная селективность х х num_rows. о Скорректированная кардинальность = 0,075 х 1200 = 90. Итак, мы ожидаем, что кардинальность для нашего запроса будет равна 90 — и с достаточной степенью уверенности можем сказать, что, когда мы запустим тест, кардинальность будет показана равной 90 (сценарий birth_month_02.sql в онлайн-хранилище кода). План выполнения (9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=2 Card=l Bytes=3) 1 0 SORT (AGGREGATE) 2 1 TABLE ACCESS (FULL) OF 'AUDIENCE' (Cost=2 Card=90 Bytes=270) Использование списков значений После того как мы узнали, как работать с простым случаем столбец = констан- та, мы можем перейти к более сложным случаям, например к запросам, содер- жащим списки или списки с неопределенными значениями, запросам, содержа- щим два столбца, содержащим диапазоны и переменные связывания. Существует множество случаев, которые надо исследовать, прежде чем пере- ходить к индексам и соединениям. Давайте начнем с простейшего случая — ис- пользования списков значений (in-lists). Придерживаясь таблицы, которая пред- ставляет нашу аудиторию из 1200 человек, мы можем написать запрос, подоб- ный следующему: select count(*) from audience where month_no in (6,7,8) Учитывая, что мы выбрали три месяца и предполагаем, что в каждом месяце родились 100 человек, мы не сильно бы удивились, если бы вычисленная кар- динальность равнялась 300. Но я собираюсь начать мое исследование с 8i, и, к сожалению, следующий план выполнения — это то, что мы получим, если построим тест с использованием этой версии Oracle (см. in_list.sql в онлайн- хранилище кода): План выполнения (8.1.7.4) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=l Card=l Bytes=3) 1 0 SORT (AGGREGATE) 2 1 TABLE ACCESS (FULL) OF 'AUDIENCE' (Cost=l Card=276 Bytes=828)
Использование списков значений 73 Откуда появилось значение 276 в полном сканировании? (При использова- нии 9i или 10g кардинальность оказывается равной 300, как и предполагалось.) Здесь надо рассмотреть два момента. Во-первых, вычисление очевидно непра- вильно. Во-вторых, что более важно, вычисление изменяется при обновлении версии Oracle — и, как я отметил в начале этой главы, правильная кардиналь- ность критична для получения правильного порядка соединений и оптимально- to выбора индексов. Вы можете обнаружить, что некоторые ваши планы выпол- нения изменились «без видимой причины» после обновления. ОШИБКИ ПРИ ИСПОЛЬЗОВАНИИ СПИСКА ЗНАЧЕНИЙ внутренне оптимизатор преобразует предикат, подобный month_no In (6,7,8), к month_no = 6 or fflontti_no = 7 or month_no = 8. Если вы добавите к запросу подсказку use_concat, оптимизатор пре- образует план в объединение этих трех частей — вот здесь 81 неожиданно выдает правильную кар- динальность. Ошибка, от которой страдает 81, состоит в том, что после разделения списка на три отдельных пре- диката применяется стандартный алгоритм для нескольких дизъюнкторов (disjuncts, технический Термин для предикатов, связанных по OR). Этот алгоритм, в общем, исправляет сдвоенные записи ГЭИ, где предикаты пересекаются, — но, конечно, предикаты, сгенерированные на основании спи- ска значений, гарантированно не пересекаются. 81 преобразует этот SQL-запрос для получения спе- циального случая дизъюнкторов, а затем не может обработать их правильно. Когда появляется причуда, подобная этой, вы должны подумать о двух ве- щах. Во-первых, является ли этот пример специальным случаем? (Наш столбец имеет только 12 различных значений; если бы различных значений было боль- ше, вызвало бы это проблему?) Во-вторых, существуют ли смежные области, которые могут вызвать другие причуды? Давайте потратим некоторое время на эксперименты, связанные с этими двумя вопросами, чтобы понять, что происходит. Код для создания базовой таб- лицы был таким: create table audience as select trunc(dbms_random.value(1,13)) month_no from all_objects Where rownum <= 1200 Давайте изменим его, чтобы сгенерировать 1000 возможных «месяцев» сре- ди 12 000 людей (так, чтобы оптимизатор вычислил 12 записей на «значение месяца»). Create table audience as select trunc(dbms_random.value(l,1001)) month_no from all_objects where rownum <= 12000
74 Глава 3. Селективность для одной таблицы После этого мы можем создать простой сценарий, который выполнял бы за- просы к таблицам, с увеличивающимися списками значений (в онлайн-храни- лище кода содержатся отдельные сценарии для двух наборов данных: in_list.sql использует первоначальные 1200 записей, in_list_02.sql — 12 000 записей с 1000 различных значений): select count(*) from audience where month_no in (1,2); select count(*) from audience where month_no in (1,2,3); select count(*) from audience where month_no in (1,2,3,4); select count(*) from audience where month_no in ( 1,2,3,4,5,6,7,8,9,10,11,12,13,14 ); select count(*) from audience where month_no in ( 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,15,16,17,18,19,20, 21,22,23,24,25,26,27,28,29,30 ); После создания таблицы результатов, как показано в табл. 3.1, вы можете увидеть, что чрезмерное расхождение, которое появляется в базовом случае, ко- гда мы имеем только 12 значений, в примере с 1000 различных значений выде- ляется намного менее заметно. Фактически мы вообще не увидим расхождения в большом примере, пока в списке значений не появится 14 элементов. Более того, если вы проверите значения для 9i и 10g, то увидите, что кардинальность всегда получается равной N х количество элементов в списке, пока количество элементов не превысит количество уникальных значений. Таблица 3.1. Маленькие списки значений могут привести к большим ошибкам в кардинальности Размер списка Кардинальность —12 значений 8i (9i, 10д) Кардинальность —1000 значений 8i (9i, 10д) 1 100 (100) 12 (12) 2 192 (200) 24 (24) 3 276 (300) 36 (36) 4 353 (400) 48 (48) 5 424 (500) 60 (60) 6 489 (600) 72 (72) 7 548 (700) 84 (84) 8 602 (800) 96 (96) 9 652 (900) 108 (108) 10 698 (1000) 120 (120) 11 740 (1100) 132 (132) 12 778 (1200) 144 (144) 13 813 (1200) 156 (156) 14 846 (1200) 167 (168) 30 1112 (1200) 355 (360) Итак, существует проблема со списками значений, грозящая проблемами для систем, в которых количество уникальных значений в столбце мало. Когда
(/^пользование списков значений 75 ВЫ выполните обновление таких систем, вы сможете увидеть, что планы выпол- нения изменились. Но в общем случае ошибки в вычислениях могут быть Незначительны. Давайте перейдем к проблемам других причуд, которые мы мо- «$ем получить после этой. Будет ли оптимизатор делать что-нибудь неожидан- ное со следующими предикатами в случае таблицы, в которой столбец month_ по принимает только значения от 1 до 12 (см. сценарий oddities .sql в он- дайн-хранилище кода)? Q where month_no = 25 — за пределами high_value. О where month_no in (4, 4) — повторяющиеся значения. О where month_no in (3, 25) — смешанное множество значений, попадающих и не попадающих в диапазон. О where month_no in (3, 25, 26) — то же самое. 13 where month_no in (3, 25, 25, 26) — то же самое с повторениями. 0 Where month_no in (3, 25, null) — обнаружит ли оптимизатор неопреде- ленное значение? О where month_no 1 n (: bl, :Ь2, : ЬЗ) — co считыванием переменных связыва- ния или без считывания? Результаты выполнения этих тестов в 8i, 9i и 10g показаны в табл. 3.2. Таблица 3.2. Граничные случаи со списками значений Предикат Кардинальность (8i) Кардинальность (9i/10g) month_no = 25 100 100 Ой month_no in (4,4) 100 100 Хорошо month_no in (3, 25) 192 200 Ой, но последовательно month_no in (3, 25, 26) 276 300 Ой, но последовательно month_no in (3,25,25, 26) 276 300 Ой, но последовательно month_no in (3, 25, null) 276 300 Ой, ой month_no in (:bl, :b2, :b3) 276 300 Ой, ой, но последовательно Итак, кроме странного «двойного подсчета» в 8i, все три версии со списками значений ведут себя одинаково (иногда странно). Они не учитывают тот факт, что значения находятся вне диапазона верхних/нижних значений и что список Может содержать неопределенные значения. Они учитывают явные дублирую- щиеся значения в списке, но не учитывают их, если те скрыты внутри перемен- ных связывания. И в качестве заключительного наблюдения, касающегося списков значений, вы можете поэкспериментировать с not iп, используя запросы, подобные этому: select count(*) from audience where month_no NOT in (1,2) Учитывая небольшие ошибки округления, вы обнаружите, что оптимизатор Версии 8i последователен. Кардинальность для month_no in {некоторый спи-
76 Глава 3. Селективность для одной таблицы сок} плюс кардинальность для month_no not in {некоторый список} равняется 1200 — общее количество записей в таблице. Однако 9i и 10g непоследовательны: хотя кардинальность month_no 1 п {не- который список} изменяется при переходе с 8i на 9i, механизм вычисления month_no not in {некоторый список} не изменяется. Дальнейшие эксперименты оставлены в качестве упражнения для вас; сце- нарии in_list_03.sql и pv.sql доступны в онлайн-хранилище данных кода в каче- стве отправной точки для исследований. Последний сценарий содержит замечания относительно изменений, которые проявляются в разных версиях, когда вы начинаете использовать списки значе- ний для создания секционированных представлений. СЕКЦИОНИРОВАННЫЕ ПРЕДСТАВЛЕНИЯ — УСТАРЕВШИЕ, НО УЛУЧШЕННЫЕ Секционированные представления можно считать устаревшей возможностью, но это один из тех странных случаев, когда термин устарел, а технология — нет. Код, который используется для обра- ботки секционированных представлений, более не является специальным, это просто код для обра- ботки представлений — и со своей задачей он справляется лучше в 10д, чем в Oracle 7, даже если параметр partition view enabled установлен в false. Обновления в10д Написать окончательную книгу о стоимостном оптимизаторе — безнадежная задача, он изменяется быстрее, чем вы можете писать (по крайней мере, быст- рее, чем я могу писать). Начиная этот том, я прошел по 10g beta, 10.1.0.2, 10.1.0.3; по мере того, как я пишу сегодня, я тестирую все примеры на 10.1.0.4 (и 10.2 выйдет до того, как книга появится в продаже), и списки значений ме- няются. Фактически вся селективность вне нижних/верхних значений изменилась. Вернемся к набору данных (см. сценарий in_list_10g.sql в онлайн-хранилище кода для специального варианта) и попробуем выполнить следующие запросы: О where month_.no = 13 — вне нижнего/верхнего значения. О where month_.no = 15 — вне нижнего/верхнего значения. О where month_.no i n (13,15) — два значения вне нижнего/верхнего значения. о where month_.no in (16,18) — два различных значения вне нижнего/верхне- го значения. Результаты, которые вы получите в 10.1.0.4 и в 10.1.0.2, будут отличаться, как показано в табл. 3.3. Таблица 3.3. Поведение за пределами диапазона отличается в разных версиях 10g Предикат Кардинальность (10.1.0.2) Кардинальность (10.1.0.4) month_no = 13 100 91 month_no =15 100 73 month_no in (13,15) 200 164 month_no in (16,18) 200 109
Диапазонные предикаты 77 Вычисления, которые использует Oracle, могут быть представлены графиче- ски, как показано на рис. 3.1: чем дальше вы уходите от известного диапазона цижних/верхних значений, тем меньше вероятность, что вы найдете данные, bracle использует линейно убывающую функцию для прогнозирования изме- нений, значение которой уменьшается до нуля, когда вы выходите за диапазон, равный разнице между нижним и верхним значениями. У нас есть диапазон, равный И — от 1 до 12. Правый отрезок пересекается i> нулем при month_no = 23 (верхнее значение + И); левый отрезок пересекает Нуль при month_no = -10 (нижнее значение -11). Это плохие новости для некоторых людей. Если у вас есть последователь- ные или временные данные в часто используемом в запросах столбце и стати- стика неактуальна, то запросы, в которых есть сравнение на равенство с этим столбцом, будут использовать линейную функцию, чтобы выдать правильный ответ по прошествии времени. Теперь те же самые запросы будут выдавать вам Кардинальность, которая будет снижаться по прошествии времени, до тех пор цока неожиданно кардинальность не станет такой низкой, что планы могут су- щественно измениться. Рис. 3.1. Расширение селективности в 10g Диапазонные предикаты Вы можете использовать множество вариаций для тестирования диапазонов: диапазоны могут быть ограниченными или неограниченными, открытыми или закрытыми. Используя те же самые базовые тестовые данные со 100 записями для каждого из 12 месяцев, я начну с таблицы примеров (см. сценарий ranges.sql » онлайн-хранилище кода) и результатов explain plan (табл. 3.4) после чего Мы обсудим используемую арифметику. Таблица 3.4. Изменения в кардинальности для диапазонных предикатов Случай Предикат Кардинальность Кардинальность __________________________(8!)___________(9i/10g)_______________________ 1 month_no > 8 437 436 Неограниченный, открытый 2 month_no >= 8 537 536 Неограниченный, ______________________________________________________закрытый продолжение
78 Глава 3. Селективность для одной таблицы Таблица 3.4 (продолжение) Случай Предикат Кардинальность (8i) Кардинальность (9i/10g) 3 month_no < 8 764 764 Неограниченный, открытый 4 month_no <= 8 864 864 Неограниченный, закрытый 5 month_.no between 6 and 9 528 527 Ограниченный, закрытый, закрытый 6 monthjro >= 6 and month_no <=9 528 527 Ограниченный, закрытый, закрытый 7 month_no >= 6 and month_.no < 9 428 427 Ограниченный, закрытый, открытый 8 monthjro > 6 and month_no <= 9 428 427 Ограниченный, открытый, закрытый 9 monthjro > 6 and monthjro < 9 328 327 Ограниченный, открытый, открытый 10 monthjro > :bl 60 60 Неограниченный, открытый 11 month_no >= :bl 60 60 Неограниченный, закрытый 12 monthjro < :bl 60 60 Неограниченный, открытый 13 monthjro <= :bl 60 60 Неограниченный, закрытый 14 monthjro between :bl and :b2 4 3 Ограниченный, закрытый, закрытый 15 monthjro >= :bl and monthjro <= :b2 4 3 Ограниченный, закрытый, закрытый 16 month_no >= :bl and monthjro < :b2 4 3 Ограниченный, закрытый, открытый 17 monthjro > :bl and monthjro < :b2 4 3 Ограниченный, открытый, открытый 18 monthjro > :bl and monthjro <= :b2 4 3 Ограниченный, открытый, закрытый 19 month_no > 12 100 100 Неограниченный, открытый 20 monthjro between 25 and 30 100 100 Ограниченный, закрытый, закрытый Как вы видите, результаты 8i, 9i и 10g очень похожи. Единственные разли- чия возможны из-за ошибок округления, которые иногда появляются в резуль- тате вычислительных проблем или изменения способа округления в коде опти- мизатора. Случаи переменных связывания (14-18), в которых 8i показывает карди- нальность, равную 4, а не 3, являются следствием ошибок в вычислениях. Ответ должен быть равен точно 3, но при преобразовании из десятичной формы в дво- ичную и обратно где-то после 15-го разряда после запятой появилась случайная цифра. 8i выполнил округление в большую сторону; 9i и 10g выполнили округ- ление до ближайшего целого.
Диапазонные предикаты 79 Другие отличия появляются, когда в результате вычислений получаются дробные значения вроде 0,36 или 0,27: и снова 8i выполнит округление в боль- шую сторону, тогда как 9i и 10g просто выполнят округление до ближайшего делого. КАРДИНАЛЬНОСТЬ Любые ошибки (или изменения) в кардинальности могут иметь значительные побочные эффекты рри выборе порядка соединений и выборе индексов. Вопреки интуиции, изменение в кардинально- с 4 на 3 намного более вероятно существенно повлияет на план выполнения, чем изменение с; 537 на 536. Точность для меньших таблиц (более конкретно — для меньших результирующих мно- жеств) очень важна. •V"—-------------------------------------------------------------------- Посмотрев на таблицу результатов, можно выделить несколько образцов по- ведения. О При использовании литералов (констант) отличие между открытым (больше чем [>], меньше чем [<]) и закрытым (больше чем, либо равно [>=], меньше чем, либо равно [<=]) интервалами равно точно 100. О Если мы выйдем за пределы доступного диапазона значений для столбцов (user_tab_col_statisties.low_value, user_tab_col_statisties.high_ value), кардинальность, похоже, зафиксируется на 100. О Значения, показанные для переменных связывания, похоже, жестко фикси- рованы (нет никакого отличия между открытыми и закрытыми интервала- ми) и имеют мало общего с реальными данными (диапазон, возвращающий 60 записей, кажется маловероятным, если отдельное значение возвращает 100 записей). Проделав небольшую работу (и несколько последующих экспериментов), Мы можем предположить, какие вычисления выполняет оптимизатор. Сначала цозьмем вторую часть таблицы результатов. О Случаи с 10 по 13 (переменные связывания с неограниченными диапазона- ми): оптимизатор просто установит селективность на уровне 5 %. Для 1200 записей (без неопределенных значений), мы получим 0,05 х 1200 = 60 записей. О Случаи с 14 по 18 (переменные связывания с ограниченными диапазонами): оптимизатор просто установит селективность на уровне 0,25 % (что на са- мом деле составляет 5 % от 5 %). Для 1200 записей (без неопределенных зна- чений) мы получим 0,0025 х 1200 = 3 записи. Q Случаи с 19 по 20 (диапазоны вне записанных нижних/верхних границ): оп- тимизатор определяет, что запрос выходит за границы известного диапазона, и, похоже, возвращает селективность, то есть кардинальность, которая будет Правильной в случае столбец = константа. (Существует, тем не менее, ужас- ный граничный случай, который появляется в 9i и далее, когда в каждой за- писи содержится одно и то же значение. Сценарий selectivity_one.sql в он- лайн-хранилище кода иллюстрирует этот случай.)
80 Глава 3. Селективность для одной таблицы ПЕРЕМЕННЫЕ СВЯЗЫВАНИЯ И ДИАПАЗОНЫ Существует одна странность с переменными связывания и диапазонами: вы можете ожидать, что character_col like:bind будет рассматриваться так же, как и between, ведь coIX like 'А%' выглядит так, будто это выражение должно обрабатываться как coIX >= 'A' and coIX < 'В' — что почти верно. Фак- тически (см. сценарий like_test.sql в онлайн-хранилище кода), когда оптимизатор видит такое сравне- ние с переменной связывания, он использует ту же самую 5-процентную селективность, как и в слу- чае неограниченных диапазонов, с вытекающими отсюда обычными проблемами, вызванными эф- фектами считывания переменных связывания. Предикаты, использующие литералы, требуют несколько более подробного объяснения. Неформальная, приблизительная версия алгоритма оптимизатора выглядит следующим образом: Селективность = необходимый диапазон / весь доступный диапазон Посмотрев на (user_tab_col_statistics.high_value - user_tab_col_ statistics.low_value), мы можем вычислить, что весь диапазон в нашем примере равен 11. И так как вы видите число И, вы знаете, что что-то не так с нашим тестовым сценарием. Мы знаем, что в тестовом сценарии около 12 дис- кретных измерений, но оптимизатор использует вычисления, в которых данные обрабатываются так, как если бы мы имели непрерывно изменяющиеся значе- ния с суммарным диапазоном И. В действиях оптимизатора над небольшим количеством уникальных значе- ний, похоже, содержится встроенная критическая ошибка. (Это похоже на не- доумение маленького ребенка по поводу количества штакетин забора и проме- жутков между ними: почему там 11 штакетин, но 10 промежутков? Или, допустим, он немного вырос: почему И чисел между 30 и 40, хотя 40 - 30 = 10?) Но как применять этот алгоритм в каждом из немного отличающихся случаев? о Случай 1: month no > 8. Это неограниченный (нет предела на одном из кон- цов), открытый (значение 8 исключено) диапазон. • Селективность = (high_value - limit) / (high_value - low_value) = = (12-8)/(12- 1) = 4/11. • Кардинальность = 1200 x 4/11 = 436,363636..., следовательно, получаемое значение — 437 или 436 — зависит от того, выполняет ли ваша версия Oracle округление в большую сторону или округление до ближайшего це- лого числа. о Случай 2: month_no >= 8. Это неограниченный, закрытый (включает значе- ние 8) диапазон, так что надо провести корректировку для закрытого интер- вала. Корректировка выполняется с помощью включения записей для за- крывающего значения — другими словами, добавления l/num_di stinet. (8i, похоже, использует densi ty, а не l/num_di stinet, но вы не обратите на это внимания, пока не начнете взламывать статистику или если будет существо- вать гистограмма.) • Селективность = (high_value - limit) / (high_value - low_value) + + l/num_di stinct = 4/11 + 1/12.
диапазонные предикаты 81 • Кардинальность = 1200 * (4/11 + 1/12) = 536,363636..., следовательно, вы получите значение 537 или 536 в зависимости от того, выполняет ли ваша версия Oracle округление в большую сторону или округление до ближай- шего целого. Случаи 3 и 4. Аналогичны случаям 1 и 2: • Селективность (3) = (limi t - low_value) /(high_value - low_value) = = (8-1)/(12-1) = 7/11. • Селективность (4) = (8 - 1) / (12 - 1) + 1/12. © Случаи 5 и 6: month_no between 6 and 9. В обоих случаях диапазоны огра- ниченные (ограничены на обоих концах) и закрытые. Выражение between в пятом случае — это просто удобная краткая запись для двух отдельных предикатов в случае 6. В результате мы получаем два закрывающих значе- ния и, соответственно, две корректировки. • Селективность = (9 - 6) / (12 - 1) + 1/12 + 1/12 ( >= , <= ). О Случаи 7, 8, 9. Как и для случая 5, но с одной корректировкой, с одной кор- ректировкой и без корректировки для закрывающего значения. • Селективность (7) = (9 - 6) / (12 - 1) + 1/12 (>=,<). • Селективность (8) = (9 - 6) / (12 - 1) + 1/12 ( > , <= ). • Селективность (9) = (9 - 6) / (12 - 1) ( > , <). Хотя я написал точные формулы для каждого случая, когда я работаю над Проблемой, я редко делаю больше, чем быстрое приблизительное вычисление значения запрошенный диапазон / полный диапазон. Однако вы увидите, что ₽сли в столбце существует только несколько различных значений, эта оценка может быть далека от действительности — это недостаток, который может иметь серьезные последствия. Поняв значимость полного диапазона, вы также поймете, что можно полу- нить некоторые неожиданные результаты, если у вас есть приложения, которые Используют бессмысленные значения, чтобы избежать использования неопре- деленных значений. Представьте приложение, которое содержит данные за пять лет (скажем, с 1 ян- варя 2000 по 31 декабря 2004). Какое значение получится в результате вычис- ления селективности оптимизатором для предиката: Where data_date between 101-Jan-2003' and '31-dec-2003' Если проигнорировать корректировки для високосных лет и граничные под- робности, ответ грубо будет равен 1/5. Если вы разрешите корректные неопре- деленные значения в столбце data_date, результат не изменится. Но если по- ставщик приложения решит, что неопределенные значения не должны быть разрешены, и вместо них решит использовать 31-Dec-9999, что произойдет? Как только единственное псевдонеопределенное значение попадет в таблицу, оптимизатор вычислит селективность как 1/8000. (Один год вне диапазона 8000.) Как вы считаете, что произойдет с планом выполнения, когда оценка оп- тимизатора будет отличаться в 1600 раз? Мы вернемся к этой и подобным про- блемам в главе 6.
82 Глава 3. Селективность для одной таблицы СЧИТЫВАНИЕ ЗНАЧЕНИЙ ПЕРЕМЕННЫХ СВЯЗЫВАНИЯ Переменные связывания хороши для OLTP-систем, так как они максимизируют совместное исполь- зование SQL и минимизируют использование процессора и конкуренцию при блокировках (latch) во время оптимизации (см. «Expert Oracle Database Architecture» Тома Кайта, издательство «Apress», сентябрь 2005). Но мы только что видели, что переменные превращают в полную ерунду вычисле- ния кардинальности. Так что в 91 было представлено считывание значений переменных связывания (bind variable peeking) для решения этой проблемы. При первой оптимизации части SQL-запроса оптимизатор обычно проверяет фактические значения всех входных переменных связывания и использует их значения для выполнения вычислений, что означает, что у оптимизатора есть шанс выбрать наилучший план для первого выполнения. Но при каждом последующем вызове синтаксического анализатора для этого оператора, если опре- делено, что текст оператора совместно используемый, обычно используется тот же самый план вы- полнения, независимо от любых изменений значений переменных связывания. (Существует исклю- чение, связанное с большими изменениями в длине символьных переменных связывания.) В OLTP-системах это, похоже, хорошо, поскольку деятельность OLTP имеет тенденцию повторять практически одни и те же операторы, которые выполняют один и тот же объем работы каждый раз, тысячи раз в день. Но даже в OLTP-системах существуют случаи, когда такой подход может вызвать проблемы. В DSS-системах обычно требуется избегать использования переменных связывания, чтобы исклю- чить совместное использование планов выполнения, которые выглядят похоже, но выполняют очень сильно отличающийся объем работы. В смешанных системах вы должны быть уверены, что у вас есть метод, позволяющий избежать ловушки с переменными связывания для всех сложных SQL-команд, которые может понадобиться выполнить. Кроме того, если вы пропустите оператор с переменными связывания через explain plan, оптимиза- тору не будут известны какие-либо значения (или даже типы связывания, bind types), так что в луч- шем случае он будет использовать фиксированные селективности для создания плана выполнения. В худшем случае он выполнит неправильное неявное преобразование, потому что обрабатывает значения как символьные, тогда как во время выполнения они могут быть числовыми, и выдаст вам полностью неправильный план выполнения. Изменения в 10g Как и в случае сравнения на равенство, в 10.1.0.4 неожиданно изменяется пове- дение, когда вы выходите за пределы верхних/нижних значений. Арифметика, или картина, используемая для диапазонов вне пределов, точно такая же, как и новый механизм, который мы видели для сравнения на равенство. Таким об- разом, вы получите некоторый вид эффектов, показанных в табл. 3.5 (см. сцена- рий ranges_10g.sql в онлайн-хранилище кода). Таблица 3.5. Поведение вне границ допустимого диапазона изменяется в разных версиях 10g Предикат Кардинальность (10.1.0.2) Кардинальность (10.1.0.4) month_no between 6 and 9 527 527 month_no between 14 and 17 100 82 month_no between 18 and 21 100 45 month_no between 24 and 27 100 1 Как вы видите, результаты, которые получились внутри предельных значе- ний, не изменились, но как только вы выйдете за границы возможных значе- ний, кардинальность сужается, хотя количество уникальных значений диктует использование постоянного значения.
Ш)3 предиката 83 два предиката Ореаде чем продолжать чтение, вернитесь к табл 3 4 Предикат month_no > 8 д^зт кардинальность, равную 437, а предикат month__no <= 8 дает кардиналь- ность, равную 864 Так как каждая запись в таблице должна соответствовать |Шно одному из этих двух предикатов (при условии, что у нас нет неопреде- ленных значений), как вы думаете, какую кардинальность вернет оптимизатор цдя следующего выражения'? Ihere month_no > 8 month_no <= 8 Существует три возможных предположения. Q Он вернет 1301, потому что это значение, получаемое в результате сложения кардинальностей двух отдельных предикатов, которые, очевидно, не пересе- каются. О Он вернет 1200, потому что объединенный предикат точно соответствует всем записям таблицы и в таблице 1200 записей. G Он вернет 986, потому что это результат последнего теста, который я проде- лал в сценарии ranges.sql, и на самом деле происходит имено так. Третий аргумент всегда очевиден, даже когда результат очевидно иррацио- нален. И чтобы еще немного вас запутать, в табл. 3.6. показаны кардинальности, Которые вы получите для этого запроса при изменении значения константы (СМ, сценарий ranges_02.sql в онлайн-хранилище кода). Таблица 3.6. Кардинальность изменяется, хотя мы знаем, что этого быть не должно Константа Кардинальность '1 2 3 4 5 ё 7 8 $ 10 11 12 1108 1110 1040 989 959 948 957 986 1035 1103 1192 1200 Не чудесна ли эта дразнящая симметричная подсказка в шестой строке; не вызывает ли эстетическое раздражение то, что она находится не совсем в сере- дине таблицы? Мы выясним, откуда взялись эти значения, через несколько Страниц; а пока я просто хочу напомнить, что оптимизатор не интеллектуа- лен — это лишь компонент программного обеспечения.
84 Глава 3. Селективность для одной таблицы Человеческому разуму очевидно, что month_no > 8 or monthno <= 8 должен вернуть все (кроме неопределенных) записи в таблице, но код оптимизатора не содержит такое распознавание; все, что он может видеть, — это два предиката с OR между ними. Тот факт, что оптимизатор может распознать немного отличный предикат month_no >= 6 and month_no <= 9 как один диапазон по одному и тому же столбцу, — это результат специального кода, из-за которого этот предикат не интерпретируется как два предиката с AND между ними. Это специальный слу- чай, и именно поэтому оптимизатор может вычислить (почти) правильный ре- зультат. Чтобы вычислить общие комбинации предикатов, потребуются три основ- ные формулы; и в этих формулах необходимо выражаться в терминах селектив- ностей, а не кардинальностей. О Селективность (predicatel AND predicate2) = селективность (predica- te!) х селективность (predicate2). О Селективность (predicatel OR predicate2) = селективность (predicatel) + селективность (predicate2) - селективность (predicatel AND predicate2)..., иначе пересечение будет посчитано дважды. О Селективность (NOT predicatel) = 1 - селективность (predicate!)..., кроме проблем с переменными связывания. Давайте вернемся к нашей аудитории в 1200 человек, чтобы на конкретном примере посмотреть, что значат эти выражения. Представьте, что аудитория со- бралась со всего Евросоюза (на 1 сентября 2003), чтобы услышать меня. Это означает, что слушатели приехали из 15 разных стран — мы будем предпола- гать, что их национальности случайно распределены (см. сценарий two__predicate_ Ol.sql в онлайн-хранилище кода). Вместо того чтобы просить, чтобы все, кто родился в декабре, подняли руки, как я делал в первом примере, я попрошу поднять руки только итальянцев, ро- дившихся в декабре. Сколько рук я увижу? Я предполагаю, что 100 родились в декабре (1 из 12); но эта группа из 100 человек приехала из 15 стран, так что только один 1 из 15, вероятно, приехал из Италии. Так как 100/15 = 6,5, я пред- полагаю, что 6 или 7 человек поднимут свои руки. Используем термины SQL. У меня есть предикат 1: month_no = 12 (селек- тивность = 1/12) и предикат 2; eu_country = ' Italy ' (селективность = 1/15). Из формулы получается, что объединенная селективность равна (1/12) х х (1/15). Формула просто алгебраически отображает арифметику, которую мы только что вывели интуитивно для нашей аудитории. Что можно сказать о случае с OR? Давайте попросим людей, родившихся в декабре или родившихся в Италии, поднять руки. Из них 1 из 12 родились в декабре (100) и 1 из 15 родились в Италии (80), так что в первом приближе- нии 180 рук поднимутся вверх. Но если на этом мы остановимся, мы посчитаем некоторых людей дважды — людей, которые родились в декабре в Италии, — так что мы должны отнять их от нашей суммы. Это легко сделать, так как мы только что определили, что часть нашей аудитории, родившаяся в декабре в Италии, равна (1/12) х (1/15).
дроблены с несколькими предикатами 85 Так что часть аудитории, которая нам необходима, — (1/12) + (1/15) - — (1/12) х (1/15), точно в соответствии с формулой. И, наконец, что можно сказать о людях, которые не родились в декабре? Так щк 1/12 аудитории родились в декабре, очевидно, что 11/12 аудитории роди- лись не в декабре. И снова наша формула лишь отражает нормальную умствен- арифметику: доля не родившихся в декабре = 1 - доля родившихся в де- кабре. Те из вас, кто знаком с теорией вероятности, должны были распознать эти формулы как стандартные формулы для вычисления объединенных веро- ятностей независимых событий. в Вероятность (Л AND В произойдут) = Вероятность (А произойдет) х Веро- ятность (В произойдет). © Вероятность (A OR В произойдут) = Вероятность (А произойдет) + Вероят- ность (В произойдет) - Вероятность (A AND В произойдут). Q Вероятность (NOT (А произойдет)) = 1 - Вероятность (А произойдет). Эта эквивалентность не очень удивительна. Вероятность появления собы- ОТЯ (неточно) — это доля раз, которую оно появилось в предыдущих тестах; се- лективность предиката (неточно) — доля записей в таблице, которые соответст- вуют этому предикату. --------------------- ----------—----------——.......................— ...... СТРАННЫЕ СЕЛЕКТИВНОСТИ ДЛЯ ПЕРЕМЕННЫХ СВЯЗЫВАНИЯ можете задаться вопросом, почему селективность month_no between :Ы and :Ь2 постоянно рав- В9 0,0025. Это происходит потому, что оптимизатор рассматривает выражение как два предиката, еоединенные по AND. Селективность, вычисленная для month_no > :Ы, имеет фиксированное значение 0,05, селектив- ность для month_no < :Ь2 та же самая; так что селективность истинности обоих предикатов равна: 0,05 х 0,05 = 0,0025 (см. сценарий bind_between.sql в онлайн-хранилище кода). Селективность not (column > :Ы) также представляет собой специальный случай: она равна 0,05, а не 0,95. Оптимизатор считает, что любая проверка неограниченного диапазона по столбцу вернет 5 % данных. Эта проблема становится еще более странной, если вы используете not(month_no between :Ы and ф2). Значение, которое использует оптимизатор, — 9,75%, потому что этот предикат эквивалентен Предикату (monthjro < :Ы or monthjro > :Ь2), который должен вернуть 5 % плюс 5 %, но оптимиза- тор вычитает 0,25 %, эмулируя стратегию «вычитание пересечения» для более общего случая OR. Проблемы с несколькими предикатами Я хочу закончить эту главу, выполнив две вещи с общими формулами для со- единения предикатов. Сначала я хочу пройтись по арифметике, которая позво- ляет оптимизатору получить кардинальность, равную 986, для выражения, ко- торое очевидно точно покрывает 100 % записей в нашей таблице из 1200 записей. Затем я хочу показать вам, почему формула, которую использует опти- мизатор, гарантирует получение неправильных значений в других, более реали- стичных случаях. Давайте «препарируем» выражение where:
86 Глава 3. Селективность для одной таблицы where month_no > 8 -- (predicate 1) or month_no <= 8 -- (predicate 2) о Из нашей формулы для единственной селективности (запрошенный диа- пазон / полный диапазон), мы вычислим селективность для predicate!., month no > 8, как (12 - 8) / (12 - 1) = 4/11 = 0,363636. О Аналогично селективность predicate2, month_no <= 8, равна (8 - 1)/(12 - 1) + + 1/12 = 7/11 + 1/12 = 0,719696. О Наша формула для селективность (Pl OR Р2) = селективность^ 1) + селек- тивность^!) - селективность (Pl AND Р2), так что объединенная селектив- ность равна 0,363636 + 0,719696 - (0,363636 х 0,719696) = 0,8216. о Умножим селективность на 1200 записей в таблице, округлим в большую сторону и получим 986 — именно то, что мы видели в табл. 3.6. Ясно, что это неправильный ответ по причинам, которые интуитивно понятны человече- скому разуму. Но машина — не человек, она следует коду. Фактически эта ошибка — просто специальный случай более общей пробле- мы, которую я покажу, вернувшись последний раз к моей аудитории из 1200 че- ловек. Предположим, что каждый в аудитории знает, под каким знаком Зодиака он родился. Если я попрошу всех людей, родившихся под созвездием Овна, под- нять руки, я ожидаю увидеть 100 рук — существует 12 знаков Зодиака, и мы предполагаем равномерное распределение данных, то есть селективность равна 1/12, кардинальность равна 1200/12 = 100. Если я попрошу всех людей, родившихся в декабре, поднять руки, я ожидаю увидеть 100 рук — существует 12 месяцев, и мы предполагаем равномерное рас- пределение данных, поэтому селективность равна 1/12, кардинальность равна 1200/12 = 100. Сколько людей поднимут руки, если я попрошу всех Овнов по гороскопу, ро- дившихся в декабре, поднять руки? А что можно сказать обо всех Овнах по горо- скопу, родившихся в марте? А об Овнах по гороскопу, родившихся в апреле? Согласно логике Oracle, ответ будет одним и тем же для всех трех вопросов. о Селективность (месяц AND знак зодиака) = селективность (месяц) х селек- тивность (знак зодиака) = 1/12 х 1/12 = 1/144. о Кардинальность = 1,200 х 1/144 = 8,5 (округленная до 8 или 9 в зависимости от версии Oracle). Но Овнами считаются те, кто родился с 21 марта по 19 апреля, так что нель- зя родиться в декабре и быть Овном по гороскопу; около 35 из 100 людей, ро- дившихся в марте, будут Овнами, и около 65 из 100, родившихся в апреле, также будут Овнами. Знаки зодиака и календарные даты не являются независимыми, но формулы Oracle для вычисления комбинаций предикатов предполагают, что предикаты независимы. Как только вы применяете несколько предикатов к одной таблице, вы долж- ны задать вопрос, есть ли некоторая зависимость между столбцами, которые вы проверяете. Если существует зависимость, оптимизатор вычислит неправиль-
Заключение 87 ные селективности, что означает и неправильные кардинальности и легко мо- жет привести к неподходящим планам выполнения. ПРИМЕЧАНИЕ вспомните, как 8i недооценил кардинальность списка значений. Мы ожидали кардинальность, рав- ную 300, для monthjro in (6,7,8), но получили кардинальность 276. Если расширить формулу для се- лективности двух предикатов, соединенных по OR, для случая трех предикатов, она будет выгля- деть следующим образом: sel(A or В or С) = sel(A) + sel(B) + sel(C) - sei(A)sei(В) - sel(B)sel(C) - sel(C)sel(A) + sel(A)sel(B)sel(C) Сдельная селективность для каждого месяца равна 1/12, но поместите 1/12 в предыдущую форму- лу, и ответ будет: 3/12 - 3/144 + 1/1728 = 0,22975. Умножьте это на 1200 записей, и получите ответ 275,69 — кардинальность, которую выдает 8i. Ошибка Oracle в 81 состояла в том, что он использовал общий метод обработки предикатов, соеди- ненных по OR, при раскрытии списков значений; он не распознавал этот специальный случай. Это Обычная ситуация с оптимизатором: множество улучшений, которые появляются в более новых версиях, — это результат распознавания специальных случаев и более правильной их обработки. Улучшения — это хорошо, но все, что меняет кардинальность операции (даже если исправляет ее), может привести к тому, что план выполнения неожиданно изменится. Особо обратите внимание на то, что тот же самый запрос с различными Входными значениями может означать, что стандартная оценка кардинальности слишком высока (Овны в декабре) или слишком низка (Овны в апреле). Это означает, что проблема зависимостей, особенно между столбцами в одной таб- лице, не из тех, что могут быть решены автоматически. 9i в качестве частичного решения предлагает динамическую выборку (dynamic sampling), a 10g предлагает профили (profiles) — обе технологии мы подробнее разберем в главе 6. Но так как ошибка в кардинальности может проявиться любым образом на Одном и том же входном тексте, единственное полное решение требует от Oracle выполнять оптимизацию для каждого набора входных значений по мере ИХ появления, выполняя выборку данных. Это может быть выполнимо в среде, Представляющей собой хранилище данных, но нежизнеспособно в высокопро- изводительных OLTP-системах, потому что в результате неизбежно появятся дополнительное использование ресурсов и конкуренция за ресурсы. Заключение Чтобы оценить количество записей, возвращаемых множеством предикатов, оп- тимизатор сначала вычисляет селективность (долю возвращаемых данных), а затем умножает это значение на количество записей на входе. В случае предиката с одним столбцом оптимизатор использует либо количе- ство уникальных значений, либо плотность в качестве основы для вычисле- ния селективности предиката равенства. Для диапазонных предикатов с одним столбцом оптимизатор основывается на селективности дроби необходимый диа- пазон / весь доступный диапазон с некоторыми корректировками для конечных значений.
88 Глава 3. Селективность для одной таблицы Для диапазонных предикатов, использующих переменные связывания, оп- тимизатор использует жестко закодированные константы для селективности: 5 % (0,05) для неограниченных диапазонов, 0,25 % (0,0025) или 9,75 % (0,975) для ограниченных диапазонов. Оптимизатор объединяет предикаты, используя формулы для вычисления вероятности независимых событий. Это может привести к ошибкам в селектив- ности (следовательно, в кардинальности), если предикат включает столбцы, со- держащие наборы данных, которые не зависят друг от друга. Запросы, использующие списки значений, показывают некоторое особенное поведение. В 8i обработка i п и not i п последовательна (и неправильна). Обра- ботка 1 п скорректирована в 9i и 10g, но будет выдавать вводящие в заблужде- ние результаты для списков с переменными связывания или значениями, выхо- дящими за пределы ожидаемого диапазона значений для столбца. Обработка not in все еще неправильна в 9i и 10g, в этих версиях используются те же са- мые вычисления, что и в 8i. Тестовые сценарии Файлы к этой главе, доступные для загрузки, перечислены в табл. 3.7. Таблица 3.7. Тестовые сценарии к главе 3 Сценарий Комментарии birth_month_01.sql hack_stats.sql Код базового примера Шаблонный сценарий для модификации некоторых статистик уровня объектов birth_month_02.sql injistsql in_list_02.sql Добавляет неопределенные значения к базовому примеру Запросы к базовому примеру со списками значений разной длины Запросы к модифицированному примеру со списками значений разной длины oddities.sql Коллекция запросов со списками значений, в которых получаются некоторые нежелательные результаты in_list_03.sql pv.sql Запросы к базовому примеру с in и not in Секционированные представления и списки значений (версии значительно варьируются) in_list_10g.sql ranges.sql selectivity_one.sql Демонстрация изменений в 10.1.0.4 Запросы к базовому примеру с различными типами диапазонов Демонстрация граничного случая для проверок, выходящих за диапазон нижних/верхних значений like_test.sql ranges_10g.sql ranges_02.sql two_predicates_O l.sql bind_between.sql Пример предиката строковое значение like :bind Демонстрация изменений в 10.1.0.4 Странности с предикатом столбец < X or столбец >= X Тестовый пример для двух независимых предикатов Аномалии при обработке диапазонных предикатов с переменными связывания setenv.sql Устанавливает стандартную среду для SQL*Plus
4 Простой доступ по бинарному дереву В 'J гой главе мы исследуем арифметику, с помощью которой оптимизатор вычис- ЙЙЙт стоимость использования простого индекса в виде В-дерева для доступа Одной таблице. Это исследование не будет исчерпывающим, мы сосредото- чимся только на основных принципах и пройдемся по нескольким специаль- ном случаям, в которых оптимизатор немного «подкручивает» значения. В основном я охвачу диапазонные сканирования, включая полные сканиро- МНИЯ и сканирования, использующие только индекс, и кратко упомяну об уни- кальных сканированиях. Конечно, есть и другие способы использования индек- сов — мы уже видели быстрые полные сканирования в главе 2 и рассмотрим сканирование с пропусками и индексные соединения в томах 2 и 3. ч Для этой главы требуется, чтобы вы были хорошо знакомы с концепцией се- лективности и тем, как оптимизатор вычисляет ее, так что, возможно, вам при- дется прочитать главу 3, перед тем как работать над этой главой. Основы оценки индексного доступа Кш вы видели в предыдущих главах, стоимость запроса — это мера времени его ЖВСршения. Один из наиболее значительных факторов, влияющих на время, Шйбходимое для выполнения, — это количество реальных физических запро- СЙВ Ввода-вывода, которые должны быть выполнены. Это очень четко показано I Основной формуле для индексного пути доступа для одной таблицы. Тем не менее, перед тем как цитировать формулу, стоит мысленно пред- Стввдть, что надо сделать при прохождении типичного индексного пути Доступа. О У вас есть предикаты по некоторым столбцам, которые определяют ин- декс. Вы находите корневой блок (root block) индекса. О Спускаетесь по нетерминальным уровням (branch levels) индекса к листо- вому блоку (leaf block), представляющему собой единственное место для
90 Глава 4. Простой доступ по бинарному дереву первой возможной записи (стартовый ключ, start key), которая может соот- ветствовать вашим предикатам. о Вы проходите по цепочке листовых блоков, пока не проскочите последнюю запись (стоп-ключ, stop key), которая может соответствовать вашим преди- катам. о Для каждой записи индекса вы решаете, обращаться к блоку таблицы или нет. Таким образом, формула для стоимости доступа к таблице посредством ин- декса должна охватывать три компонента, связанных с блоками: количество не- терминальных уровней, которые надо пройти, количество листовых блоков, ко- торые надо пройти, и количество блоков таблицы, к которым надо обратиться. Как мы знаем, оптимизатор предполагает, что обращение к одному блоку при- равнивается к реальному запросу ввода-вывода, поэтому количество обраще- ний к блокам и является стоимостью (если вы не включили оценку стоимости процессорных ресурсов). СКАНИРОВАНИЕ С ПРОПУСКАМИ (SKIP SCANS) Так как листовые блоки индекса содержат указатели и на следующий элемент, и на предыдущий, стандартный механизм сканирования диапазона (range scan) для использования индекса должен только один раз пройти по нетерминальным уровням, чтобы найти начальный блок (независимо от того, выполняется ли сканирование диапазона по возрастанию или по убыванию). Механизм сканирования с пропусками, который будет рассмотрен позже, заставляет Oracle прохо- дить вверх и вниз по нетерминальным уровням. Изменяется не только арифметика, но и стратегия закрепления буферов. Каждый блок в пути от корневого блока до текущего листового, похоже, за- крепляется при сканировании листового блока — вы ведь не хотите, чтобы кто-то другой разбил вашу буферизованную копию нетерминального блока, если собираетесь практически сразу вер- нуться к нему. Выражение, которое я называю базовой формулой для оценки стоимости индексного доступа, впервые было обнародовано в документе Вольфганга Брейтлинга (Wolfgang Breitling) (www.centrexcc.com) на конференции IOUG-A в 2002 году. Эта формула состоит точно из тех трех компонентов, которые я только что описал: стоимость = blevel + ceilingCleaf_blocks * эффективная селективность индекса) + ceiИng(clustering_factor * эффективная селективность таблицы) о Первая строка формулы представляет собой количество обращений к бло- кам, необходимых для прохода по индексу (исключая стоимость фактиче- ского нахождения первого листового блока, который вам необходим). В Oracle реализована версия сбалансированных В-деревъев (Balanced B-trees), так что ка- ждый листовой блок находится на одинаковом расстоянии от корневого бло- ка, и мы достаточно уверенно можем говорить о высоте индекса. Количество уровней нетерминальных блоков, которое надо пройти, одно и то же незави- симо от того, до какого листового блока надо дойти.
Начало 91 © Вторая строка формулы представляет собой количество листовых блоков, которое надо пройти, чтобы получить все идентификаторы записей (rowid), соответствующие заданному набору входных значений. Эффективная селек- тивность индекса (effective index selectivity) соответствует записи под назва- нием ix_sel в файле трассировки 10053. © Третья строка представляет собой количество обращений к блокам таблицы, Которое надо выполнить, чтобы получить записи посредством выбранного индекса. Эффективная селективность таблицы (effective table selectivity) со- ответствует параметру, который имеет название tb_sel в файле трассиров- ки 10053, но в файле трассировки 10g называется (более точно) i x_sel_wi th_ filters. Эта строка часто создает наибольший компонент стоимости и вно- сит наибольшую ошибку в вычисление стоимости использования индекса, представленного В-деревом. Мы рассмотрим различные причины этих оши- бок и способы их исправления в главе 5. ВЫСОТА ИНДЕКСА ЙСй еще существует несколько документов и презентаций, в которых очень много рассказывается об Индексах, основанных на последовательностях, порождающих дополнительные уровни с правой Шроны. Этого не происходит. Когда листовой блок расщепляется, расщепление идет вверх, если это необходимо, а не вниз. Все листовые блоки в сбалансированном В-дереве будут находиться на одинаковом расстоянии от корня. Яддавно мне попалась статья, в которой описывался механизм, используемый RDB для добавления узпов переполнения в случае повторяющихся неуникальных ключей. Если это описание на самом Дёле верно, возможно, что неправильное понимание реализации этого процесса в Oracle является до0очным эффектом, когда человек переходит с одной платформы на другую и считает, что его Предыдущее знание все еще уместно. Значения blevel, leaf_blocks и clustering_factor доступны в представ- лении user_i ndexes после того, как вы соберете статистику. Эффективная се- лективность индекса и эффективная селективность таблицы вычисляются во время оптимизации на основании используемых предикатов. Мы исследуем то, как вычисляются эти две селективности, на рабочем примере. Итак, оптимизатор оценивает количество обращений к блокам, предполага- ет, что каждое обращение превратится в запрос ввода-вывода, и в основном это И есть стоимость. Существуют многочисленные специальные случаи, масса по- бочных эффектов, таких как «подкрутка» параметров и взлом статистики, и не- которые аномалии, связанные с ошибками и улучшениями в разных версиях Кода. При использовании 9i с его оценкой стоимости процессорных ресурсов МЫ также должны добавить процессорный компонент для каждого блока, к ко- торому выполняется обращение, и еще один процессорный компонент для про- смотра данных. Начало Давайте создадим пример, чтобы увидеть различные части этой формулы в дей- ствии. Как обычно, в моей демонстрационной среде используется размер блока
92 Глава 4. Простой доступ по бинарному дереву 8 Кбайт, экстенты по 1 Мбайт, локально управляемые табличные пространства, ручное управление пространством сегментов, системная статистика отключена (оценка стоимости процессорных ресурсов). Этот пример взят из сценария btree_.cost_01.sql в онлайн-хранилище кода: create table tl as select trunc(dbms_random.value(0,25)) nl, rpad('x',40) ind_pad, trunc(dbms_random.value(0,20)) n2, lpad(rownum,10,'0') small_vc, rpad('x',200) padding from all_objects where rownum <= 10000 create index tl_il on tl(nl, ind_pad, n2) pctfree 91 Кое-что требует небольшого пояснения. Во-первых, установка pctfree для индекса необычно большая; я сделал это, чтобы индекс распределился по боль- шому количеству листовых блоков при первоначальном создании. Но pctfree не применяется к нетерминальным блокам — вот почему я добавил ind_pad в качестве второго столбца индекса. Так как этот столбец содержит одинаковые значения для всех записей, это не сказывается на общей статистике и распреде- лении, но не дает Oracle возможности упаковать множество записей в каждый нетерминальный блок, что легко поднимает индекс до blevel 2. PCTFREE ДЛЯ ИНДЕКСОВ В применении к индексам параметр хранения pctfree имеет несколько отличное значение по сравне- нию с таблицами. Для индексов pctfree имеет смысл только при создании индекса, перестроении или объединении и применяется только к листовым блокам. Для таблиц параметр хранения pctfree указывает Oracle, когда остановить вставку новых записей в блок, чтобы в каждом блоке было оставлено место для модификации существующих в нем запи- сей. Но элементы в индексе никогда не модифицируются — когда вы изменяете элемент в индексе, он обычно перемещается куда-то в другое место индекса, таким образом, модификация индекса — это на самом деле удаление, за которым следует вставка, так что вы резервируете место не для мо- дификаций, а для новых записей. Обратите внимание: nl создается так, что будет содержать 25 различных значений (от 0 до 24), а п 2 будет содержать 20 различных значений (от 0 до 19). Из-за случайного распределения мы, вероятно, увидим все 500 различных воз- можных комбинаций чисел, около 20 записей на комбинацию (10 000 записей разделить на 500 комбинаций). После использования пакета dbms_stats для вычисления статистики по таблице и индексу вы должны получить результаты, показанные в табл. 4.1.
«мало Ж#*—... Цеблица 4.1. Статистические данные для используемой таблицы и ее индекса ^тистика Значение Качество записей в таблице 10 000 качество блоков таблицы (ниже уровня максимального заполнения) ^ИЧество записей (элементов) индекса 371 10 000 Количество листовых блоков индекса 1111 индекса 2 »цчество уникальных ключей 500 РКТОР кластеризации индекса 9745 ||^днее количество листовых блоков индекса на ключ 2 ^йднее количество блоков данных (таблицы) индекса на ключ 19 ^дичество уникальных значений в столбце п! 25 качество неопределенных значений в столбце nl 0 ^фНОсть столбца п! 0,04 Количество уникальных значений в столбце п2 20 Ьичество неопределенных значений в столбце п2 0 Леность столбца п2 0,05 ®жчество уникальных значений в столбце IND_PAD 1 Количество неопределенных значений в столбце IND_PAD 0 $ЙЙТНОсть столбца IND_PAD 1 Теперь переключимся к autotrace и выполним следующий запрос. Auto- под 9i или 10g отобразит план выполнения, который следует за этим за- просом. $«lect smaU_vc frwn tl nl = 2 Ittd ind_pad = rpad('x',40) W n2 = 3 I ЙОН выполнения (9.2.0.6 и 10.1.0.4, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=25 Card=20 Bytes=1160) И TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=25 Card=20 Bytes=1160) >1 INDEX (RANGE SCAN) OF 'Т1_1Г (NON-UNIQUE) (Cost=5 Card=20) В этом запросе оптимизатор выбрал индекс и показал стоимость 25, с карди- фдмостью 20. Мы знаем, что кардинальность оценена хорошо, но как получи- Шйь стоимость? Первая часть стоимости получается из строки 2 (Cost = 5) для Обращения к индексу, а следующая часть (Cost = 25, приращение 20) получает- ся да строки 1 для обращения к таблице. Мы не должны быть удивлены тем фактом, что стоимость обращения к таб- лице получилась равной 20. Мы ожидали около 20 записей и знали, что записи распределены по всей таблице. Так что мы можем принять, что добавочная ЙТОймость 20 в строке, связанной с таблицей, соответствует 20 различным бло-
94 Глава 4. Простой доступ по бинарному дереву кам, к которым нам надо обратиться, чтобы получить эти 20 различных за- писей. Аналогично, когда мы видим, что стоимость использования индекса была равна 5, мы можем отдать две единицы на спуск по нетерминальным уров- ням (blevel индекса равен 2). Но остаются еще три единицы, которые надо учесть, — возможно, оптимизатор посчитал, что мы пройдем три листовых бло- ка, чтобы получить 20 необходимых идентификаторов записей. Такое неформальное размышление полезно в качестве отправной точки, но если мы решим исследовать формулу и лучше понять использование индексов оптимизатором, нам надо узнать значимость двух селективностей и фактора кластеризации (clustering_factor). Эффективная селективность индекса В главе 3 я описывал селективность как долю записей, которые оптимизатор ожидает найти на основании предоставленных предикатов. Но теперь я поднял ставки до двух селективностей — эффективной селективности индекса и эф- фективной селективности таблицы. Принцип тот же самый. На секунду забудьте о таблице; какую долю элемен- тов индекса для записей мы вернем, если мы использовали три предиката из за- проса? Все три предиката основываются на столбцах, которые есть в индексе, так что мы исследуем их все, начиная с их плотности или num_di sti net: nl = {константа} (Расчет: 1 запись из 25, или 4% записей, или 0.04 * количество записей) ind_pad = {константа!} (Расчет: 100% записей) п2 = {константа} (Расчет: 1 запись из 20, или 5% записей, или 0.05 * количество записей) Применим формулу из главы 3 для объединения селективностей: селективность (Р и Q) = селективность (Р) * селективность (Q) Но у нас есть три события, так что мы должны немного расширить формулу: селективность (X и Y и Z) = селективность ((X и Y) и Z) = селективность (X и Y) * селективность (Z) = селективность (X) * селективность (Y) * селективность (Z) Мы просто перемножаем все три селективности. В этом случае эффектив- ная селективность индекса равна 0,04 х 1 х 0,05 = 0,002 (запомним это значе- ние). Предикаты заставляют нас посетить 0,2 % элементов индекса. Однако листовые блоки индексов расположены в отсортированном порядке. Если мы определим, что нам необходимо рассмотреть X % записей индекса, то нам необ- ходимо пройтись по X % листовых блоков, чтобы сделать это. Вот почему один из компонентов стоимости равен leaf_blocks х эффективная селективность индекса. В этом месте вы можете решить остановиться, отметив одну странность. В представлении user_indexes есть столбец под названием di stinct_keys — из него я получил значение 500 для моего списка статистических данных не-
10 95 только страниц назад. Оптимизатор «знает», что этот индекс содержит 500 уличных элементов, и я просто использовал запрос каждый столбец индекса) = {константа} Почему оптимизатор просто не сообщит, что существует 500 возможных на- &ров значений в индексе, а вам необходим один возможный набор значений, что селективность равна 1/500 = 0,002? Хороший вопрос, и совершенно разумное предложение. Оптимизатор действи- ®доно перемножает отдельные селективности вместо просмотра объединенной се- лективности индекса. (Похоже, это лишь один специальный случай, который ди рассмотрим в главе 11.) Тем не менее не забудьте, что Oracle записывает gdensity, и num_distinct для каждого столбца. Возможно, что стратегия пе- ремножения селективностей отдельных столбцов — это универсальное реше- рда, которое учитывает искаженные данные, когда нет гистограмм и количеству рНИкальных значений в индексе нельзя доверять. Эффективная селективность таблицы Мм уже знаем, как вычислять селективности таблиц; и это особенно легко ^случае, если все предикаты, относящиеся к таблице, соединены по AND. Мы HfpCTO перемножаем отдельные селективности. Но в случае индексов есть изменение, которое надо рассмотреть. Представь- 1^ЛТО мой тестовый пример включает дополнительный предикат small_vc = « ’0000000001'. Если вы решите обращаться к таблице посредством сущест- вующего индекса, вы не сможете проверить этот последний предикат, пока не достигнете таблицы, так что этот предикат не влияет на долю данных, к кото- рым вы собираетесь обратиться, — он влияет только на долю данных, которые будут возвращены в конечном итоге. При получении стоимости использования индекса эффективная селектив- Ийсть таблицы должна основываться только на тех предикатах, которые могут быть вычислены в индексе до того, как таблица будет достигнута. (Вот почему М Использую термин «эффективная селективность таблицы», а не просто «се- ДрКТИвность таблицы», и вот почему в 10g release 2 соответствующий параметр ЙШЛ переименован в 1x_sel_wi th_f 1 Iters.) В этом случае все предикаты для таблицы могут быть разрешены с исполь- зованием индекса, так что мы с уверенностью можем сказать, что эффективная ^ЛШпивностъ таблицы равна (также) 0,04 х 1 х 0,05 = 0,002. Фактор кластеризации (clustering_factor) Фактор кластеризации (clusteri ng_f actor) — это мера для сравнения упоря- ДрЯСННости индекса с уровнем неупорядоченности в таблице. Оптимизатор, по- ®же, вычисляет cluster! ng_factor, проходя таблицу в порядке индекса и за- поминая количество перемещений от одного блока таблицы к другому. (Конечно, Ж самом деле он так не работает; этот код просто сканирует индекс и извлекает адреса блоков таблицы из идентификаторов записей.) При каждом переходе ШвТчик увеличивается — конечное значение счетчика и является значением Cluster! ng_factor. Рисунок 4.1 иллюстрирует этот принцип.
96 Глава 4. Простой доступ по бинарному дереву Фактор кластеризации 53153 88828 66 10 6 10 94497 Таблица Рис. 4.1. Вычисление фактора кластеризации На рис. 4.1 показана таблица с четырьмя блоками, 20 записями и индексом по столбцу VI, значения которого также показаны. Если вы пойдете по основа- нию индекса, первый rowid указывает на третью запись в первом блоке. Мы еще не посетили ни один блок, так что это новый блок, и значение счетчика равно 1. Сделаем еще один шаг по индексу, и г owl d укажет на четвертую запись во втором блоке — мы сменили блок, так что увеличим счетчик. Сделаем еще один шаг по индексу, и rowid укажет на вторую запись в первом блоке — мы снова сменили блок, так что снова увеличим счетчик. Сделаем еще один шаг по индексу, rowid укажет на пятую запись первого блока — мы не сменили блок, так что не будем увеличивать счетчик. На приведенной диаграмме я поместил значение под каждой записью табли- цы, чтобы показать значение счетчика, когда при прохождении мы достигаем этой записи. К тому времени, как мы доберемся до конца индекса, мы сменим блоки таблицы десять раз, так что clusteri ng_factor имеет значение 10. Обратите внимание на то, как небольшие группы данных останавливают рост clusteri ng_factor — взгляните на блок 2, в котором значение 8 появля- ется четыре раза, потому что четыре последовательных элемента индекса ука- зывают на один и тот же блок; тот же эффект обнаруживается в блоке 3, трем записям присвоено значение 6. Таблица не должна быть полностью отсортирована, чтобы такая ситуация произошла; в ней просто должны присутствовать небольшие группы (или кла- стеры) записей, которые почти отсортированы, — вот почему используется тер- мин фактор кластеризации, а не фактор сортировки. Зная, как вычисляется clusteri ng_factor, вы можете оценить, что его наи- меньшее возможное значение равно количеству блоков в таблице, а наибольшее возможное значение равно количеству записей в таблице, если у вас есть вы- численная статистика. Если существует множество блоков, подобных блоку 2 в таблице, clus- teri ng_f ас to г окажется достаточно близким к количеству блоков в таблице, но если данные в таблице распределены равномерно, clusteri ng_factor будет иметь тенденцию приближаться к количеству записей в таблице.
jjWiq 97 Индексы и фактор кластеризации Исторически сложилось мнение, что хороший индекс имеет низкий фактор кластеризации, а плохой ,,илАкс имеет высокий фактор кластеризации. Очевидно, в этом утверждении есть доля правды, особенно в свете того, что представляет собой ^tetering factor. Однако я всегда испытывал неприязнь к словам «низкий», «высокий», «малень- кий», «большой» и выражениям вроде «близкий к нулю», когда речь идет об Oracle. Например, ift&OO —это низкий clustering_factor или высокий? Он будет низким, если в таблице есть 10 000 бло- и высоким, если в таблице 100 блоков. Так что вы можете попробовать написать несколько не- больших сценариев, которые соединяют user_tables с userjndexes (и другие сценарии для секцио- нированных таблиц и т. д.), чтобы сравнить эти критические значения. фактически по причинам, которые я опишу в главе 5, я часто использую столбец avg_data_ bfocks per key, чтобы понять, насколько хорошим Oracle считает индекс. Итак, почему clusteri ng_factor используется в формуле для оценки стои- ЮСТИ? Если вы получаете более одной записи из таблицы посредством индекса другими словами, если вы выполняете сканирование диапазона индекса), вы йроходите часть индекса — скажем, X % индекса. По мере прохождения индекса вы будете перескакивать в таблице от записи к записи. Если clustering_ Ж Ю г является действительно репрезентативным представлением распределе- ЗШ данных по таблице, то при прохождении X % индекса количество смен бло- ЖОВ таблицы и будет равно X % от clusteri ng_factor. Оптимизатор ведет себя так, как если бы каждая смена блока приводила вас Ж блоку, который вы до этого не посещали, что в свою очередь требует выполне- ния запроса ввода-вывода. В некоторых случаях это может быть довольно ра- зумным предположением и объясняет, почему значение clustering_factor х х эффективная селективность таблицы, округленное в большую сторону, пред- ставляет собой финальный компонент стоимости. Конечно, у этого подхода есть недостатки, и мы рассмотрим их более под- робно в главе 5. Рассмотрим, например, первый блок таблицы на рис. 4.1. Если мы запустим запрос, который использует индекс, чтобы посетить все пять запи- сей в этом блоке, оптимизатор в вычислении стоимости учтет три запроса фи- зического ввода-вывода (в блоке три номера посещений). Но блок, возможно, будет помещен в буфер (и, возможно, закреплен) для всех посещений после Первого. Достаточно распространена ситуация, когда эта часть формулы полу- шёт значение, которое не соответствует действительности. Благодаря механиз- му упреждающей выборки (table pre-fetch), который появился в 9i (см. главу 11), вероятно, будет постоянно расти различие между вычисленными стоимо- ШЯМИ и фактическими обращениями к блокам во все увеличивающемся коли- честве случаев. ЩБерем все вместе У Нас есть следующая формула для пути доступа с использованием индекса: cost - blevel + ceiling(leaf_blocks * эффективная селективность индекса) + ceiling(clustering_factor * эффективная селективность таблицы)
98 Глава 4. Простой доступ по бинарному дереву Мы знаем, что представляет собой каждый из элементов, так что можем про- верить на нашем простом примере, дает ли формула правильный ответ, — дру- гими словами, дает ли она значения, которые мы увидим в выводе autotrace. Давайте просто возьмем значения из нашего первого теста: blevel = 2 Эффективная селективность индекса = 0,002 (как было вычислено ранее). leaf_blocks = 1,111 Эффективная селективность таблицы = 0,002 (как было вычислено ранее). clustering_factor = 9,745 Количество записей в таблице = 10 000. Итак, в соответствии с формулой, стоимость будет равна 2 + ceiling(l,111 * 0.002) + ceiling(9,745 * 0.002) = 2 + ceiling(2.222) + ceil1ng(19.49) = 2 + 3 + 20 = 5 + 2® = 25 Сравните это с планом выполнения, в котором стоимость в строке с индек- сом была равна 5 (что, как мы предположили, соответствует 2 для прохождения нетерминальных блоков и 3 для прохождения листовых блоков), а суммарная стоимость была равна 25. Полное соответствие. Таким образом, формула подходит для этого простого случая; результаты вычислений соответствуют значениям, полученным в листинге плана выполне- ния. Что же конкретно мы узнали о том, как воспринимает оптимизатор стои- мость использования индексов на В-деревьях? Практический опыт показывает, что blevel обычно имеет значение 3 или меньше, часто 2, иногда 4; индексы имеют тенденцию быть плотно упакованны- ми; элементы индекса обычно меньше записей таблицы, так что листовые бло- ки обычно содержат намного больше элементов, чем соответствующие блоки таблицы; и количество листовых блоков в индексе часто мало по сравнению с количеством блоков таблицы. , Итак, для любого набора предикатов, который затрагивает более трех или четырех записей в таблице, наиболее значимый компонент в вычислении стои- мости, весьма вероятно, зависит от clustering_factor, то есть от того, на- сколько случайно распределены необходимые данные. Если оптимизатор считает, что данные в вашей таблице хорошо кластеризо- ваны, то стоимость будет низкой и предпочтительно использовать индексы. Если оптимизатор считает, что данные в таблице сильно рассеяны, то стои- мость будет выше, и предпочтительно использовать альтернативные планы вы- полнения (например, табличные сканирования). Вопреки интуиции, состояние вашей таблицы может производить намного большее впечатление на вычисления оптимизатора, чем состояние самого ин- декса. Конечно, если данные на самом деле распределены так, как считает оптими- затор, то вычисление стоимости будет часто близко к реальному использова- нию ресурсов во время выполнения, и вы будете считать, что оптимизатор хо-
Начало 99 рошо поработал. Если cluster!ng_factor не представляет реальное распреде- дение, то вычисление стоимости будет неправильным, и оптимизатор, вероятно, выберет неподходящий план выполнения. ПЕРЕСТРОЕНИЕ ИНДЕКСОВ Зачастую можно уменьшить количество листовых блоков (и очень редко — blevel) индекса, пере- строив индекс; но перестроение индекса не имеет никакого эффекта по отношению к clustering factor. Перестроение индекса может сделать этот индекс более предпочтительным для оптимизатора, но побочные эффекты могут быть как хорошими, так и плохими. Положительный эффект состоит в том, что перестроение индекса может привести к преимуществам кэширования этого индекса во время выполнения запроса, но обратная сторона состоит в конкуренции при выполнении DML и увеличе- нии расщеплений листовых блоков и повторов, генерируемых по мере того, как индекс «пытается» вернуться в состояние равновесия. Вопреки интуиции, это может привести к тому, что индексу «по- надобится» регулярное перестроение, потому что вы начали перестраивать его регулярно (см. сце- нарий rebuild_test.sql в онлайн-хранилище кода, в котором приведен искусственный пример). ggnn вы хотите, чтобы оптимизатор вел себя правильно, обычно намного важнее проверять, соот- ветствует ли dustering_factor реальному распределению данных, и делать что-то с этим, вместо того чтобы просто втискивать индекс в более узкие рамки. Расширение алгоритма Давайте рассмотрим более общие примеры вычисления стоимости: О Как обрабатывать диапазонные проверки (например, n2 between 1 and 3)? Q Как обрабатывать частичное использование индекса из нескольких столб- цов? О Какие эффекты будут в случае использования списков значений? О Насколько специальным случаем является полное сканирование индекса? О Что делает оптимизатор, если не требуется обращаться к таблице? Как обрабатывать проверки диапазонов (например, п2 between 1 and 3)? Давайте попробуем и посмотрим. Запустите снова следующий запрос (btree_ cost_02.sql в онлайн-хранилище кода) по нашему базовому набору данных С включенной автотрассировкой: select /*+ index(tl) */ small_vc from tl where nl = 2 end ind_pad = rpad('x',40) and n2 between 1 and 3 План выпопнения (9.2.0.6 и 10.1.0.4) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=93 Card=82 Bytes=4756)
100 Глава 4. Простой доступ по бинарному дереву 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=93 Card=82 Bytes=4756) 2 1 INDEX (RANGE SCAN) OF ’T1_I1' (NON-UNIQUE) (Cost=12 Card=82) Обратите внимание, что мне пришлось включить в оператор подсказку ис- пользования индекса, так как стоимость использования индекса значительно выше стоимости табличного сканирования. (Количество блоков ниже макси- мального уровня заполнения было 371, и моя установка db_file_mul.tibl.ock_ read_count была 8, так что в отсутствие оценки стоимости процессорных ресур- сов 9i вычислил стоимость табличного сканирования как 1 + 371/6,588 = 58.) Как и ранее, мы должны подумать о селективности. Отдельные селективно- сти столбцов i nd_pad и nl не изменились и равны 1 и 0,04 соответственно, но теперь у нас есть диапазонный предикат по п2. В соответствии с главой 3 мы можем получить формулу для предиката between: селективность (n2 between 1 and 3) = требуемый диапазон / суммарный диапазон + l/num_distinct + l/num_distinct = (3 - 1) / (19 - 0) + 1/20 + 1/20 = 0.205263 Как и в предыдущем примере, каждый предикат применяется и к индексу и к таблице, так что это значение может применяться в обоих местах в формуле. После подстановки значений мы получим следующее: Эффективная селективность индекса = 1 * 0.04 * 0.205263 = 0.0082105 Эффективная селективность таблицы = 1 * 0.04 * 0.205263 = 0.0082105 Стоимость = 2 + ceiIing(l,111 * 0.0082105) + -- 10 ceiling(9,745 * 0.00082105) -- 81 = 12 (строка с индексом в плане) + 81 = 93 -- как и требовалось В этом тесте есть небольшая проблема, которую я не показал. Я назвал план выполнения «(9.2.0.6 и 10.1.0.4)». Когда я повторил этот тест на 8i, появилось небольшое отличие — кардинальность изменилась с 82 на 83. Вычисленная кардинальность определяется так: Количество записей на входе * вычисленная селективность 10,000 * 0.00882105 = 82.105 -- это должно быть равно 82 или 83? На самом деле это нечто вроде того, что мы видели ранее в главе 3. 8i, похо- же, всегда округляет кардинальность в большую сторону (функция с е i I i n g (), или, как она называется в SQL, cei I ()), но 9i и 10g выполняют округление до ближайшего целого значения (функция round ()). Еще о диапазонных проверках Мы выбрали простой вариант и выполнили диапазонную проверку по послед- нему столбцу в индексе. Что произойдет, если мы выполним диапазонную про- верку по более раннему столбцу в индексе? Попробуйте такой пример: alter session set "_optimizer_skip_scan_enabled"=false: select /*+ index(tl) */ small_vc from tl where nl between 1 and 3 and ind_pad = rpad('x',40)
jgiwflo Ю! i3dd П2 = 2 ? План выполнения (8.1.7.4) Ц SELECT STATEMENT Optimizer=ALL_ROWS (Cost=264 Card=82 Bytes=4756) 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=264 Card=82 Bytes=4756) 1 INDEX (RANGE SCAN) OF 'T1_U' (NON-UNIQUE) (Cost=184 Card=82) План выполнения (9.2.0.6 и 10.1.0.4) | SELECT STATEMENT Optimizer=ALL_ROWS (Cost=264 Card=82 Bytes=4756) | 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=264 Card=82 Bytes=4756) J, 1 INDEX (RANGE SCAN) OF ’T1_U' (NON-UNIQUE) (Cost=184 Card=1633) Команда alter session используется для 9i и 10g. План выполнения без додсказок использовал полное табличное сканирование, но если я сначала вставлю подсказку для индекса, оптимизатор настаивает на использовании ме- данизма index skip scan, и в 10g, если я добавляю подсказку no_index_ss(), индекс отключается и план выполнения возвращается к использованию таб- личного сканирования (я склонен считать такое поведение ошибкой — мне ка- жется достаточно разумным сказать: «Используй этот индекс, но не выполняй skip scan», — но, возможно, это намеренное поведение). Важно отметить, что кардинальность этого плана выполнения соответствует Жачению, которое мы увидели в предыдущем запросе (82), но стоимость на- шего выше (264 вместо 93). Давайте попробуем использовать эту формулу стандартным образом и посмотрим, что произойдет. 0 Селективность столбца nl равна (3 - 1) / (24 - 0) + 2/25 = 0,1633333. Q Селективность i nd_pad по-прежнему равна 1. Q Селективность п2 по-прежнему равна 0,05. Так что мы можем вычислить объединенную селективность как 1 х X 0,1633333 х 0,05 = 0,0081667 (и, умножив это на количество записей в таблице и округлив, мы получим 82, как и сообщает план). Если мы подставим эти значения в оба места в формуле, какие результаты МЫ получим? Стоимость = 2 + -- blevel *»iling(0.0081667 * 1,111) + -- 10 ceiling(0.0081667 * 9745) -- 80 f» 92 -- что-то не так, ответ допжен быть равен 264! Вернувшись к плану выполнения, мы увидим, что строка с индексом пока- зывает стоимость, равную 184, а строка с таблицей показывает стоимость, рав- йую 264, разница равна 80, что мы и получим из части формулы, связанной 0 кластеризацией. Это говорит о том, что коэффициент, связанный с листовым блоком индекса, неправилен. Кардинальность в строке с индексом плана 9i/10g дает нам ключ к разгадке: оптимизатор сообщает нам, что он собирается проверить 1633 элемента — ог- ромную часть индекса. Почему? Потому что, когда у нас есть сканирование
102 Глава 4. Простой доступ по бинарному дереву диапазона по столбцу, используемому где-то в середине определения индекса, или мы не можем предоставить условие по такому столбцу, предикаты по по- следним столбцам не ограничивают выбор листовых блоков индекса, которые мы должны проверить. Вот почему в нашей формуле две селективности: эффективная селектив- ность таблицы (которая объединяет все предикаты по столбцам индекса) и эф- фективная селективность индекса (для которой будет использоваться подмно- жество предикатов, основанных на ведущих столбцах индекса). В этом случае эффективная селективность индекса должна вычисляться на основании предиката только по столбцу nl. Так как проверка по nl использует диапазон, предикаты по i ndex_pad и п2 не ограничивают количество листовых блоков индекса, которые мы должны пройти. Конечно, когда мы наконец дохо- дим до исследования листовой записи, мы можем использовать все три преди- ката, чтобы проверить, должны ли мы обращаться к таблице, так что эффектив- ная селективность таблицы все равно включает все три отдельных предиката по столбцам. Итак, эффективная селективность индекса равна 0,1633333 (селективность, которую равнее мы вычислили для столбца n 1), и конечная формула для стои- мости: Стоимость = 2 + -- blevel CeiIing(0.1633333 * 1,111) + -- 182 Ceiling(0.0081667 * 9745) -~ 80 = 184 + 80 = 264 -- как и предполагалось Этот результат подтверждает хорошо известное указание по расположению столбцов в индексе из нескольких столбцов. Столбцы, которые обычно исполь- зуются в диапазонных проверках, должны в обгЦем случае находиться в индексе позже столбцов, обычно участвующих в проверках на равенство. К сожалению, изменение порядка столбцов в индексе может иметь другие противоположные эффекты, которые мы рассмотрим в главе 5. УЛУЧШЕНИЯ В EXPLAIN PLAN В 9i в таблицу plan_table добавлены два очень важных столбца для поддержки explain plan. Это столбцы filter_predicates и access_predicates, которые точно сообщают вам, как и где оптимизатор думает использовать компоненты вашего выражения where. Если вы неэффективно используете индекс, строка с индексом плана выполнения очень точно пока- жет проблему. В столбце access_predicates будут показаны предикаты, используемые для генерации стартстопных ключей индекса, но в столбце filter_predicates будут показаны предикаты, которые не могут быть использованы, пока не будут достигнуты листовые блоки (другими словами, предикаты, которые не должны использоваться при вычислении эффективной селективности индекса). Диапазоны в сравнении со списками значений В главе 3 мы обнаружили странность со списками значений и табличными ска- нирования в 8i. Стоит проверить, проявляется ли эта странность, когда ожидае- мым путем доступа будет доступ по индексу. Итак, выполним простой тест (btree_cost_04.sql в онлайн-хранилище кода) с нашими базовыми данными: select /*+ index(tl) */
Начало ---- 103 small_vc from tl Where nl = 5 and ind__pad = rpad(’x',40) and n2 in (1,6,18) План выполнения (8.1.7.4) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=65 Card=58 Bytes=3364) 1 0 INLIST ITERATOR 2 1 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=65 Card=58 Bytes=3364) 3 2 INDEX (RANGE SCAN) OF ’T1_I1’ (NON-UNIQUE) (Cost=9 Card=58) План выполнения (9.2.0.6 и 10.1.0.4) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=68 Card=60 Bytes=3480) | 0 INLI5T ITERATOR S 1 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=68 Card=60 Bytes=3480) 3 2 INDEX (RANGE SCAN) OF 'T1_U' (NON-UNIQUE) (Cost=9 Card=60) Достаточно очевидно, что стоимость плана выполнения со списком значе- ний изменяется между 8i и 9i. Это не должно вызвать удивления; ошибка в 8i Напрямую связана с селективностью списка значений, и только косвенно со Стоимостью. Ошибка появилась еще до того, как мы приступили к вычислению Стоимости. Так что, если вы используете списки значений с индексами в 8i, стоимость плана выполнения может увеличиться, и оптимизатор может пере- ключиться на другой индекс или даже на табличное сканирование при переходе HU 9i или 10g. 6ptimeer_index_caching и списки значений Параметр optimizer_index_caching появился в 8i для того, чтобы дать некоторую возможность кор- ректировки предположения оптимизатора, что все чтения представляют собой физические чтения. Обычно упоминается, что это влияет на вычисления стоимости доступа к блокам индекса для внут- ренней (второй) таблицы в соединениях с использованием вложенных циклов, но это также влияет На вычисление стоимости итераций по списку значений. Точнее, это не сказывается на вычислении Стоимости для простого индексного пути доступа с одной таблицей. У Меня нет полного понимания того, как этот механизм работает со списками значений, но как толь- ко Параметру optimizer_index_caching присваивается ненулевое значение, эффективная стоимость Компонента blevel формулы, похоже, уменьшается в два раза, после чего вся индексная сосгавляю- щая корректируется на процент использования кэша с обычными странностями функций round() и CeilQ, Которые вносят путаницу. Однако, похоже, существует нижняя граница, определяющая ко- личество листовых блоков, деленное на произведение селективностей столбцов. Выло бы хорошо определить весь алгоритм, но в большинстве случаев достаточно иметь такое гру- бое приближение. Как обрабатывать частичное использование индекса из нескольких столбцов? После примера сканирования диапазона в начале индекса я не вижу необходи- мости рассматривать все подробности для этого случая, потому что это факти-
104 Глава 4. Простой доступ по бинарному дереву чески специальный случай предыдущей проблемы. Возьмем таблицу с индек- сом (coll, со12, соТЗ, со14) и рассмотрим следующий запрос: select * from tl where coll = {const} and col2 = {const} -- без предиката no col3 and col4 = {const} Эффективная селективность индекса вычисляется только по столбцам coll и со 12, а эффективная селективность таблицы — по столбцам coll, со!2 и с о 14. Затем применим основную формулу, и работа сделана. Полное сканирование индекса Есть случаи, когда оптимизатор решает, что оптимальным путем доступа явля- ется чтение всего индекса в правильном порядке, начиная с листового блока на одном конце индекса и затем следуя по листовым указателям, пока не будет достигнут другой конец индекса. ОПТИМИЗАЦИЯ FIRST_ROWS Одним из значений для optimizer_mode в 8i было first_rows. Этот режим все еще существует в 9i и 10g для обратной совместимости, но является устаревшим. Одной из критических особенностей оптимизации first_rows (по сравнению с более новой оптимизацией first_rows_N) было существова- ние нескольких правил, которые перекрывали обычное поведение при оценке стоимости. Одно из таких правил: если существует индекс, который может использоваться, чтобы избежать сортировки, оптимизатор будет использовать его независимо от того, насколько дорогим может оказаться путь доступа. Так что запрос, получавший пять записей и сортировавший их при оптими- зации all_rows, может переключиться на получение 1 000 000 записей, а затем выбросить все из них, кроме пяти, при оптимизации first_rows, если это означает, что можно избежать сортировки. Так что одним из побочных эффектов оптимизации first_rows было сравнительно частое появление полных сканирований индексов. (Фактически вы можете изменить это поведение с помощью изме- нения значения скрытого параметра _sort_elimination_cost_ratio, поскольку значение по умолчанию делало это поведение скорее экстремальным.) Одной из возможных причин такого поведения является избежание сорти- ровки по выражению order by, другой причиной может быть то, что практиче- ски все данные в таблице могли бы быть удалены и индексный доступ только к нескольким оставшимся записям будет быстрее, чем сканирование очень большого количества пустых блоков таблицы. Рассмотрим пример (btree_cost_03.sql в онлайн-хранилище кода): alter session set ,,_optimizer_skip_scan_enabled"=false; select /*+ tndex(tl) */ small_vc from tl where n2 = 2
начало 105 order by nl дран выполнения (8.1.7.4, 9.2.0.6 и 10.1.0.4) 0 SELECT STATEMENT Opt1mizer=ALL_ROWS (Cost=1601 Card=500 Bytes=8500) J, 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=1601 Card=500 Bytes=8500) 1 INDEX (FULL SCAN) OF ’T1_I1' (NON-UNIQUE) (Cost=1113 Card=500) Обратите внимание: в этом примере нет никакой информации о том, что вы- ражение order by присутствует в запросе. Более сложные планы выполнения Когут быть немного более полезны с явными строками с sort (order by) posort. Как в данном случае получается стоимость? У нас нет никаких формальных граничений по индексу, пока мы не достигнем листовых блоков, то есть мы мо- «ем ожидать, что эффективная селективность индекса будет равна 1,00 (100 %). Исследуя листовые блоки, мы можем определить элементы, где п2 = 2, единст- венный предикат с селективностью 0,05, так что мы можем считать, что это бу- дет эффективной селективностью таблицы. Итак, давайте подставим значения в формулу и проверим это предположение: Стоимость = г * (1 * 1111) + (0.05 * 9745) = 2 + 1111 + 487.25 = -- round() или ceil() ? 1113 + 488 = -- Я выбрал се11() 1601 Значения получены правильно — но это только потому, что я немного сжульничал. Обычно в 8i для стоимости используется функция cei 1 (), а в 9i и 10g — функция round(). В этом примере ошибка мала, так что я не очень об этом волнуюсь. (Для общих случаев я мог бы позаботиться о закреплении ошибки; для пограничных случаев я счастлив, когда вычисления на 99 % совпа- дают с моделью, тем более что код ядра содержит множество «корректировок» для специальных случаев.) Запросы, использующие только индексы Что мы должны сделать с базовой формулой, когда запросу вообще не надо об- ращаться к таблице? Мы снова можем начать с неформального рассуждения И проверить, правильно ли работает вычисление. Если нам не надо посещать таблицу, то, возможно, мы просто проигнорируем последний компонент — Часть, которая представляет собой посещение таблицы (btree_cost_03.sql в он- дайн-хранилище кода). Select /*+ index(tl) */ n2 from tl Where nl between 6 and 9 Order by
106 Глава 4. Простой доступ по бинарному дереву nl План выполнения (9.2.0.6 и 10.1.0.4) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=230 Card=2051 Bytes=12306) 1 0 INDEX (RANGE SCAN) OF 'T1_I1' (NON-UNIQUE) (Cost=230 Card=2051 Bytes=12306) Как обычно, мы должны проявить осторожность при вычислении селектив- ности диапазонного предиката по nl. В этом случае она равна (9 - 6) / (24 - - 0) + 2/25 = 3/24 + 2/25 = 0,205. Так что если мы забудем о табличном компо- ненте базовой формулы, получим: Стоимость = 2 + 0.205 * 1111 = 2 + 227.755 = 230 Похоже, наше предположение верно. Эти три селективности Чтобы подвести итог, давайте добавим последнее усовершенствование к наше- му тестовому случаю, чтобы показать, что в общем случае доступа к одной таб- лице по В-дереву будут использоваться три селективности. Пропустите сле- дующий запрос (снова сценарий btree_cost_02.sql в онлайн-хранилище кода) через autotrace: select /*+ index(tl) */ small_vc from tl where nl between 1 and 3 and ind_pad = rpad('x',40) and n2 = 2 and small_vc = lpad(100,10) План выполнения (9.2.0.6 и 10.1.0.4) 0 SELECT STATEMENT Opt1mizer=ALL_ROWS (Cost=264 Card=l Bytes=58) 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=264 Card=l Bytes=58) 2 1 INDEX (RANGE SCAN) OF 'Т1_1Г (NON-UNIQUE) (Cost=184 Card=1633) Этот запрос начинается, как и пример из раздела «Еще о диапазонных про- верках», но в нем добавляется еще один предикат: small_vc = lpad(100,10). Особо обратите внимание на то, что стоимость не изменилась, но кардиналь- ность уменьшилась с 82 до 1 (Card = 1 в первой и второй строках). Оптимиза- тор вычислил, что запрос вернет только одну запись. Во время выполнения происходит следующее. О Проверяются все листовые блоки индекса для диапазона nl between 1 and 3 (эффективная селективность индекса).
Начало 107 q Проверяются все записи таблицы, для которых элемент индекса удовлетво- ряет всем трем предикатам {эффективная селективность таблицы). q Возвращаются записи, которые удовлетворяют всем четырем предикатам. Итак, мы имеем: q Эффективная селективность индекса (nl) = 0,1633333. О Эффективная селективность таблицы (nl, i nd_pad, п2) = 0,163333 х 1 х 0,05. О Окончательная селективность таблицы (nl, ind_pad, n2, small_vc) = » 0,163333 X 1 X 0,05 X 0,0001. Мы получим следующие кардинальности (которые всегда представляют со- бой некоторую вариацию селективность х количество записей на входе)'. Строка индексного доступа = round(0.163333 * user_tables.num_rows (10,000)) = 1,633 Жточник Rowid (не является частью плана) = round(0.0081667 * user_tables.num_rows (10,000)) = 82 будет получено из таблицы = found(0.0000081667 * user_tables.num_rows (10,000)) = 0! На первый взгляд кажется, что это еще одно место, где моя теория относи- Ядьно того, как 9i и 10g используют функцию round () для получения оконча- тельной кардинальности, похоже, рушится. В данном случае это не очень уди- вительно, и вы найдете несколько других случаев, в которых ответ, технически ^глядящий равным нулю, изменяется на единицу. Интересно, конечно, что 9i и 10g показывают в этом плане 1633 в качестве Кардинальности в строке индекса. В большинстве случаев показываемая карди- нальность представляет собой кардинальность на выходе, а не кардинальность М входе — другими словами, количество записей, которые будут переданы этой строкой плана, а не количество записей, которые будут обработаны этой стро- кой плана. В идеале, конечно, мы бы хотели видеть и 1633, и 82 в строке с ин- дексом в плане, так как оба значения имеют смысл — к сожалению, в плане вы- полнения есть место только для одного значения. Вернемся снова к разделу «Еще о диапазонных проверках». 8i, похоже, сле- дует правилу количество на выходе, когда показывает 82 в строке индекса, где 9i И 10g выводят 1633. В этом примере 8i пошел дальше и показал кардиналь- ность, равную единице, в строке индекса. Подобные небольшие несогласован- йости всегда будут существовать, вызывая бесконечную путаницу, особенно )яри переходе между различными версиями Oracle. Проблемы с 10.1.0.4 ^СЛИ вы тщательно рассмотрите сценарий btree_cost_02.sql, то увидите, что он идаывает копию моего сценария hack_stats.sql, который был установлен для из- ЖНения значения user_indexes.num_rows для критичного индекса. Первона- чально Я сделал это для того, чтобы проверить, использует ли Oracle значение HUiti_rows, хранимое в user_tables, или user_indexes при менее традицион- ном использовании индексов. В 8i и 9i выводимые кардинальности не измени- лись, но в 10.1.0.4 я получил следующие результаты для предыдущего запроса, Изменив значение num_rows в user_indexes на И 000 и 9 900 соответственно:
108 Глава 4. Простой доступ по бинарному дереву План выполнения (10.1.0.4, автотрассировка - user_indexes.num_rows = 11,000) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=264 Card=l Bytes=58) 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (TABLE) (Cost=264 Card=l Bytes=58) 2 1 INDEX (RANGE SCAN) OF 'T1_I1' (INDEX) (Cost=184 Card=1797) План выполнения (10.1.0.4, автотрассировка - user_indexes.num_rows = 9,900) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=93 Card=l Bytes=58) 1 0 TABLE ACCE55 (BY INDEX ROWID) OF 'Tl' (TABLE) (Cost=93 Card=l Bytes=58) 2 1 INDEX (RANGE SCAN) OF 'T1_I1' (INDEX) (Cost=12 Card=82) В этом случае, когда я увеличил значение num_rows с 10 000 до И ООО, кар- динальность в строке с индексом увеличилась с 1633 до 1797 — похоже, опти- мизатор использовал user_indexes.num_rows для вычисления кардинально- сти этой строки (1797 = 1633 х 11 000/10 000). Немного удивительно, но совершенно рационально, и стоимость запроса изменилась незначительно, так что это достаточно безопасный шаг. Но посмотрите, что произошло, когда я искусственно уменьшил значение user_i ndexes. num_rows: стоимость строки с индексом уменьшилась с первона- чальной 184 до 12, и кардинальность уменьшилась с 1633 до 82. Изменение в кардинальности не представляет большой проблемы, так как ошибка сама скорректируется к тому времени, как мы достигнем таблицы, но значительное уменьшение стоимости отзывается на всем плане. В основном вычисления про- сто уменьшили стоимость сканирования листового блока (возможно, за счет простой числовой ошибки: trunc(9,000/10,000) = 0). Конечно, не стоит делать абсолютно точные выводы из взломанной стати- стики, поэтому сценарий btree_cost_02a.sql в онлайн-хранилище кода повторяет последний тест btree_cost_02.sql, но включает дополнительную строку для соз- дания данных: update tl set nl = null, ind_pad = null, n2 = null where rownum <= 100 » Эта строка изменяет данные так, что в таблице существует 100 записей, ко- торые не представлены в индексе. Это фактически не сказывается на значениях стоимости и кардинальности планов, полученных в 8i и 9i, но когда мы исполь- зуем 10g, мы снова сталкиваемся с неприятным сюрпризом — результаты, кото- рые мы получаем при взломе статистики, по-прежнему появляются, когда дан- ные являются оригинальными. Перед вами план выполнения для последнего запроса с немного модифицированными данными — я включил эквивалентный план от 9.2.0.6 для сравнения: План выполнения (10.1.0.4, автотрассировка - 100 неопределенных элементов) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=90 Card=l Bytes=58) 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (TABLE) (Cost=90 Card=l Bytes=58)
Оценка стоимости процессорных ресурсов 109 | 1 INDEX (RANGE SCAN) OF 'Т1_1Г (INDEX) (Cost=ll Card=80) )|лан выполнения (9.2.0.6, автотрассировка - 100 неопределенных элементов) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=260 Card=l Bytes=58) 0 TABLE ACCESS (BY INDEX ROWID) OF ’Tl’ (Cost=260 Card=l Bytes=58) 2 1 INDEX (RANGE SCAN) OF ’T1_I1' (NON-UNIQUE) (Cost=182 Card=1633) Найдя это в файле трассировки 10053, мы обнаружим следующие важные Подробности (обратите внимание, что элемент с названием ix_sel_with_ filters в более ранних версиях Oracle называется tb_sel): |ез неопределенных элементов: ix_sel: 1.6333е-001 ix_sel_with_filters: 8.1667е-003 tjp 100 неопределенными элементами: 1X_sel: 8.0850е-003 ix_sel_with_fliters: 8.0850е-003 Ясно, что проблемы начинаются, когда что-то приводит к копированию fx_.sel_wi th_f1 Iters в ix_sel, вызывающему потерю стоимости доступа К Листовым блокам. Выполнив сценарий btree_cost_02a.sql несколько раз под- ряд, вы обнаружите, что иногда стоимость получается неправильной, а ино- да — при появлении следующей строки в трассировке 10053 — правильной: HX_sel: 1.6333е-001 ix_sel_with_fliters; 8.0041е-003 Основная проблема состоит в том, что вызов dbms_stats. gather_table_ Stats () с параметром cascade, установленным в true, иногда не может обно- вить статистику индекса. Вопреки интуиции, когда dbms_stats работает непра- вильно, в плане выполнения получаются правильные стоимости (так как значе- ние user_i ndexes. num_rows остается таким же, каки user_tables. num_rows), В когда dbms_stats отрабатывает правильно, в плане выполнения получаются неправильные стоимости, потому что корректно записывается: user_i ndexes. num_ rows меньше, чем user_tables. num_rows. В настоящее время я считаю это ошибкой, так что я не проводил дальней- ших экспериментов с этой проблемой — она может быть решена, пока книга бу- дет в печати. Но имейте в виду: при обновлении с 9i до 10g эта ошибка означает, ЧТО некоторые очень простые запросы могут перейти от табличных сканирова- ний к индексному доступу, так как стоимость индексного пути доступа неожи- данно становится нереально низкой и, похоже, это может происходить более ИЛИ менее случайно. Оценка стоимости процессорных ресурсов При использовании 9i вы можете включить системную статистику, что приво- дит к отличиям в планах выполнения. В главе 2 мы рассмотрели, как работает Новый механизм оценки стоимости, но теперь можем рассмотреть, как повлияет ЭДа возможность даже на очень простые планы выполнения, использующие ин- дексы. Так как рассматриваются индексы, арифметика в основном не изменяется. Как мы видели в предыдущих главах, критичными особенностями оценки стои- мости процессорных ресурсов являются: временной компонент для многоблоч-
110 Глава 4. Простой доступ по бинарному дереву ных операций ввода-вывода и загрузка процессора. Но обычный индексный доступ выполняется с помощью одноблочных, операций ввода-вывода, так что временной компонент не имеет отношения к делу, и использование процессора при проходе по индексу для поиска и получения нескольких записей из табли- цы обычно относительно мало. Наибольшее отличие, которое появляется при включении оценки стоимости процессорных ресурсов, состоит в том, что табличные сканирования неожидан- но становятся более дорогими. Я продемонстрирую это на примере. Для начала перед вами код для создания некоторых системных статистических данных (этот код был специально разработан для 9i, но он также верен и для 10g, в ко- тором появилось несколько других статистик): alter session set "_optimizer_cost_model" = cpu; begi n dbms_stats.set_system_stats('CPUSPEED',350); -- MHz dbms_stats.set_system_stats('MREADTIM’,20); -- millisec dbms_stats.set_system_stats('MBRC',8); dbms_stats.set_system_stats('SREADTIM',10): -- millisec end: / В этом фрагменте кода мы сообщаем оптимизатору, что скорость нашего процессора 350 МГц (или, возможно, 350 млн операций Oracle в секунду), что для выполнения запроса многоблочного ввода-вывода понадобится 20 мс и что обычно мы выбираем 8 блоков, тогда как одноблочная операция ввода-вывода занимает 10 мс. Преднамеренная настройка модели оценки стоимости на использование оценки стоимости процессорных ресурсов выполняется для достижения одина- кового эффекта в 9i и 10g. Значение по умолчанию для этого параметра — choose, и 9i выберет использование оценки стоимости процессорных ресурсов только если существует системная статистика, но 10g будет всегда выбирать ис- пользование оценки стоимости процессорных ресурсов, а затем синтезировать некоторые статистические данные, если они не существуют (и я не могу заста- вить delete_system_stats удалить системную статистику в 10g). Если мы выполним следующий запрос по нашему базовому набору данных, то сможем увидеть эффекты оценки стоимости процессорных ресурсов (см. сценарий btree_cost_05.sql в онлайн-хранилище кода): select small_vc from tl where nl = 2 and ind_pad = rpad('x',40) and n2 in (5,6,7) В моем тестовом сценарии я выполнял запрос с подсказкой index() hint, с подсказкой full() и без подсказок. Поведение стоимости показано в табл. 4.2.
$уенка стоимости процессорных ресурсов 111 “Шблица 4.2. Эффекты системной статистики Тестовый сценарий Первоначальная стоимость Стоимость с включенной системной статистикой 9i (log) Подсказкой index() 68 69 (68) Подсказкой full() 58 96 (95) (|уть доступа без подсказок Полное сканирование таблицы Индексный В отсутствие системной статистики оптимизатор оценивал, что при исполь- зовании индексного пути надо будет отдельно посетить 68 блоков, что соответ- ствует 68 запросам ввода-вывода, в сравнении с 1 + ( 371/6,59) для индексного сканирования. (Помните, что в случае табличных сканирований и полных бы- стрых индексных сканирований при игнорировании табличных пространств 4SSM оптимизатор делит значение максимального уровня заполнения на скор- ректированное значение db_file_multiblock_read_count). При включении системной статистики оптимизатор решает, что стоимость индексного пути все еще будет равна 68 для компонента ввода-вывода плюс не- большая дополнительная стоимость использования процессора для получения блоков и выполнения нескольких сравнений. СИСТЕМНАЯ СТАТИСТИКА И OPHMIZER_INDEX_COST_ADJ Одной из наиболее важных особенностей системной статистики (или оценки стоимости процессор- ных ресурсов) является то, что она позволяет оптимизатору находить правильный баланс между стоимостями одноблочных и многоблочных чтений, масштабируя стоимость многоблочного чтения. В качестве раннего исправления существовавшего дисбаланса в 8i появился параметр optimizer index_cost_adj. в действительности этот параметр, похоже, означает процент, который использует- ся для уменьшения стоимости одноблочных чтений по сравнению с многоблочными чтениями. (См. статью Тима Гормана (Tim Gorman) «The Search for Intelligent Life in the Cost Based Optimizer» на www.evdbt.com, это первая полезная статья о данном параметре.) Недостаток этого исправления в том, что параметр optimizerJndex_cost_adj уменьшает стоимость одноблочных чтений вместо увеличения стоимости многоблочных чтений. К несомненным плюсам Нужно отнести сокращение чрезмерного использования табличных сканирований оптимизатором. Однако округление, встроенное в вычисления, могло привести к переключению оптимизатора с хо- рошего индекса на плохой, потому что их стоимости становились одинаковыми после корректиров- ки и округления. При масштабировании вниз ошибки округления становятся более значимыми. Стоимость пути с использованием табличного сканирования изменяется бо- лее существенно, когда мы переключаемся на оценку стоимости процессорных ресурсов. Чтобы получить компонент стоимости для операций ввода-вывода, Мы делим значение максимального уровня заполнения на фактическое храни- мое значение MBRC и округляем: ceiUng(371/8) = 47. Далее мы удваиваем это значение, потому что значение mreadtimB два раза больше sreadtim, полу- чаем 94. Затем добавляем немного для стоимости процессорных ресурсов по Исследованию каждой записи в таблице. Результат состоит в том, что если ваша системная статистика в действительно- сти отражает типичное поведение вашей системы, то стоимостный оптимизатор мо- жет получить стоимость, которая более точно отражает фактическое использование
112 Глава 4. Простой доступ по бинарному дереву ресурсов, и время, необходимое для выполнения запроса, до того как запрос бу- дет выполнен. Но, как показано в последней строке таблицы, обновление (или включение системной статистики через некоторое время после обновления) может привес- ти к тому, что множество планов выполнения спонтанно изменятся, весьма ве- роятно также переключение с табличного сканирования на индексный путь доступа. В теории любые изменения должны привести к улучшению путей до- ступа, но я не гарантирую, что это будет так в каждом случае. Разное Начав экспериментировать с индексами, вы можете обнаружить, что значения не всегда работают корректно. Оптимизатор может использовать различные корректировки в различных обстоятельствах, которые способны привести к тому, что к результатам будет добавляться (или от них будет отниматься) единица. Мы уже обнаружили, что существуют отличия между разными версиями Oracle (а иногда и внутри одной версии), связанные с использованием round() и ceil(). Я потратил много времени на то, чтобы это обнаружить, потому что все мои тесты генерировали результаты, на которых round() и celI() давали одинаковые значения. Вывод: вы никогда не должны считать, что точно опреде- лили, что происходит; хорошее приближение близко настолько, насколько вы можете его получить. Но когда вы считаете, что достаточно близки к результату, вы обнаруживае- те, что на самом деле формулы нет — есть дерево выбора с разными формулами в конце каждой ветви. Вот несколько общих случаев, в которых могут появить- ся расхождения между базовой формулой и фактическим результатом. О Для уникальных индексов или неуникальных индексов, созданных для под- держки ограничений уникальности или ограничений первичных ключей, оп- тимизатор использует стандартную формулу и отнимает единицу. Однако в случае неуникального индекса для поддержки уникального ограничения или первичного ключа эта корректировка не выполняется, если ограничение отложенное (deferrable). Так что будьте осторожны: вы можете соблазниться сделать все ограничения отложенными просто на всякий случай, но существу- ют небольшие побочные эффекты, которые могут привести к изменению пла- нов выполнения. О Индексы с blevel, установленным в единицу (индекс идет напрямую от корневого блока к листовым блокам). Оптимизатор фактически игнорирует blevel, если каждый столбец в индексе участвует в предикате равенства. Это интересный случай, так как расщепление корневого блока (что может произойти при изменении всего одной записи в таблице) увеличит стоимость индекса на два — что может изменить путь доступа. Это лишь один из мно- гих случаев, когда маленькие таблицы могут быть более важны, чем боль- шие, при решении проблем с оптимизатором.
Тестовые сценарии ИЗ ИНДЕКСЫ ПО МАЛЕНЬКИМ ТАБЛИЦАМ Стратегия обработки индексов и статистики по маленьким таблицами может быть очень важна, по- 10МУ что маленькие индексы (очевидно) должны регулярно перестраиваться. Если они находятся на границе blevel, равной 1, и blevel, равной 2, и вы продолжаете собирать статистику, одна дополни- тельная Запись может вызывать переход с blevel 1 на blevel 2. Это незначительное изменение в раз- мере данных или в количестве блоков индекса, но оно вызывает резкий скачок в вычислении сгои- мости. ©рли у вас есть несколько таблиц в таком необычном состоянии, одним из вариантов является оста- новка сбора статистики, другой вариант — вручную установить значение blevel после сбора стати- Йгики, и третий вариант — перестраивать индекс каждый раз до выполнения сбора статистики. включение эта глава была посвящена только тому, как оптимизатор выполняет вы- числение использования простого В-дерева, мы охватили много вопросов. Клю- чевые моменты, которые надо помнить, следующие. О Обычная стоимость использования В-дерева состоит из трех компонентов: глубина индекса, основанная на blevel; количество листовых блоков индек- са, которые надо посетить, основанное на leaf_blocks; и количество посе- щений таблицы, основанное на clusteri ng_factor. О Фактор кластеризации индекса (clusteri ng_f actor) — это основной инди- катор того, насколько желательным индекс является для оптимизатора. Не- достаток этого подхода в том, что вы должны сравнивать clustering_f ac- tor с количеством записей и блоков таблицы — значением, которое само по себе фактически бессмысленно. О Если у вас есть столбцы в индексе, которые часто опускаются в выражении where, или у вас есть диапазонные проверки, применяемые к ним, обычно их надо помещать в конец определения индекса; иначе может случиться так, что ваш запрос будет выполнять много работы с листовыми блоками индек- са. Вычисления стоимости оптимизатора отразят этот факт, что может при- вести к игнорированию индекса в пользу альтернативных путей. О Системная статистика должна быть включена при переходе на 9i (или 10g), к вы должны знать, как это скажется на планах выполнения. Истовые сценарии Файлы к этой главе, доступные для загрузки, перечислены в табл. 4.3. 4,3. Тестовые сценарии к главе 4 Щйнарий Комментарии ltM.cost_01.sql Основной сценарий, используемый в этой главе fgbmidjest.sql Показывает, что перестроение индекса может вызвать проблему Wec.cost_02.sql Примеры вычисления стоимости для диапазонных сканирований и частичного использования индекса , л -—— ---------------------------------------------------продолжение &
114 Глава 4. Простой доступ по бинарному дереву Таблица 4.3 (продолжение) Сценарий Комментарии btree_cost_04.sql btree_cost_03.sql hack_stats.sql btree_cost_02a.sql Примеры ошибок co списками значений в 8i Полные индексные сканирований, сканирования только индексов Сценарий для модификации статистики напрямую в словаре данных Повторяет пример btree_cost_02.sql, но добавляются неопределенные элементы btree_cost_05.sql setenv.sql Пример влияния оценки стоимости процессорных ресурсов Устанавливает стандартную среду для SQL*Plus
5 Фактор кластеризации 1 предыдущей главе я предупредил вас о том, что clusteri ng_factor пред- ъявляет собой важную часть стоимости использования В-дерева для диапазон- )фго сканирования и легко может стать наиболее частой причиной ошибок при вычислении стоимости. Теперь настал момент, чтобы определить, почему что-то Может пойти не так. Эта глава очень важна, так как в ней описывается множе- ство разумных стратегий, которые администраторы баз данных применяют для увеличения производительности или для снижения конкуренции за ресурсы, обнаруживая побочные эффекты, из-за которых оптимизатор может игнориро- вать индексы, которые следует использовать. Почти неизменно разумная стра- тегия приводит к проблемам в случае некоторых запросов из-за ее влияния на Cluster!ng_factor. Фактор кластеризации (clusteri ng_factor) представляет значение, кото- рое показывает степень случайности распределения данных в таблице, и это хо- рошая мысль — создать значение, представляющее распределение данных в таб- лице. К сожалению, некоторые новые и не такие уж новые возможности Oracle Могут сделать это магическое значение недостоверным. В следующих обсуждениях мы сфокусируем свое внимание на традицион- ных таблицах, организованных в виде кучи (heaporganized), и вы увидите, что Индексы в моих примерах построены по временным или последовательным данным. Вавовый пример Чтобы увидеть, как плохо может все оказаться, мы начнем с тестового сцена- рия, подобного сценарию из реальной жизни, и посмотрим, на что похож clus- <Шг1 ng_f actor, когда все хорошо. Начнем с таблицы с первичным ключом из двух частей: первая часть будет Датой, а вторая — последовательным числом. Затем запустим пять параллельных Процессов для выполнения процедуры, эмулирующей активность пользователей.
116 Глава 5. Фактор кластеризации Эта процедура вставляет данные для 26 дней по 200 записей на день, но чтобы ускорить эксперимент, работа всего дня загружается в течение 2 с. Общий объем данных будет равен: 5 процессов х 26 дней х 200 записей на день = 26 000 записей. На достаточно современном оборудовании вы обнаружи- те, что данные загружаются менее чем за минуту. Как обычно, в моем демонстрационном окружении используются блоки раз- мером 8 Кбайт, локально управляемые табличные пространства с однородными экстентами размером 1 Мбайт и ручное управление свободным пространством сегментов, системная статистика (CPU costing) отключена (см. сценарий base_ Line.sqL в онлайн-хранилище кода). create table tl( date_ord date constraint tl_dto_nn not null, seq_ord number(6) constraint tl_sqo_nn not null, small vc varchar2(10) ) pctfree 90 pctused 10 create sequence tl_seq create or replace procedure tl_load(i_tag varchar2) as m_date date; begin for i in 0..2S loop -- 26 дней m_date : = trunc(sysdate) + 1; for j in 1..200 loop -- 200 записей на день insert into tl values( m_date, tl_seq.nextval, i_tag l| j -- используется для идентификации сеансов ): commit; dbms_lock.sleep(0.01); -- для уменьшения конкуренции end loop; end loop; end; I rem rem Теперь настройте сеансы для выполнения нескольких копий этой процедуры rem для заполнения таблицы rem Обратите внимание на необычные значения pctused и pctfree для табли- цы; эти значения используются для генерации достаточно большой таблицы без необходимости генерировать большое количество данных. Для выполнения этого теста необходимо создать последовательность (se- quence), таблицу и процедуру, а затем запустить пять различных сеансов для одновременного выполнения этой процедуры. В приведенном сценарии про- цедура также использует пакет dbms_lock для синхронизации времени парал- лельного запуска своих копий, но для сокращения кода примера в этот текст я не вставил дополнительные строки. Когда пять параллельно выполняющихся
117 |$й0Вый пример w+l'l1'1' .. ——— Процедур закончат выполняться, вам надо создать подходящий индекс, а затем. сгенерировать и проверить соответствующие статистические данные. Следую- щие результаты получены с использованием версии 9i: create index tl_i1 on tl(date_ord, seq_ord); liegin dbms_stats.gather_table_stats( user, tl', cascade => true, estimate_percent => null, method_opt => 'for all columns size 1' ); дай; «elect blocks, num__rows from user_tables table_name = 'Tl' BLOCKS NUM_ROWS 749 26000 «elect index_name, blevel, leaf_blocks, clustering_factor from user_indexes Where table_name = 'Tl' INOEX_NAME BLEVEL LEAF_BLOCKS CLUSTERING_FACTOR — w w w w w w w w w w WWW— ww— wwww “•“•“•“•*W__^WWWWWWW— w TL.I1 1 86 1008 Обратите внимание, что в этом случае значение clusteri ng_f ас tor очень близко к количеству блоков в таблице и намного меньше количества записей Ш таблице. Вы можете обнаружить, что при повторных запусках теста с lus- ter ing_f ас tor изменяется на 1 % или 2 %, но его значение, скорее всего, оста- ЖТСЯ где-то рядом с отметкой 750 или 1000, в зависимости от того, используете Ж Вы машину с одним процессором или с несколькими. Это выглядит так, как будто используется хороший индекс, так что давайте протестируем его с ис- Вбльзованием несколько недружелюбного (но совершенно обычного) запроса, Который выбирает все данные для заданной даты. Stt autotrace traceonly explain «Cleet count(small_vc)
118 Глава 5. Фактор кластеризации from tl where date_ord = trunc(sysdate) + 7 set autotrace off План выполнения (9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=44 Card=l Bytes=13) 1 0 SORT (AGGREGATE) 2 1 TABLE ACCESS (BY INDEX ROWID) OF ’Tl' (Cost=44 Card=1000 Bytes=13000) 3 2 INDEX (RANGE SCAN) OF 'T1_I1' (NON-UNIQUE) (Cost=5 Card=1000) Обратите внимание, что наш запрос использует только один столбец индек- са из двух столбцов. Применяя формулу В. Брейтлинта, которую я показал в главе 4, мы можем увидеть, что получаются следующие значения: стоимость = blevel + ceil(эффективная селективность индекса * leaf_blocks) + ceil(эффективная селективность таблицы * clustering_factor) В данном случае мы используем 1 день из 26 — селективность равна 3,846 % или 0,03846, и две селективности идентичны. Подставим эти значения в форму- лу: стоимость = 1 + ceil(0.03846 * 86) + ceil(0.03846 * 1,008) = 1 + 4 + 39 = 44 Как известно, с помощью clustering_factor Oracle может определить, что все записи для заданной даты были достигнуты в одно и то же время и что они заполняют небольшое количество смежных блоков. Индекс использует- ся даже при том, что Oracle должен выбрать 1000 строк, или около 4 % дан- ных. Это хорошо, так как наша простая модель возможно представляет собой разумное представление многих систем, в которых используется ежедневная активность. Уменьшение конкуренции при доступе к таблице (несколько списков свободных блоков) Но в нашем примере могут быть проблемы. В системах с высоким уровнем па- раллельного доступа мы можем испытывать трудности из-за высокой конку- ренции за ресурсы. Взгляните на несколько первых записей в тестовых данных, которые мы только что получили. Вы, вероятно, увидите нечто подобное: select /*+ full(tl) */ rowid, date_ord, seq_ord, small_vc
уменьшение конкуренции при доступе к таблице 119 from tl Where rownum <= 10 DATE_ORD SEQ_ORD SMALL_VC AAAMJHAAJAAAAAKAAA 18-FEB-04 1 Al AAAMJHAAJAAAAAKAAB 18-FEB-04 2 Bl aaamjhaajaaaaakaac 18-FEB-04 3 Cl aaamjhaajaaaaakaad 18-FEB-04 4 A2 aaamjhaajaaaaakaae 18-FEB-04 5 DI AAAMJHAAJAAAAAKAAF 18-FEB-04 6 El AAAMJ haaj AAAAAKAAG 18-FEB-04 7 B2 AAAMJHAAJAAAAAKAAH 18-FEB-04 8 D2 AAAMJHAAJ AAAAAKAAI 18-FEB-04 9 B3 AAAMJ haaj aaaaakaaJ 18-FEB-04 10 E2 Вспомните, что расширенный идентификатор записи (extended rowid) состо- ит из следующих элементов. О Идентификатор объекта (object_id): первые шесть букв (AAAMJH). Ь Относительный идентификатор файла (file_id): следующие три буквы (АА J ). О Блок внутри файла: следующие шесть букв (АААААК). О Запись внутри блока: последние три буквы (AAA, ААВ, ААС...). Все эти записи находятся в одном и том же блоке (АААААК). Когда я выпол- нял тест, я заполнил столбец small_vc признаком, который может использо- ваться для идентификации процесса, вставившего запись. Все наши пять про- цессов были заняты, обращаясь к одному и тому же блоку таблицы в одно и то же время. В сильно загруженных системах (в частности, с высоким уровнем од- новременного доступа) мы можем увидеть множество ожиданий (buffer busy Waits) для блоков класса «блок данных» (data block), в которые выполнялись Все вставки. Как решить эту проблему? Это просто: мы прочитаем рекомендацию в «Ora- cle Performance Tuning Guide and Reference» и (особенно для старых версий Oracle) поймем, что мы должны создать таблицу с несколькими списками сво- бодных блоков (freelists'). В данном случае, так как мы предполагаем уровень од- новременного доступа равным 5, мы можем использовать этот предел и создать Таблицу с дополнительным параметром — storage (freelists 5). ', При использовании этого параметра Oracle будет поддерживать пять связ- 'ЦЫХ списков свободных блоков, связанных с заголовочным блоком сегмента щблицы (segment header block), и когда процессу понадобится вставить запись, рй использует свой идентификатор (process ID), чтобы определить, к какому из списков он должен обратиться для нахождения свободного блока. Это означает, что нам немного повезло и наши пять параллельных процессов никогда не Станут конфликтовать друг с другом; они всегда будут использовать пять раз- ных блоков таблицы для вставки своих записей.
120 Глава 5. Фактор кластеризации УПРАВЛЕНИЕ СПИСКАМИ СВОБОДНЫХ БЛОКОВ Полный цикл действий, связанных с управлением списками свободных блоков, выходит за рамки этой книги, но следующие моменты достаточно важны для простейших случаев. По умолчанию таблица создается с одним сегментом списка свободных блоков (segment freelist), Oracle увеличивает отметку максимального уровня заполнения на пять блоков и добавляет эти бло- ки в список свободных блоков каждый раз, когда список становится пустым. В общем случае только один верхний блок списка свободных блоков доступен для вставок в обычных таблицах-кучах. Если вы определите несколько списков свободных блоков, Oracle выделит на один сегмент списка больше, чем вы предполагаете, и использует его в качестве главного списка свободных блоков (master freelist). Этот главный список используется как точка фокусировки для того, чтобы гаранти- ровать, что остальные списки ведут себя разумно и остаются примерно одной длины (которая обыч- но лежит где-то между нулем и пятью блоками). Ранее мы могли определять параметр freelists только при создании таблицы, но это поведение изме- нилось где-то в линейке 81 (возможно в 8.1.6), так что изменить значение для будущих распределе- ний места можно с помощью простой и дешевой команды alter table. Повторите базовый тест с параметром freelists, равным 5, и вы обнаружи- те, что цена уменьшения конкуренции может быть очень высока. Давайте по- смотрим, что произошло с clustering_factor и желательностью индекса, когда я первый раз выполнил это изменение (сценарий free_lists.sql в онлайн-храни- лище кода): INDEX_NAME BLEVEL LEAF_BLOCKS CLUSTERING_FACTOR T1_I1 1 86 26000 select count(small_vc) from tl where date_ord = trunc(sysdate) + 7 План выполнения (9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=115 Card=l Bytes=13) 1 0 SORT (AGGREGATE) 2 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=115 Card=1000 Bytes=13000) Clustering_factor изменился с примерно 1000 (что близко к количеству блоков таблицы) до 26 000 (количество записей в таблице), так что оптимиза- тор считает, что это на самом деле ужасный индекс, и отказывается использовать его для нашего запроса с одной датой. Самое печальное здесь то, что данные для этой одной даты по-прежнему находятся в тех же самых блоках таблицы с 30 по 35, где они были, когда значение freelists было равно 1, изменился только порядок записей. В главе 4 я создал схему с таблицей и индексом, показывающую условный механизм, который Oracle использует для подсчета clustering_fасtor. Если создать подобную диаграмму для упрощенной версии нашего предыдущего примера, используя freelists, равный 2, считая, что Oracle добавляет только два блока за один раз в список свободных блоков, то схематическое представле- ние будет больше похоже на приведенное на рис. 5.1.
|даньшение конкуренции при доступе к таблице 121 ID столбца Рис. 5.1. Clustering_factor и несколько списков свободных блоков Процесс 1 занят вставкой записей в блок 1, но процесс 2 использует другой деюок свободных блоков, так что он занят вставкой записей в блок 3 (в реаль- ной системе он, скорее всего, вставлял бы записи в позицию на пять блоков ШМе в таблице). Но оба процесса используют один и тот же генератор последо- вательностей, и если обоим процессам случится идти точно в ногу, различные дачения из последовательности окажутся в различных блоках. Следовательно, по того как Oracle проходит индекс, он выполняет шаг назад и вперед между од- Шй и той же парой блоков таблицы, увеличивая на единицу clustering- factor по мере прохождения. Счетчик I, показанный для clustering-factor йа первой схеме, здесь не нужен, потому что его значения совпадают со значе- Нцями столбца ID. Если вы посмотрите на диаграмму, станет очевидно, что для Получения всех значений от 1 до 10 необходимо только два чтения блоков, но ме- •щд Oracle для вычисления clustering-factor заставляет оптимизатор думать, Ш ему придется посетить 10 различных блоков. Проблема в том, что Oracle не запоминает недавнюю историю при вычислении clustering-factor, он просто Проверяет, является ли текущий блок тем же самым, что и предыдущий. ВЙВОР СПИСКА СВОБОДНЫХ БЛОКОВ КОГда процессу необходимо вставить запись в таблицу с несколькими списками свободных блоков, выбирает свободный блок на основании идентификатора процесса. (Статья 1029850.6 на MetaLink показывает, что алгоритм таков: mod(process_id, freelist count) + 1.) Это означает, что кол- ИИВии между процессами все еще могут происходить независимо от определенного вами количе- ства списков свободных блоков. В моем тесте мне посчастливилось получить пять процессов, кото- Выбрали различные списки свободных блоков. Ваши результаты могут значительно отличаться. ЙЙЖет немного удивлять, что выбор списка основывается на идентификаторе процесса, а не на ЙШификаторе сеанса, но логическим объяснением этого выбора может быть минимизация конку- ренции в среде с разделяемым сервером (MTS). ----------------------------------------------------------------------- Когда вы решите проблему с конкуренцией за таблицу, вы можете обнару- жить, что запросы, которые должны использовать диапазонные сканирования по Атдексам, неожиданно начинают использовать табличные сканирования из-за Йростых арифметических проблем. Вскоре мы увидим интересный метод реше- йИя этой проблемы.
122 Глава 5. Фактор кластеризации Уменьшение конкуренции при доступе к листовым блокам (индексы по инвертированному ключу) Перед тем как рассматривать решения проблемы с неправильным значением cluster1ng_factor, давайте рассмотрим несколько других возможностей, ко- торые могут привести к такому же эффекту. Первая возможность — это индекс по инвертированному ключу (reverse key index), который появился в Oracle 8.1 в качестве механизма для уменьшения конкуренции (особенно в RAC-систе- мах) за ведущую часть индексов, основанных на последовательностях. Индекс по инвертированному ключу инвертирует порядок байтов каждого столбца индекса, перед тем как вставлять результирующее значение в структу- ру индекса. Эффект состоит в превращении последовательных значений в эле- менты индекса, которые распределены случайно. Рассмотрим, например, сле- дующее значение: (date_ord, seq_no) = ('18-Feb-2004', 39); мы можем использовать функцию dump(), чтобы увидеть, что внутренне это значение бу- дет представлено как ({78,68,2,12,1,1,1} , { cl, 28}): select dump(date_ord,16) date_dump, dump(seq_no,16) seq_dump from tl where date_ord = to_date('18-feb-2004') and seq_no = 39 DATE_DUMP SEQJWMP Typ=12 Len=7: 78,68,2,12,1,1,1 Typ=2 Len=2: cl,28 но после инверсии оно становится равным ({1,1,1,12,2,68,78}, {28,cl}): select dump(reverse(date_ord),16) date_dump, dump(reverse(seq_no),16) seq_dump from tl where date_ord = to_date('18-feb-2004') and seq_no = 39 DATE_DUMP SEQJWMP Typ=12 Len=7: 1,1,1,12,2,68,78 Typ=2 Len=2: 28,cl Обратите внимание, что эти два столбца инвертируются отдельно, то есть в нашем примере все элементы для 18 February 2004 все еще будут находиться рядом "в нашем индексе, но нечто странное произойдет с последовательностью числовой части внутри этой даты. Если мы выведем часть индекса вокруг зна- чения (' 18-Feb-2004' , 39) с помощью команды alteг system dump datafile, мы найдем следующие десять значений в качестве последовательных элементов для seq_ord (сценарий reverse.sql в онлайн-хранилище кода создаст такой же
уменьшение конкуренции при доступе к листовым блокам 123 результат намного более удобным способом, так что вы можете увидеть, как да- леко от 39 находятся значения 38 и 40): ^VERSED_SEQ_ORD SEQ_ORD 4^*““-------------- ---------- ------------- ^8,7,с2 639 |Л,8,с2 739 28.9.С2 839 3S.a,c2 939 39 Ш.2.С2 140 j|,3,c2 240 |9,4,с2 340 >,5,с2 440 29.6.С2 540 Что в результате произошло с clustering_factor и планом выполнения? Вернемся к таблице, которую мы использовали в базовом тесте (используя зна- чение по умолчанию для freelists, равное единице), и перестроим индекс как Индекс по инвертированному ключу (сценарий reversed_ind.sql в онлайн-храни- даце кода): alter index tl_i1 rebuild reverse, begin dbms_stats gather_table_stats( user, 'tl', cascade => true, estimate_percent => null, method_opt => 'for all columns size 1' ), end, / INDEX_NAME BLEVEL LEAF_BLOCKS CLUSTERING_FACTOR 1Ц1 1 86 25962 select count(small_vc) from tl Where date_ord = trunc(sysdate) + 7 План выполнения (9206, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=115 Card=l Bytes=13) I D SORT (AGGREGATE) 2 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=115 Card=1000 Bytes=13000) Цель инвертирования индекса состоит в разбросе элементов индекса, когда Элементы таблицы содержат последовательные значения, но еще одним его по- следствием является то, что смежные элементы индекса соответствуют разбро- ёЙМНЫм элементам таблицы; другими словами, cluster!ng_factor становится Чрезвычайно большим. 0 Так как теперь cluster! ng_f actor близок к количеству записей в нашем тес- овом примере, план выполнения изменился со сканирования диапазона индекса
124 Глава 5. Фактор кластеризации на сканирование таблицы, даже при том, что записи, которые нам необходимы, все еще находятся в том же самом небольшом кластере блоков таблицы. Распределение данных не изменилось, но изменилось то, как Oracle воспри- нимает его, так как механизм вычисления clusteri ng_f ас to г не учитывает влияние индексов по инвертированному ключу. ИНДЕКСЫ ПО ИНВЕРТИРОВАННОМУ КЛЮЧУ И СКАНИРОВАНИЕ ДИАПАЗОНОВ Вы могли слышать, что индексы по инвертированному ключу не могут быть использованы для диа- пазонных сканирований (несмотря на пример, приведенный в тексе). Это справедливо только в спе- циальном случае уникального индекса из одного столбца (то есть в самом простом, но, возможно, наиболее общем случае, когда они могли бы использоваться). Для индекса из нескольких столбцов, например {order_date, order_number}, Oracle может использо- вать диапазонное сканирование для запроса с проверкой на равенство по ведущим столбцам индек- са. Аналогично предикат равенства по неуникальному индексу из одного столбца — возможно, ме- нее вероятный кандидат для инвертирования индекса — приведет к сканированию диапазона. Очень важно аккуратно обращаться со словами — это общее заблуждение, связанное с индексами по инвертированному ключу и диапазонными сканированиями, появляется из-за того, что операция диапазонного сканирования не должна быть результатом диапазонного предиката. Уменьшение конкуренции при доступе к таблице (ASSM) Существует еще одна новая возможность, которая, несомненно, может свести на нет эффективность индекса. И снова эта возможность направлена на умень- шение конкуренции за счет разброса данных, а попытка решить одну проблему с производительностью может привести к новой. В большинстве тестовых сценариев в этой книге я создаю мои данные в таблич- ных пространствах, которые используют традиционный вариант управления про- странством с использованием списков свободных блоков (freelist space management). Основная цель, ради которой я использую управление пространством со спи- сками свободных блоков, состоит в том, чтобы гарантировать достаточную вос- производимость тестов. Но для следующего теста требуется табличное про- странство, которое использует автоматическое управление свободным пространством сегментов (более известное как автоматическое управление пространством сегментов — automatic segment space management, ASSM). Например: create tablespace test_8k_assm blocksize 8K datafile 'd:\oracle\oradata\d9204\test_8k_assm.dbf' size 50m reuse extent management local uniform size IM segment space management auto Oracle представил эту новую стратегию управления пространством сегмен- тов для решения проблем с конкурентным доступом к блокам таблицы во вре- мя вставок, особенно в RAC-средах. Существует две ключевые особенности, связанные с ASSM.
125 Уменьшение конкуренции при доступе к таблице (ASSM) ---------------------------------'----------------- Первая особенность структурная: каждый сегмент в табличном простран- стве ASSM использует несколько блоков в начале каждого экстента (обычно ^ин или два для каждых 64 блоков в экстенте) для карты всех остальных бло- ^Ьв в экстенте с грубой индикацией (с точностью до ближайшей четверти блока) Jjoto, сколько свободного пространства доступно в каждом блоке. УИМЕЧАНИЕ в® описание блоков ASSM не дает полной картины происходящего, так как детали могут варьиро- из-за размера блока, общего размера сегмента и смежности экстентов. Вы можете обнару- Йить в большом объекте с несколькими смежными небольшими экстентами, что единственный блок ^годном экстенте содержит карту для следующих двух или трех экстентов, вплоть до 256 блоков. Вы обнаружите, что первый экстент сегмента представляет собой специальный случай: в табличном ||©странстве ASSM с размером блока 8 Кбайт блок заголовка сегмента — четвертый блок в сег- |ЙйТе1 ’Ййй® при рассмотрении карты пространства значение выражения «свободный» немного расплыв- чато, Когда битовая карта показывает пространство как свободное с элементами наподобие Шг.75-100 % свободно», этот диапазон представляет собой процент от блока, но если вы установи- Зй очень низкое значение PCTFREE для объекта, вы можете обнаружить, что разница между Й5-100 % свободно» и «полностью заполнено» — всего несколько байтов. (И Oracle, похоже, не- кого медлит с изменением статуса «полностью заполнено», когда вы удаляете записи.) Вторая особенность ASSM проявляется во время выполнения: когда процес- су надо вставить запись, он выбирает блок карты пространства, который дикту- « его идентификатор процесса, а затем выбирает из карты пространства блок данных, который снова диктует его идентификатор процесса. Суммарный эф- фект ASSM состоит в том, что параллельные процессы будут иметь тенденцию щдбирать различные блоки для вставки своих записей, а это уменьшает конку- ренцию между процессами без вмешательства администратора базы данных. Самая важная фраза в последнем предложении — «различные блоки». Что- бы избежать конкуренции, различные процессы разбрасывают свои данные по различным блокам — это может заставить вас подумать, что с clus- tWing_factor случилось нечто ужасное. Снова выполните базовый тест, но (дадайте табличное пространство, подобное предыдущему, и добавьте следую- щую строку к оператору создания таблицы (сценарий assm_test.sql в онлайн- |р4цилище кода): tablespace test_8k_assm Результаты, которые вы получите из теста на этот раз, могут выглядеть по- Жбно следующим — но также могут значительно отличаться: 1NMX_NAME blevel leaf_blocks clustering_factor ----------------- ---------- ----------- ----------------- ftjl 1 86 20558 Itlect count(small_vc) from tl НЙге date_ord = trunc(sysdate) + 7; ЖйН выполнения (9.2.0.6, автотрассировка) — — — — — — —— —— — — — — — — — — — — — — — — —— — — — — — — — — — — — — — —— — — — — — — — — — — —
126 Глава 5. Фактор кластеризации 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=116 Card=l Bytes=13) 1 0 SORT (AGGREGATE) 2 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=116 Card=1000 Bytes=13000) И снова без каких-либо изменений в коде вставки данных, определении дан- ных или активности конечных пользователей мы ввели некоторую особенность Oracle на инфраструктурном уровне, которая изменила план выполнения с ин- дексного пути доступа на сканирование таблицы. Ваши результаты этого теста могут значительно отличаться от моих. Это особенность ASSM, которая состоит в том, что разброс данных при вставке за- висит от идентификаторов процессов, выполняющих вставку. В нескольких по- следовательных повторах этого теста, после отключения и повторного подклю- чения к Oracle для получения отличных идентификаторов процесса, при одном выполнении теста было получено значение clustering_f actor, равное 22 265, а при следующем — 18 504. Другая информация, полученная из этого теста — случайность вставок и кон- куренция при использовании ASSM. Выполнив следующий код SQL, я могу по- лучить меру того, сколько коллизий было при доступе к блокам таблицы: select ct, count(*) from ( select block, count(*) ct from ( select distinct dbms_rowid.rowid_block_number(rowid) block. substr(small_vc.1.1) from tl ) group by block ) group by ct Вспомните, что я включил некоторый признак в каждый вызов процедуры, и значение этого признака копируется в столбец small_vc. В предыдущем за- просе я выбрал номер блока и значение признака (я обозначил буквами А-Е пять процессов, выполняющих эту процедуру), чтобы определить, сколько бло- ков использовалось для вставки всеми пятью процессами, сколько блоков ис- пользовалось для вставки любыми четырьмя процессами, и так далее. Результа- ты приведены в табл. 5.1. Таблица 5.1. Количество коллизий с ASSM и списками свободных блоков Количество процессов, одновременно обращающихся к блоку Блоки с ASSM, тест 1 Блоки с ASSM, Тест freelists тест 1 5 (а) Тест freelists 5(b) 1 361 393 745 447 2 217 258 297 3 97 37 4 38 33
(даьшение конкуренции при доступе к таблице (ASSM) 127 количество процессов, Повременно обращающихся к блоку Блоки с ASSM, Блоки с ASSM, Тест freelists тест 1 тест 1 5 (а) Тест freelists 5(b) Jj^to блоков 12 6 75 727 745 744 Как вы видите, тесты ASSM (второй и третий столбцы) все еще показывают муительное количество блоков с очевидными коллизиями между параллель- gmffl процессами. Например, в третьем столбце таблицы мы видим 258 блоков, которые использовались двумя процессами вставки данных. Для сравнения я также привел результаты двух тестов, в которых установил $рцение freelists равным 5. В тесте freelists (а) каждый отдельный блок абдицы использовался для вставки только одним процессом. Однако это не га- ^КХИрованный результат — мне просто повезло в этом тесте, так как случи- Е, что каждый из пяти моих процессов выбрал отдельный список свободных :ов. В тесте f reelists (b) два процесса выбрали один и тот же список сво- их блоков, так что во время выполнения они были в состоянии постоян- мй коллизии. Итак, вы видите, что отлично сконфигурированный набор списков свобод- блоков может дать вам минимальную конкуренцию за блоки таблицы при дфаллельных вставках — или полностью наоборот. С другой стороны, некото- |йЙ уровень случайности, вносимый ASSM, означает, что вам будет сложно ко- tga-либо получить полное предотвращение конкуренции, но конкуренция, ко- фрую вы получите, вероятно, будет меньше и будет распределена во времени — факт, что все пять процессов использовали блок X для вставок, не обяза- Шльно означает, что все они пытались использовать его в один и тот же момент. Вы можете отметить, что также присутствует небольшая экономия про- (йфййства: тесты с ASSM использовали примерно на 20 блоков данных меньше, тесты с freelists. Это побочный эффект алгоритма вставки ASSM, кото- умеет быть немного более хитрым и постоянным, чем алгоритм вставки ДЧЙ традиционной обработки со списками свободных блоков. В моем случае, ШМ йе менее, экономия была компенсирована тем фактом, что 12 блоков были ЖДелены для битовых карт первого уровня (2 на экстент), один блок был вы- Ж^ей для битовой карты второго уровня и 16 дополнительных блоков в начале ?|рдады были предварительно отформатированы и частично использованы. «К цто на самом деле ниже отметки максимального уровня заполнения были отформатированных блока. ЯЙЙВЧАНИЕ ||ЙЫЦ1ал о нескольких случаях, когда Oracle становился намного менее постоянным при использо- &ASSM и проверял буквально сотни блоков данных, чтобы найти один с достаточным простран- ДЛя вставки записи, — и не отмечал в карте пространства, что этот блок заполнен, когда дол- ЖИ ЙЫЛ это сделать. Мй вы действительно беспокоитесь о конкуренции при большом количестве параллельных вста- Йж, вы должны исследовать относительные выгоды и стоимости особенностей, которые Oracle пре- доставляет для уменьшения конкуренции. В частности, вы должны быть особенно осторожны, когда существует только несколько процессов, которые делают всю работу.
128 Глава 5. Фактор кластеризации Уменьшение конкуренции в RAC (группы списков свободных блоков) Другим вариантом уменьшения конкуренции для таблиц с высоким уровнем параллельных вставок, особенно для OPS (как это было раньше) и RAC (как это происходит сейчас), была возможность создания таблицы с несколькими группами списков свободных блоков (freelist groups). В руководствах все еще приводятся указания на то, что эта возможность имеет смысл только в слу- чае множественных экземпляров Oracle, но фактически она может использо- ваться и с единственным экземпляром Oracle, где она производит эффект, по- добный множественным спискам свободных блоков. (Из этих двух вариантов список свободных блоков обычно более подходящий и достаточный для един- ственного экземпляра Oracle.) Множественные списки свободных блоков, как мы видели ранее, уменьша- ют конкуренцию, так как каждый список имеет свой собственный верхний блок, так что вставка выполняется в несколько блоков таблицы, а не в один блок. Од- нако все еще существует точка конкуренции, так как список свободных блоков начинается с указателя на его верхний блок и все указатели находятся в заголо- вочном блоке сегмента. При использовании RAC, даже если вы избавились от конкуренции при доступе к блокам таблицы, определив несколько списков сво- бодных’ блоков, вы все еще можете получить «перегретый» заголовочный блок сегмента, перескакивающий между экземплярами. Вы можете указать несколько групп списков свободных блоков как часть оп- ределения таблицы (см. сценарий flg.sql в онлайн-хранилище кода): storage (freelist groups 5) Если вы сделаете это (в не-ASSM табличном пространстве), вы получите один блок на группу списков свободных блоков в начале сегмента, после заго- ловочного блока сегмента. Каждый блок (группа) ассоциируется с идентифика- тором экземпляра, и каждый блок (группа) поддерживает независимый набор списков свободных блоков. Так что конкуренция за заголовок сегмента устра- няется. ПРИМЕЧАНИЕ Один из классов, записываемых в представление v$waitstat, называется free list. Учитывая сущест- вование списков свободных блоков и групп списков свободных блоков, отметим, что это название немного неоднозначно. Фактически этот класс относится к блокам группы свободных списков. Ожи- дания заголовка сегмента списка свободных блоков могут быть одной из причин, когда вы видите ожидания класса segment header. Использование групп свободных блоков может быть очень эффективно, если вы сможете решить, сколько их использовать. Если вы удостоверитесь, что установили это значение достаточно высоким, так что каждый ваш экземпляр (и каждый экземпляр, который может вам понадобиться) имеет свой собствен- ный блок группы списка свободных блоков, то проблемы конкурентного досту- па при вставках, даже с индексированными последовательными столбцами, должны исчезнуть.
и<еньшение конкуренции в RAC (группы списков свободных блоков) 129 Существуют некоторые сценарии, при которых наличие нескольких групп тисков свободных блоков для таблицы автоматически уменьшает конкурен- цию за блоки таблицы в RAC-системах. Более того, конкуренция за листовые |агяси. индекса по столбцам с последовательными значениями может быть уст- Езнабез побочных эффектов с cluster! ng_f ас to г, если вы гарантируете вы- нение двух условий. Во-первых, размер кэша последовательности должен ь достаточно большим, например: grttate sequence big_seq cache 10000: Тйк как каждый экземпляр поддерживает свой собственный «кэш» (на са- деле просто пары значений «нижнее/верхнее» или «текущее/целевое»), «аяченич. вставляемые одним экземпляром, будут значительно отличаться от мнйчений. вставляемых другим экземпляром, и большое числовое отличие, ве- |»ятно, превратится в пару отдаленных листовых блоков. Во-вторых, вы также должны оставить значение freelists для таблицы |»wwbtM единице, чтобы на часть индекса, заполняемую каждым экземпляром, не влиял эффект «сальто», описанный в разделе, посвященном спискам свобод- ных блоков. Однако существует значительный побочный эффект, о котором вы должны Шать. Так как каждый экземпляр фактически заполняет свою собственную, дайкретную часть индекса, каждый экземпляр, исключая тот, который заполня- ет текущую часть индекса с наибольшим значением, будет вызывать расщепле- ния листового блока 50/50, без возможности обратного заполнения. Другими словами, в RAC-системах вы можете принять меры, чтобы избежать конкурен- там за таблицу, конкуренции за индексы по столбцам, основанным на последо- Йтельностях, и избежать повреждения clustering_factor для этих индексов; МО ценой будет увеличение размера индекса (точнее, количества листовых бло- ком), возможно, в два раза по сравнению с тем, когда база данных работает под управлением единственного экземпляра. ШВТОРНАЯ БАЛАНСИРОВКА ГРУПП СПИСКОВ СВОБОДНЫХ БЛОКОВ Хорошо известная проблема с множественными группами списков свободных блоков состоит в том, црйрли вы используете единственный процесс для удаления большого объема данных, все блоки, освобожденные удалением, будут ассоциированы с одной группой списков свободных блоков и не быть автоматически получены для использования списками свободных блоков других групп. ЖМйначает, что процессы, пытающиеся вставить новые данные, не смогут использовать это сво- ЙЙЬе пространство, пока они сами не будут присоединены к «правильной» группе свободных бло- Ж Поэтому вы можете обнаружить, что объект форматирует новые блоки или даже добавляет но- Ж экстенты, когда очевидно, что существует много свободного места. & решения этой проблемы существует процедура с неоднозначным названием dbms_repair. |djfeelists(), которая перераспределяет свободные блоки равномерно по всем группам списков 1$9бодных блоков объекта. К несчастью, в ее коде допущена ошибка, которая делает распределе- ний Неравномерным, если только идентификатор процесса, выполняющего процедуру, не представ- WTСобой подходящее значение, так что вам может понадобиться выполнить процедуру несколько рЙ Из разных сеансов, чтобы заставить ее работать оптимально. **»* Hi., —-—_——------------------------------------------------------------ Незначительный недостаток групп списков свободных блоков (как и для мно- Шственных списков свободных блоков) состоит в том, что будет существовать
130 Глава 5. Фактор кластеризации множество наборов новых пустых блоков, ожидающих использования. Отметка максимального уровня заполнения для объектов с несколькими группами сво- бодных блоков будет немного больше, чем в противоположном случае (в худ- шем случае 5 х freelist groups х freelists), но для больших объектов это, возможно, не будет существенно. Главный недостаток групп списков свободных блоков состоит в том, что вы не можете изменить количество групп, не перестроив объект. Так что если ко-t личество групп точно совпадает с количеством экземпляров в системе, вы полу- чите проблему реорганизации, когда решите добавить несколько узлов к систе- ме. Планируйте с учетом расширения. Порядок столбцов В главе 4 мы увидели, как диапазонные предикаты (например, coll between 1 and 3) могут снизить преимущества последних столбцов в индексе. Любые пре- дикаты, основанные на столбцах, которые находятся дальше самого раннего диапазонного предиката, будут проигнорированы при вычислении эффектив- ной селективности индекса — хотя они все равно будут использоваться в эф- фективной селективности таблицы — и это может привести к тому, что Oracle будет иметь необоснованно высокие значения для стоимости этого индекса. Отсюда предположение, что вы должны реструктурировать некоторые индек- сы, поместив столбцы, которые часто появляются в диапазонных предикатах, в конец определения индекса. Это только одно важное соображение по выбору порядка столбцов в индек- се. Другое соображение состоит в возможности улучшить сжимаемость индек- са, поместив наименее селективные (с чаще повторяющимися значениями) столбцы в начало. Другой вариант — так разместить столбцы, чтобы некоторые наиболее частые запросы могли бы выполнять order by без необходимости сор- тировки (механизм выполнения, который обычно показывается как sort (or- der by) nosortB плане выполнения). Какова бы ни была причина изменения порядка столбцов в индексе, помните, что это может изменить clustering_factor. Косвенный эффект может состо- ять в том, что вследствие слишком высокой вычисленной стоимости индекса для диапазонного сканирования Oracle будет игнорировать индекс. Мы можем использовать преувеличенную модель, чтобы продемонстрировать этот эффект (см. сценарий col_order.sql в онлайн-хранилище кода): create table tl pctfree 90 pctused 10 as select trunc((rownum-1)I 100) clustered, mod(rownum - 1, 100) scattered, lpad(rownum,10) small_vc from all_objects where rownum <= 10000
Порядок столбцов 131 create index tl_il_good on tl(clustered, scattered); create index tl_12_bad on tl(scattered, clustered); -- Здесь соберите статистику, используя dbms_stats Я использовал стандартный трюк с установкой большого значения pctfree для распределения таблицы по большому количеству блоков, без генерации ог- ромного количества данных. Для 10 000 достаточно маленьких записей, созданных сценарием, требуется 278 блоков. Функция trunc(), используемая в столбце clustered, дает мне значения от 0 до 99, каждое из которых повторяется 100 раз, перед тем как измениться; функция mod(), используемая в столбце scat- tered, применяется для. цикла по числам от 0 до 99. Я создал два индекса по од- ной и той же паре столбцов, изменив порядок столбцов хорошего индекса, что- бы получить плохой индекс. Имена индексов отражают их cluster! ng_f а с tor: INDEX_NAME BLEVEL LEAF_BLOCKS CLUSTERING_FACTOR T1_I1_GOOD 1 24 278 T1_I2_BAD 1 24 10000 v Когда мы выполняем запрос, который (в соответствии с общей теорией для диапазонных предикатов), как мы думаем, может использовать индекс tl_12_ bad, вот что мы увидим: select count(small_vc) from tl where scattered =50 -- равенство по первому столбцу tl_i 2_bad and clustered between 1 and 5 -- диапазон по второму столбцу tl_i2_bad План выполнения (9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=4 Card=l Bytes=16) 1 0 SORT (AGGREGATE) 2 1 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=4 Card=6 Bytes=96) 3 2 INDEX (RANGE SCAN) OF 'T1_I1_GOOD' (NON-UNIQUE) (Cost=3 Card=604) Несмотря на тот факт, что у нас есть индекс, который, кажется, прекрасно соответствует требованиям этого запроса, его первый столбец (с предикатом равенства) разбросан, а его второй столбец (с диапазонным предикатом) — сгруппирован, оптимизатор выбрал использование неправильного индекса, того, который будет управляться диапазонным предикатом. Когда мы добавляем подсказку, чтобы заставить оптимизатор использовать индекс, который, как мы считаем, тщательно составлен, чтобы соответствовать запросу, Oracle будет использовать его, но стоимость вырастет более чем в два раза по сравнению с индексом, который оптимизатор выбрал по умолчанию, select /*+ index(tl tl_i2_bad) */ count(small_vc)
132 Глава 5. Фактор кластеризации from tl where scattered = 50 and clustered between 1 and 5 План выполнения (9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=9 Card=l Bytes=16) 1 0 SORT (AGGREGATE) 2 1 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=9 Card=6 Bytes=96) 3 2 INDEX (RANGE SCAN) OF 'T1_I2_BAD' (NON-UNIQUE) (Cost=2 Card=6) Здесь проявляется основной дефект производной clustering_f actor, кото- рую он использовал для вычисления стоимости индексного пути доступа. Оп- тимизатор оценивает количество посещений блоков таблицы, но у него нет ни- каких соображений о том, сколько из этих посещений не должны приниматься в расчет, потому что они возвращаются к недавно посещенному блоку. Независимо от того, какой индекс мы будем использовать в этом примере, мы посетим в точности одинаковое число блоков таблицы — но порядок посе- щения будет отличаться, и этого достаточно, чтобы в вычислениях оптимизато- ра было большое отличие. Для полноты изложения пропустим наши статистические данные через фор- мулу. Селективность 'scattered = 50': 1/100 = 0.01 Селективность 'clustered between 1 and 5': (5 - 1) / ( 99 - 0 ) + 2/100 = 0.060404 Объединенная селективность: 0.01 * 0.060404 = 0.00060404 cost (tl_il_good) = 1 + ceil(0.060404 * 24) + -- диапазон по первому столбцу, второй столбец не используется ceiI(0.00060404 * 278) -- второй столбец может быть использован до посещения таблицы = 1 + 2 + 1 = 4 cost (tl_i2_bad) = 1 + ceiI(0.00060404 * 24) + -- можно использовать оба столбца для старт-стопных ключей cei1(0.00060404 * 10000) -- но clustering_factor огромен = 1 + 1 + 7 = 9 Эти числа наглядно показывают влияние clusteri ng_f actor. Хотя первый набор значений показывает, что диапазонный предикат по первому столбцу уменьшил эффективность индекса tl_i l_good, это уменьшение незначительно по сравнению с влиянием, вызванным огромным увеличением clustering- factor во втором наборе значений. В этом примере дополнительные ресурсы, которые будут использоваться из-за того, что мы выбрали неправильный индекс, минимальны, мы все равно посетим точно такое число записей таблицы, то есть дополнительные операции
Дополнительные столбцы 133 ввода-вывода исключены, и когда мы используем неправильный индекс, мы по- сетим два листовых блока, а не один. Гарантируемым штрафом за использование неправильного индекса будет дополнительное использование процессорных ресурсов, связанное со сканиро- ванием ненужного числа элементов индекса. Существует 500 элементов в лю- бом из индексов, где clustered between 1 and 5, и мы исследуем около 400 из них (с (1, 50) по (5, 50)), если используем индекс tl_il_good для нашего за- проса. Существует 100 элементов в индексе, для которых scattered = 50, и мы исследуем около пяти из них (с (50, 1) по (50, 5)), если используем индекс tl_12_bad. В реальных системах проблема выбора более тонкая, чем выбор между дву- мя индексами по одним и тем же столбцам в немного разном порядке; возмож- ность ошибки становится намного больше с изменениями в сложных планах выполнения — это может привести не только к пустому использованию неболь- шого количества процессорных ресурсов. Дополнительные столбцы К проблемам может привести не только порядок столбцов. Достаточно об- щая (и зачастую эффективная) практика состоит в добавлении столбца или двух к существующему индексу. Но теперь я уверен, что вы не будете удив- лены, когда обнаружится, что это тоже может привести к значительным из- менениям clustering_factor и, следовательно, к изменениям в желатель- ности индекса. Представьте систему, в которой фигурирует таблица для отслеживания пе- ремещения товаров. В ней существует достаточно очевидный индекс по move- ment_date, но через некоторое время администратору базы данных может стать очевидно, что некоторое количество часто используемых запросов получит выгоду от добавления столбца product_id к этому индексу (см. сценарий extra_col.sql в онлайн-хранилище кода). create table tl as select sysdate + trunc((rownum-1) / 500) trune(dbms_random.value(1,60.999)) trunc(dbms_random.value(1,10.000)) lpad(rownum,10) rpad('x',100) from all_objects Where rownum <= 10000 rem create index tl_il on tl(movement_date) rem create index tl_il on tl(movement_date, movement_date, -- 500 записей на день product_id, qty, small_vc, padding --20 дней * 500 записей на день -- первоначальный индекс product_id); -- модифицированный индекс
134 Глава 5. Фактор кластеризации INDEX_COLUMNS BLEVEL LEAF_BLOCKS CLUSTERING_FACTOR movement_date 1 27 182 movement_date, product_Jd 1 31 6645 Хотя размер индекса (как показывает счетчик листовых блоков) несколько вырос, clustering_factor снова существенно изменился. Когда индекс построен только по (movement_date), мы ожидаем увидеть множество записей для одной и той же даты, вводимых в базу данных в одно и то же время, и 500 записей, которые мы создали для каждой даты, упакован- ных в 9 или 10 смежных блоков в таблице, примерно по 50 записей на блок. Ин- декс, основанный только на movement_date, будет иметь очень хорошее значе- ние clustering_factor. Когда мы меняем индекс на (movement_date, product_id), данные все еще сгруппированы по дате, но любые два элемента для одного и того же рго- duct_id, соответствующие одной и той же дате, скорее всего, будут находиться в двух различных блоках таблицы в этой небольшой группе из девяти или деся- ти блоков. По мере того как мы проходим индекс в поиске заданной даты, мы будем прыгать вперед и назад вокруг небольшой группы блоков таблицы, не ос- таваясь внутри одного блока таблицы для 50 переходов по индексу. Наш clus- teri ng_factor очень сильно увеличится. Мы можем увидеть результат с помощью нескольких запросов: select sum(qty) from tl where movement_date = trunc (sysdate) + 7 and product_id = 44 select product_id, max(small_vc) from tl where movement_date = trunc(sysdate) + 7 group by product_id Первый запрос представляет собой пример типа запросов, которые побужда- ют нас добавить дополнительный столбец в индекс. Второй запрос представля- ет собой пример запроса, который пострадает от последствий такого изменения. В обоих случаях Oracle посетит один и тот же небольшой набор примерно из десяти блоков в таблице — но дополнительный столбец изменяет порядок, в ко- тором посещаются записи (как раз к этому имеет отношение clusteri ng_ factor), так что стоимость изменяется, и во втором случае план выполнения меняется в худшую сторону. Начнем с планов выполнения для первого запроса (до и после изменения) и отметим, что стоимость запроса уменьшается некоторым образом, в чем, воз- можно, выражается эффект более высокой точности индекса:
|ррректировка статистики 135 уан выполнения (9.2.0.6, автотрассировка - первый запрос - первоначальный |ндекс) У SELECT STATEMENT Optimizer=ALL_ROWS (Cost=12 Card=l Bytes=14) I 0 SORT (AGGREGATE) 1 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=12 Card=8 Bytes=112) ’I 2 INDEX (RANGE SCAN) OF 'T1_U' (NON-UNIQUE) (Cost=2 Card=500) ithaw выполнения (9.2.0.6, автотрассировка - первый запрос - модифицированный ^ндекс) Л?* ~ - • | SELECT STATEMENT Optimizer=ALL_ROWS (Cost=7 Card=l Bytes=14) 4 0 SORT (AGGREGATE) 1 1 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=7 Card=8 Bytes=112) | 2 INDEX (RANGE SCAN) OF ’T1_U' (NON-UNIQUE) (Cost=l Card=8) А теперь приведем планы выполнения для второго запроса (до и после изме- нения), показывающие несчастье, которое может случиться, если £l.(j5tering_factor больше не будет представлять первоначальное предназна- чение индекса: План выполнения (9.2.0.6, автотрассировка - второй запрос - первоначальный индекс) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=19 Card=60 Bytes=1320) 1 0 SORT (GROUP BY) (Cost=19 Card=60 Bytes=1320) 2 1 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=12 Card=500 Bytes=11000) 3 2 INDEX (RANGE SCAN) OF 'T1_U' (NON-UNIQUE) (Cost=2 Card=500) План выполнения (9.2.0.6, автотрассировка - второй запрос - модифицированный Индекс) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=36 Card=60 Bytes=1320) 1 0 SORT (GROUP BY) (Cost=36 Card=60 Bytes=1320) 2 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=29 Card=500 Bytes=11000) Как вы видите, обманчиво высокое значение clustering-factor спрятало местоположение данных, и оптимизатор переключился с точного индексного Пути доступа на более экстравагантное табличное сканирование. Йрректировка статистики До сих пор я тратил все свое время на описание того, что фактор кластеризации К том виде, в каком он вычисляется Oracle, может не представлять действитель- ную группировку данных в таблице. Имея некоторое представление о данных Я о том, какие вычисления выполняет Oracle, вы можете скорректировать про- блему, и я хочу подчеркнуть слово «скорректировать». Использование функций sys__op_countchg() Возможно сообщить Oracle все что угодно о статистике вашей системы, пере- крыв любые значения, которые он собрал; но самое сложное состоит в том, что- бы определить неправильные значения и предоставить ему правильные. Не
136 Глава 5. Фактор кластеризации имеет смысла просто играть с числами, пока не случится, что некоторый код SQL начнет работать так, как вам необходимо. Очень просто взломать статистику, но ваша цель должна состоять в том, чтобы дать Oracle более точную информацию, потому что с более точным пред- ставлением о данных и способах их использования оптимизатор может рабо- тать лучше. Так как основной предмет обсуждения этой главы — clustering_factor, я собираюсь рассказать вам, как изменить его и на что его менять. Взгляните на пакет dbms_stats. Он содержит два критичных класса про- цедур: get_xxx_stats и set_xxx_stats. Для целей этой главы нам интересны только get_i ndex_stats и set_i ndex_stats. В принципе, мы всегда можем изменить значение clustering_factor индекса с помощью кода PL/SQL, ко- торый читает статистические данные из словаря данных, модифицирует неко- торые из них и записывает модифицированные значения назад в словарь дан- ных, например, так (сценарий hack_stats.sql в онлайн-хранилище кода): declare m_numrows m numlblks number; number; m_numdist number; m_avglblk number; m_avgdblk number; m_clstfct number; m_indlevel number; begin dbms_stats.get_i ndex_stats( ownname => NULL, indname => '{название numrows => m_numrows, numlblks => m_numlblks, numdist => m_numdist, avglblk => m_avglblk, avgdblk => m_avgdblk, clstfct => m_clstfct, indlevel => m_indlevel индекса}', / » m_clsfct := {нечто совершенно другое}; dbms_stats.set_i ndex_stats( ownname => NULL, indname => '{название индекса}', numrows => m_numrows, numlblks => m_numlblks, numdist => m_numdist, avglblk => m_avglblk, avgdblk => m_avgdblk, clstfct => m_clstfct, indlevel => m indlevel ); end; /
(рректировка статистики 137 ?ЛОМ СЛОВАРЯ ДАННЫХ шествует огромная разница между взломом словаря данных с помощью опубликованного доку- •нтированного PL/SQL API и взломом с помощью операторов, подобных update col$ set. приведенном выше примере вы можете не понимать, что сообщаете Oracle о своей системе, но, по 0йней мере, вы оставите словарь данных в согласованном состоянии. В иных случаях вы, во-пер- Й, не знаете, как много других изменений вы должны также выполнить, а во-вторых, — попадут jjlp самом деле все ваши изменения в словарь данных, так как он, видимо, обновляется из кэша Ьваря (v$rowcache) достаточно случайным образом; следовательно, в-третьих, вы можете легко ^йить базу данных в несогласованном состоянии, что приведет к последующим проблемам безопасностью, авариям и тихим, но обширным повреждениям данных. Этот способ прост, самый тонкий момент здесь состоит в том, чтобы решить, f&Koe значение использовать для clusteri ng_factor. Ответ на этот вопрос зависит от обстоятельств. Однако давайте начнем с со- |мпенно другого вопроса как именно Oracle вычисляет clusteri ng_f ас to г? эдЙэовите dbms_stats.gather__index_stats() с включенным параметром sql_ ^face, и если вы работаете с Oracle 91, вы это узнаете. Для простого индекса ^жиде В-дерева файл трассировки будет содержать часть кода SQL, который дедет выглядеть подобно следующему (попробуйте сделать это после сценария 4wO’ne.sql)- Д |ШйОлните этот код из сеанса SQL*Plus, а затем проверьте файл трассировки, session set sql_trace true, fe^in dbms_stacs gather^ndex_stats( user, ’tl_il’, estimate_percent => null ), WL / Wit ♦/ Select /*+ cursor_shan ng_exact dynamic_sampli ng(0) nojnonitori ng no_expand index(t,"Tl II") noparallel_7ndex(t,"T1_I1") w count(*) as nrw, count(distinet sys_op_lbid(49721,'L',t rowid)) as nib, count( distinct hextoraw( sys_op_descend("DATE_ORD”)||sys_op_descend("SEQ_ORD") ) ) as ndk, sys_op_countchg(substrb(t rowid,1,15),1) as elf
138 Глава 5. Фактор кластеризации from "TESTJJSER". "Tl" t where "DATE_0RD" is not null or “SEQ_ORD” is not null В предыдущем примере столбец nrw превращается в количество записей в индексе (user_i ndexes . num_rows), nib превращается в количество листовых блоков (user_i ndexes. leaf_blocks), ndk становится количеством уникаль- ных ключей в индексе (user_i ndexes .di sti nct_keys), a elf становится clus- teri ng_f actor (user_i ndexes.clusteri ng_factor). Появление здесь функции sys_op_descend() может вызвать некоторое удив- ление; эта функция обычно используется для генерации значений, хранимых для индексов со столбцами по убыванию, но здесь, я думаю, она используется для вставки байта-разделителя между столбцами в индексе по нескольким столб- цам, чтобы можно было при подсчете отличать элементы вида (* ааа’ , ’ Ь') и (’аа'.’аЬ’), которые иначе выглядели бы идентично. Функция sys_op_lbi d (), очевидно, возвращает идентификатор листового блока {leaf block ID), и точный смысл возвращаемого идентификатора зависит от параметра из одной буквы. В этом примере 49 721 — это ob ject_i d индекса, указанного в подсказке index, и при использовании параметра L, похоже, воз- вращается абсолютный адрес первого элемента листового блока, в котором су- ществует указанный идентификатор записи таблицы. (Существуют варианты для индексно организованных таблиц — index organized tables, IOTs, вторичных индексов по ЮТ, индексов на основе битовых карт, секционированных индек- сов и так далее.) Но для наших целей наиболее интересна функция sys_op_countchg(). Судя по ее названию, эта функция, вероятно, подсчитывает изменения, и пер- вый ее входной параметр — это элемент, представляющий собой идентифика- тор блока в идентификаторе записи таблицы (ob j ect_i d, относительный номер файла, номер блока), так что эта функция точно соответствует нашему описа- нию того, как вычисляется clustering_factor. Но что представляет собой единица, которую мы видим в качестве второго параметра? Когда я впервые понял, как определяется clusteri ng_f actor, то вскоре осоз- нал самый большой недостаток этого способа: Oracle не запоминал предыдущую историю при прохождении по индексу; он запоминал только предыдущий блок таблицы, чтобы можно было проверить, находится ли последняя запись в том же самом блоке таблицы, который был ранее, или в новом блоке. Так что когда я уви- дел эту функцию, моим первым предположением (или надеждой) было, что вто- рой параметр представляет собой метод, позволяющий указать Oracle запоми- нать список предыдущих посещений блоков при проходе по индексу. Вспомните таблицу, которую я создал в сценарии freelists.sqL, с параметром freelists, установленным в 5. Посмотрите, что произойдет, если мы выпол- ним запрос для сбора статистики самого Oracle (переписанный и подчищен- ный, как показано далее) по этой таблице, используя различные значения для этого второго параметра (сценарий cLufac_caLc.sqL в онлайн-хранилище кода): select /*+ cursor_shari ng_exact
Корректировка статистики 139 dynamic_sampli ng(0) no_moni tori ng no_expand index (t,"Tl_U”) noparallel_index(t,"T1_I1") */ sys_op_countchg(substrb(t.rowid,1,15),&m_history) as elf from S "TESTJJSER”. "Tl" t where ”DATE_0RD" is not null $Г "SEQ_ORD" is not null fjnter value for m_history: 5 CLF 746 1 row selected. Мне пришлось использовать подставляемый параметр при выполнении это- ГО запроса из простого сеанса SQL*Plus, так как функция давала сбой, если я пытался вставить этот запрос в цикл PL/SQL с переменной PL/SQL в ка- честве входного параметра. Я выполнил этот запрос семь раз подряд, вводя каждый раз разные значения для m_hi story. Результаты этого теста приве- дены в табл. 5.2. Первый набор результатов — это результаты выполнения freelists.sql, когда я использовал пять параллельных процессов и мне посчастли- вилось получить совершенное разделение процессов. Второй набор — это ре- зультат выполнения, в котором я удвоил количество параллельных процессов С 5 до 10, с менее счастливым разделением процессов — что дало мне 52 000 за- писей и 1 502 блока в таблице. Таблица 5.2. Эффекты изменения загадочного параметра sys_op_countchg mjtfstory Вычисленный фактор кластеризации Вычисленный фактор кластеризации (5 процессов) (10 процессов) SI C! UlAWN 26 000 43 615 26 000 34 533 26 000 25 652 25 948 16 835 746 3 212 746 1 742 746 1 496 Как только значение m_history совпадает с параметром freelists для таб- лицы, значение clustering_factor неожиданно изменяется со слишком боль- шого на действительно достаточно разумное! Сложно поверить, что это про- стое совпадение. Итак, использование собственной функции Oracle для вычисления clus- ter! ng_factor, но с подстановкой значения freelists для таблицы, может
140 Глава 5. Фактор кластеризации быть корректным методом исправления некоторых ошибок в значении clus- teri ng_factor для индексов по строго последовательным данным. (Эта стра- тегия применима, если вы используете множественные группы списков свобод- ных блоков, но при этом необходимо умножить freelists на freelist groups для получения значения второго параметра.) Может ли подобная стратегия использоваться для нахождения модифици- рованного значения clusteri ng_f a ctor в других обстоятельствах? Я думаю, что ответом будет «да» для таблиц, которые находятся в табличных простран- ствах ASSM. Вспомните, что в настоящее время Oracle выделяет и форматирует 16 новых блоков за раз при использовании автоматического управления пространством сегментов (даже когда размеры экстентов очень большие). Это означает, что но- вые данные будут небрежно разбросаны по группам из 16 блоков, вместо того чтобы быть сильно упакованными. Вызова функции Oracle sys_op_countchg() с параметром 16 может быть достаточно, чтобы получить разумное значение clustering_factor, когда Oracle создает бессмысленное значение. Однако значение 16 должно использо- ваться в качестве верхней границы. Если в действительности уровень паралле- лизма обычно меньше 16, то ваше фактическое значение уровня параллелизма, вероятно, будет более подходящим. Что бы вы ни делали, экспериментируя с этой функцией, не применяйте ее просто ко всем подряд индексам или даже ко всем индексам некоторой табли- цы. Возможно, существует всего лишь горстка критичных индексов, для кото- рых полезно сообщить Oracle немного больше правды о вашей системе, в дру- гих случаях вы только внесете путаницу. Неформальные стратегии Мы все еще испытываем проблемы с инвертированными ключами индексов, индексами с добавленными столбцами и индексами, в которых порядок столб- цов был изменен. Игры с функцией sys_op_countchg() в этих случаях не по- могут. Однако если вы рассмотрите примеры в этой главе, вы увидите, что все они имеют одну общую черту. В каждом случае основное применение индекса выте- кает из подмножества его столбцов. В примере с инвертированным ключом (date_ord, seq_no) важность ин- декса зависит только от столбца date_ord, и наличие столбца seq_no не добав- ляет точности нашим запросам. В примере с добавлением дополнительных столбцов (date_movement, рго- duct_id) важность индекса определяется столбцом date_movement; product_ id — это небольшая уловка для увеличения производительности (некоторых запросов). В примере с изменением порядка столбцов (scattered, clustered) этот ар- гумент слабее, но мы можем определить, что порядок в таблице определяется кластерным индексом, хотя столбцы в индексе не упорядочены таким образом.
разное 141 Во всех трех случаях вы можете утверждать, что более подходящее значение clusteri ng_f ас to г может быть получено с помощью создания индекса только $3 определяющих столбцов, вычисления clusteri ng_factor для этого индекса К переноса результата в первоначальный индекс (конечно, вы должны делать ^*0 на резервной копии вашей базы данных). Я считаю, что этот способ очень хорош в первых двух случаях, упомянутых |>дНее, но меньше подходит для третьего случая. В третьем случае правильность $гого аргумента больше зависит от фактического использования индекса и при- воды запросов Однако когда аргумент, касающийся определяющих столбцов, Терпит неудачу, вы можете вернуться к методу sys_op_countchg(). В примере, । котором данные сгруппированы по столбцу clustered с группой из 9 или (О блоков, вызов функции sy s_op_countchg () со значением 9 может быть наи- лучшим способом определения подходящего значения clusteri ng_factor для Использования в этом индексе И наконец, возможны варианты, когда просто известен правильный ответ. Веди вы знаете, что при типичном значении ключа все данные будут найдены, Скажем, в 5 блоках таблицы, но Oracle считает, что при этом придется посетить 100 блоков таблицы, вы можете просто разделить clusteri ng_factor на 20, цтобы сообщить Oracle правду. Чтобы определить, сколько блоков таблицы Oracle считает необходимым посетить, просто посмотрите значение столбца H$er_indexes.avg_data_blocks_per_key, который представляет собой изме- ренную форму clustering_factor и вычисляется как round(cluster!ng_ factor / distinct_keys). Рязное Существует множество других случаев, которые надо рассмотреть, если вы хо- тите получить полную картину того, как cluster!ng_factor может повлиять ва оптимизатор. В этой книге нет места, чтобы рассматривать их, но вот одна мысль на будущее В Oracle 10g появился механизм для сжатия таблиц в режи- ме онлайн. Это работает только для таблиц с включенным параметром row movement, которые хранятся в табличных пространствах, использующих ASSM. Для сжатия таблицы вы можете использовать следующий код: alter table х enable row movement. alter table x shrink space compact, -- перемещает записи alter table x shrink space, -- уменьшает отметку максимального заполнения Прежде чем броситься использовать эту возможность, запомните, что она позволяет вам вернуть пространство, заполнив «дырки» в начале таблицы дан- ными, перемещенными из конца таблицы Другими словами, любая естествен- ная группировка данных на основании времени их поступления может быть Потеряна, так как данные перемещаются по одной записи за раз из одного кон- ца таблицы в другой Будьте осторожны с эффектом: он может повлиять на cluster! ng_factor и желательность индексов такой таблицы.
142 Глава 5. Фактор кластеризации Заключение Фактор кластеризации очень важен для подсчета стоимости диапазонных ска- нирований по индексу; однако существуют некоторые особенности Oracle и не- которые стратегии, связанные с производительностью, которые могут привести к неподходящим значениям clustering_factor. Во многих случаях мы можем предсказать возможные проблемы и использо- вать альтернативные методы для генерации clusteri ng_f actor. Мы всегда мо- жем использовать пакет dbms_stats, чтобы установить правильное значение clusteri ng_factor. Если clustering_factor преувеличен из-за использования множества спи- сков свободных блоков или ASSM, вы можете генерировать clustering__ factor с помощью внутреннего кода Oracle, указав модифицированное значе- ние второго параметра функции sys_op_countchg() для получения более реа- листичного значения. Если clustering_factor преувеличен из-за индексов по инвертированным ключам, добавленных столбцов или даже из-за изменения порядка столбцов, то вы можете сгенерировать его значение на основании вашего знания о зависимо- сти фактической функциональности индекса от подмножества столбцов. Если необходимо, постройте уменьшенный индекс на резервной копии данных, сге- нерируйте правильное значение clusteri ng_f ас to г и переместите это значе- ние в рабочий индекс. Корректировка clustering_factor — это не хакерство или жульничество, а попытка дать оптимизатору более точную информацию по сравнению с той, которую он может в настоящее время получить сам. Тестовые сценарии Файлы к этой главе, доступные для загрузки, перечислены в табл. 5.3. Таблица 5.3. Тестовые сценарии к главе 5 Сценарий Комментарии basejine.sql freejists.sql reversedjnd.sql reverse.sql Сценарий для создания базового теста с freelists, равным 1 Повторяет тест с freelists, установленным в 5 Повторяет тест и затем инвертирует индекс Код SQL для создания дампа списка чисел, отсортированных по их инвертированному внутреннему представлению assm_test.sql flg.sql col_order.sql extra_col.sql hack_stats.sql clufac_calc.sql . setenv.sql Повторят тестовый случай с табличным пространством с использованием ASSM Повторяет тест с freelists, установленным в 2, и freelist groups, установленным в 3 Демонстрирует то, как изменение порядка столбцов влияет на clustering_factor Демонстрирует эффекты при добавлении столбца к существующему индексу Сценарий для модификации статистики напрямую в словаре данных Код SQL, используемый пакетом dbms_stats для вычисления clusteringjactor Установка стандартного тестового окружения для SQL*Plus
6 Вопросы селективности До сих пор я рассматривал только простые случаи. Я использовал числовые данные, избегал значений NULL, генерировал аккуратное распределение данных и предпринимал множество других действий, чтобы оптимизатор работал пра- вильно. Однако есть множество случаев, когда расчеты выполняются, как описано, но результаты получаются абсолютно неверными. В этой главе я хочу обсудить чаще всего встречающиеся причины, почему стандартные расчеты селективно- сти выдают неправильные результаты, и рассказать о том, как (иногда) можно Избежать этой проблемы. Я также хочу обсудить пару особенностей, которые Могут привести к тому, что оптимизатор выполнит расчеты не по тем предика- там. В главе 3 я не вдавался в подробности гистограмм. Я не буду вдаваться в них И в этой главе. Влияние гистограмм двух типов привносит дополнительную Сложность в расчеты селективности. Остается еще немало вопросов, на которые нужно ответить перед рассмотрением гистограмм. Я начну с демонстрации того, как можно применить стандартную формулу К типам дат и символьным типам, а затем расскажу, что происходит, когда мы Храним данные в столбцах с неверным типом данных. Затем можно будет рас- смотреть варианты, когда «специальные значения» могут порождать проблемы, Ж закончить случаями, когда Oracle выполняет расчеты с помощью предикатов, о существовании которых вы не знали. Различные типы данных Мы использовали базовую формулу селективности пока только для числовых типов данных. Пришло время рассмотреть пару других часто используемых ти- ров Данных, для того чтобы знать, чем они отличаются. Мы использовали базовую формулу для расчета селективности предикатов ПИ основе диапазона и знаем, что формула несколько различается в зависимо- сти от того, есть ли одна или две нестрогие границы диапазона (что означает,
144 Глава б. Вопросы селективности что значение находится между <= и >=), или таких границ нет. Обозначив ко- личество нестрогих границ как N, версию формулы, которая использует user_ tab_columns . num_di sti net, можно записать следующим образом: (необходимый диапазон) / (наибольшее значение в столбце - наименьшее значение в столбце) + N / num_dlstinct Например, в столбце с 1000 различных значений типа integer от 1 до 1000 и парой предикатов coIX > 10 и coIX <= 20 есть одна нестрогая граница диапа- зона (<=), следовательно, формула будет выглядеть следующим образом: (20 - 10) / (1000 - 1) + 1/1000 = 10/999 + 1/1000 Видно, что этот пример не даст абсолютно правильного результата, потому что нужны 10 возможных значений из 1000, но результат достаточно близок к правильному. Значения типа «дата» При использовании типов данных «дата» меняется немногое. Рассмотрим стол- бец, содержащий дату без времени, в котором содержатся значения от 1 января 2000 года до 31 декабря 2004 года. По предикату date_col between 30 декабря 2002 года and 5 января 2003 года будет получена следующая селективность (5 января 2003 года - 30 декабря 2002 года) / ( 31 декабря 2004 года - 1 января 2000 года) + 2/(количество различающихся дат) К счастью, Oracle может выполнять соответствующие арифметические опе- рации над датами, в результате чего получается: 6/1826 + 2/1827 Это также не совсем правильный ответ, потому что оценка задачи человеком предполагает 7 дат из 1827 с селективностью, равной 7/1827. Ошибка в обоих приведенных примерах демонстрирует проблему, которая была поднята в главе 3. Oracle выполняет расчеты для непрерывных данных, а люди во многих случаях используют дискретные данные (то есть список опре- деленных значений). Когда количество уникальных значений в списках боль- шое, то, конечно, ошибка в расчетах обычно невелика — но для списков только с несколькими уникальными значениями необходимо соблюдать осторожность. Символьные значения Рассмотрим еще один пример. Что означает оценить общий диапазон символь- ного столбца, в котором наименьшее значение — 'Aardvark', а наибольшее — ' Zymu rgy' ? И как будет вычислена разница между двумя словами для предика- та colC between 'Apple' and 'Blueberry'? Начать поиск ответов на эти во- просы лучше с представления user_tab_histograms, в котором видно, что Oracle использует числовое представление символьных строк (здесь не будет обсуждаться, что оптимизатор делает с этой информацией, мы просто восполь- зуемся возможностью видеть сгенерированные значения). В тестовую таблицу tic двумя столбцами — один типа varchar2(10),a дру- гой char(10) — были добавлены строки 'Aardvark', 'Apple', 'Blueberry'
различные типы данных 145 и 'Zymurgy' в оба столбца, а затем была сгенерирована статистика, включая гистограммы. Значения в столбцах типа char(n) дополняются пробелами до их полной Объявленной длины (кроме случаев, когда значение содержит NULL), поэтому $ включил в этот пример столбец char(n). Как обычно, в моей демонстрацион- ИОЙ среде используются блоки размером 8 Кбайт, локально управляемые таб- личные пространства с экстентом размером в 1 Мбайт и ручное управление раз- мером сегмента, системная статистика (CPU costing) отключена — см. сценарий char_types.sql в онлайн-хранилище кода: create table tl ( vl0 varchar2(10), cl0 char(10) Добавление данных и генерация гистограмм (см. сценарий в онлайн-хранилище кода) select columnjiame, endpoint_number, endpoint_value from user_tab_hi stograms rthere table_name = 'Tl' Order by column_name, endpoint_Number ColEnd noEnd Value C101339,475,752,638,459,000,000,000,000,000,000,000 -- 'Aardvark ' 2339,779,832,781,209,000,000,000,000,000,000,000 -- 'Apple ' 3344,891,393,972,447,000,000,000,000,000,000,000 -- 'Blueberry ’ 4469,769,561,047,943,000,000,000,000,000,000,000 -- 'Zymurgy ' VJ01339,475,752,638,459,000,000,000.000.000,000,000 -- 'Aardvark' >339,779,832,781,057,000,000,000,000,000,000,000 -- 'Apple' >344,891,393,972,447,000,000,000,000,000,000,000 -- 'Blueberry' 4469,769,561,047,943,000,000,000,000,000,000,000 -- 'Zymurgy* Це вполне очевидно, что означают эти числа, но это можно определить с по- мощыо представления user_tab_columns и представления user_tab_ histo- grams (для версии 8i), в котором столбец endpoint_actual_value всегда за- Шлнен. Примечание Й версии 9i Oracle заполняет столбец endpoint_actual_value представлений гистограммы толь- ко в том случае, если существует по крайней мере одна пара значений, которая идентична по Первым шести символам — вот почему я использовал версию 81 для генерации вывода в этом Примере.
146 Глава 6. Вопросы селективности Oracle делает следующее. О Извлекает максимум 32 байта из столбца; это представление значения столб- ца показывает, как хранятся значения low_value, high_value и end_poi nt_ actual. О Извлекает первые 15 байтов из 32, дополняя их нулями справа, если необхо- димо. О Конвертирует 15 байтов шестнадцатеричного числа в десятичное и округля- ет его до 15 знаков. Рассмотрим рабочий пример — строку 'Aardvark', чтобы увидеть, что полу- ченное значение соответствует значению в гистограмме. О 'Aardvark' при извлечении в шестнадцатеричном формате из столбца с h а г (10) содержит следующий список байтовых значений (с дополнением пробела- ми — значениями 20,20, что обусловливается типом char(10)): '41,61,72,64,76,61,72,6В,2G,2G' О Так как значение получилось меньше 15 байт, то добавляются несколько ну- лей, чтобы получить число 0Х416172647661726В20200000000000 (если бы столбец был объявлен как char(40),TO значение уже было бы дополне- но пробелами (0x20) до 40 символов, в результате чего пришлось бы остановиться на пятнадцатом байте и число выглядело бы как 0Х416172647661726В20202020202020). О После конвертации этого достаточно большого шестнадцатеричного числа в десятичное будет получено 339 475 752 638 459 G43 G65 991 628 037 554 176 О И если отбросить все цифры после первых 15, то будет получено 339 475 752 638 459 GGG G0G GGG GG0 GGG GGG G0G Взглянув на округление, можно заметить, что после первых шести или семи символов строки остальные символы не играют никакой роли для числового представления, используемого оптимизатором, — вот почему при сравнении по- лученных значений типов char(10) Hvarchar2(10) различаются только чи- словые значения для 'Apple'. Нет нужды рассматривать это сравнение более детально, но можно представить проблемы, с которыми сталкивается оптимиза- тор, имея дело с данными, состоящими, например, из URL, когда множество ад- ресов начинается с http:// (проблема частично решается тем фактом, что пер- вые 32 символа URL хранятся как endpoi nt_actual_value). Проблема на самом деле может быть еще серьезнее из-за растущей популяр- ности поддержки других языков. Если в качестве набора символов базы данных будет выбран набор символов с многобайтовым представлением каждого сим- вола, то Oracle будет использовать первые 15 байтов строки, а не первые 15 символов. Из-за этого точность будет еще хуже (конечно, если переключить- • ся в многобайтовый набор символов с фиксированной длиной, то все символь- ные данные станут длинее, что в любом случае потребует тестирования произ- водительности). См. сценарий nchar_types.sql в онлайн-хранилище кода.
Различные типы данных 147 Строки символов могут стать причиной множества проблем с запросами по диапазонам значений, но проблемы обычно появляются тогда, когда вы на са- мом деле работаете не со строками, как вы увидите в следующем разделе. Неверные типы данных В одном из первых примеров в этой главе рассматривался предикат date_col between 30 декабря 2002 года and 5 января 2003 года на таблице, содержащей данные от 1 января 2000 года до 31 декабря 2004 года, и мы видели, как опти- мизатор рассчитал селективность такого предиката. Но давайте посмотрим, ка- кие могут возникнуть ошибки в распространенных способах хранения дат. Мы создадим и наполним таблицу строками по одной на каждую дату в пятилетием диапазоне (см. сценарий date_oddity.sql в онлайн-хранилище кода). create table tl ( dl date, nl number(8), vl varchar2(8) ) insert into tl select dl, to_number(to_char(dl,'yyyymmdd')), to_char(dl,*yyyymmdd') from ( select to_date('31-Dec-1999*) + rownum dl from all_objects where rownum <= 1827 ) В этом примере я сохранил одну и ту же информацию тремя разными спосо- бами. Первый столбец имеет тип «дата» в Oracle. Второй и третий столбцы яв- ляются распространенными вариантами представления дат для приложений, Независимых от базы данных, в которых дата хранится как строка или число в фор- мате ГГГГММДД (год/месяц/день). Как это повлияет на работу оптимизатора и ка- ким образом он справится со следующими простыми запросами? Select * from tl Where dl between to_date('30-Dec-2002','dd-mon-yyyy') and to_date('05-Jan-2003','dd-mon-yyyy') Select * from tl where nl between 20021230 and 20030105
148 Глава б. Вопросы селективности select * from tl where vl between '20021230' and '20030105' Во всех трех случаях Oracle вынужден выполнять полное табличное скани- рование, так как мы не создали индексов, — но сколько строк вернет каждый за- прос по мнению оптимизатора? Запустите каждый запрос с автотрассировкой и проверьте кардинальность. В табл. 6.1 показаны результаты выполнения на нескольких разных версиях Oracle. Таблица 6.1. Использование разных типов данных приводит к разной кардинальности Тип столбца Кардинальность Кардинальность в версии Кардинальность в версии в версии 8.1.7.4 9.2.0.4/10.1.0.2 9.2.0.6 /10.1.0.4 Тип «дата» 9 8 8 Числовой тип 397 396 396 Символьный тип 457 457 396 Нужный столбец с датой выдает результат, близкий к правильному; ошибка обычно возникает в связи с тем, что оптимизатор производит расчеты над не- прерывно изменяющимися значениями, когда в действйтельности мы обраба- тываем относительно небольшой список дискретных значений. Oracle понима- ет значения типа «дата» — что они значат и как над ними производить расчеты. Но что происходит с числовыми и символьными представлениями? В дей- ствительности ничего особенного — мы просто скрыли от оптимизатора, что они содержат даты, так что оптимизатор использовал стандартные расчеты на нестандартном наборе данных. Вспомните стандартную формулу для диапазона — в этом случае использование версии с нестрогими границами (выражение between — это сокращение от 1 >= X и <= Y1). Давайте применим ее для числового типа: Селективность = (требующийся диапазон) / (наибольшее значение - наименьшее значение) + 2 / num_distinct = (20030105 - 20021230) / (20041231 - 20000101) + 2/1827 = 8875/41130 + 2/1827 = 0.215779 + 0.001095 = 0.216874 -- что явно больше 20% данных! Наконец, умножим селективность на количество записей в таблице (1827) и получим 1827 х 0,216874 = 396,228. Оптимизатор не знает, что мы работаем с датами, так что хотя мы видим числа, распознаваемые нами как диапазон дат, пересекающий границы месяца или года, оптимизатор не понимает, что это просто «следующий день», он ви- дит большой пробел между значениями — а мы не дали ему никакой информа- ции о таких пробелах. Все расчеты неизбежно будут неверными. Чтобы проще показать, что значение 457, выведенное для символьных типов в старых версиях, получено из стандартной формулы, я создал функцию PL/ SQL для применения алгоритма конвертации, описанного ранее в этой главе (см. сценарий char_fun.sql в онлайн-хранилище кода). Теперь мне нужно просто выполнить следующую команду SQL, чтобы рассчитать кардинальность:
различные типы данных 149 select round( 1827 * ( 2/1827 + (cbo_char_value('20030105') - cbo_char_value('20021230')) / (cbo_char_value('20041231') - cbo_char_value('20000101')) ),2 ) cardinality from dual CARDINALITY 456.51 Конечно, можно заметить, что кардинальность изменяется от версии 9.2.0.6 К 10.1.0.4 (так что следите за обновлениями версий), и причина изменений до- вольно туманна. Так или иначе, похоже, что Oracle обработал значения столбца varchar2 и значения его предиката как числа. Возможно, что программист в группе разработки стоимостного оптимизатора вставил хитроумный меха- низм работы в оптимизатор для особого случая, как этот. Влияние этого меха- низма исчезает, как только вы отформатируете строку с датой, чтобы она имела Не только числовые символы (например, ' 2002-01-02'). Если производительность вашего приложения страдает от использования неверных типов данных, вы можете применить некоторые опасные ограниче- ния, создав гистограммы для критически важных столбцов. В этом примере чи- словые и символьные столбцы покрывают 60 месяцев, так что графическое Представление данных будет похоже на 60 пиков и 59 провалов. Попробуйте создать гистограмму из 120, 180 или 240 хэш-групп и посмотрите, что произой- дет. Таблица 6.2 показывает результат создания гистограммы со 120 хэш-группами на двух столбцах, имеющих типы, отличные от типа «дата». Улучшение значи- тельное, хотя цифры все еще далеки от идеальных. Небольшое различие между версией 81 и другими версиями вызвано изменениями в генерации гистограмм В Oracle, которые мы рассмотрим в главе 7. Таблица 6.2. Гистограммы могут помочь справиться с проблемами при использовании неверных типов данных Тии столбца Исходные цифры Версия 81 Версия 9i/10g Числовой ТИП 397/398 16 15 Символьный тип 457/397 16 15 Другим способом, который может помочь оптимизатору получить правиль- ное значение селективности, являются индексы на базе функций (function-based indexes, FBI). Конечно, добавление индекса к таблице приведет к дополнитель- ной стоимости ресурсов, которую нужно соответственно учитывать; но если ва- шему коду SQL нужна правильная селективность (а подсказки cardinalityO
150 Глава 6. Вопросы селективности или selectivityO не подходят), то вы можете переписать ваш код, чтобы он соответствовал индексам на базе функций. Например, создайте индекс на столбце to_date (vl,' yyyymmdd'): create index tl_vl on tl(to_date(vlyyyymmdd')); begin dbms_stats.gather_i ndex_stats( ownname => user, indname =>'T1_V1', estimate_percent => null ); end; / begi n dbms_stats.gather_table_stats( ownname => user, tabname =>'T1', cascade => false, estimate_percent => null, method_opt => 'for all hidden columns size 1' ): end; / Я вызвал процедуру gather_index_stats(), после чего вызвал процедуру gather_table_stats(), передав параметру method_opt значение for all hid- den columns. В версии 9i вы можете обратиться к представлению user_ tab_cols, чтобы определить виртуальные столбцы, по которым необходима статистика, вместо обращения ко всем столбцам таблицы. После этого наконец измените код с where vl between '20021203' and '20030105' на where to_date(vl,'yyyymmdd') between to_date('30-Dec-2002','dd-mon-yyyy') and to_date('05-Jan-2003','dd-mon-yyyy') Оптимизатор сможет использовать статистику виртуального столбца, опре- деленного в индексе, чтобы рассчитать правильную кардинальность, даже если план выполнения не использует индекс. Лидирующие нули Одним из других случаев, в которых могут появиться ошибки, связанные с ти- пами данных, является использование синтетических ключей (synthetic keys), также известных как суррогатные ключи, surrogate keys, или бессмысленные идентификаторы, meaningless IDs. Различные проблемы начинают возникать в более сложных системах, когда бессмысленные числа вставляются в данные, и количество проблем возрастает, когда синтетическое значение хранится как символьный столбец фиксированной длины с лидирующими нулями.
Лидирующие нули 151 Например, рассмотрим базу данных, которую я недавно видел: она не имеет нужных первичных ключей, но в ней используется тип char (18) для каждого идентификатора в системе. На момент написания этих строк некоторые из этих последовательностей превысили значение в один миллион, то есть типичное значение равнялось 000000000001000123. Большинство запросов в системе, ис- пользующей эту стратегию, являются вариациями формы where id = {строко- вая константа}, но во время использования предикатов с диапазонами могут возникнуть странные проблемы с производительностью. Сценарий char_seq.sql в онлайн-хранилище кода эмулирует эту ситуацию, создавая таблицу с двумя миллионами строк со столбцом идентификаторов i d, которые генерируются в виде последовательности чисел с лидирующими нуля- ми. Когда я начал писать эту главу первый раз, существовали версии Oracle 9.2.0.4 и 10.1.0.2. Вот что я получил после генерации простой статистики и за- пуска следующего запроса с включенной автотрассировкой: select * From tl where id between '000000000G0G060G00' and '000000GG000G07000G' План выполнения (версия 9.2.0.4) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=lG39 Card=17 Bytes=323) 1 0 TABLE ACCESS (FULL) OF 'Tl' (Cost=lG39 Card=17 Bytes=323) Обратите внимание на очень низкую кардинальность (17 строк) для запро- са, который явно должен вернуть около 10 000 строк (значения могут несколь- ко отличаться для других версий Oracle, но они все равно будут неверны и бу- дут отличаться на два или три порядка). Как и с проблемой использования символьных или числовых типов для дат, я мог уменьшить степень ошибки с помощью создания гистограмм или исполь- зования индексов на базе функций. С гистограммой, состоящей из 75 хэш- групп по умолчанию, план выполнения оценил кардинальность в 8924 — значе- ние, которое, по крайней мере, находится внутри правильного диапазона. ПРИМЕЧАНИЕ По умолчанию в версии 10g во время сбора статистики используется auto_sample_size для генера- ции гистограммы, но в данном случае рассчитанная кардинальность равнялась бы единице! После создания индекса, который включает в виде одного из столбцов выра- жение to_number(id), повторной генерации статистики (без гистограммы) и соответствующего изменения запроса рассчитанная кардинальность прибли- зилась к 10 000 в версиях 9i и 10g (но всего лишь к 5000 в версии 8i). Но к тому времени, как я решил пересмотреть эту главу, я использовал вер- сии 9.2.0.6 и 10.1.0.4, и все изменилось. Ниже показан план выполнения, кото- рый я получил в версии 9.2.0.6:
152 Глава б. Вопросы селективности План выполнения (9.2.0.6) G SELECT STATEMENT Optimizer=ALL_ROWS (Cost=1039 Card=10002 Bytes=220044) 1 0 TABLE ACCESS (FULL) OF 'Tl' (Cost=lG39 Card=lGG02 Bytes=22GG44) Кардинальность стала верной, как по волшебству. И здесь сработал тот же «хитрый» механизм, с которым мы столкнулись во время рассмотрения дат. Насколько я понял, если значения user_tab_columns. low_value, user_ tab_columns. high_value и символьные предикаты выглядят как числа, то оп- тимизатор рассчитывает селективность таким образом, как будто он делает это на столбце с числовым типом данных. Поэтому для демонстрации в версиях 9.2.0.6 и 10.1.0.4 проблем, которые из- начально возникали в более ранних версиях, мне пришлось изменить сценарий char_seq.sql, чтобы значения начинались с символа А. Проблемы со значениями по умолчанию Представьте, что вы собрали важные бухгалтерские данные за пять лет — ска- жем, с 1 января 2000 года по 31 декабря 2004 года — и решаете запустить отчет по всем данным 2003 года. Все запросы, имеющие предикаты where date_ closed between 1 января 2003 года and 31 декабря 2003 года, должны исполь- зовать табличное сканирование, так как они собираются получить около 20 % данных. Вы сохранили даты в столбцах с типом «дата» в Oracle — так где же могут возникнуть ошибки? (Предположим, что в бухгалтерской системе нет пробле- мы излишней поддержки, которая может привести к хранению очень маленько- го количества данных в очень больших таблицах.) Увы, даже если приложения, независимые от баз данных, будут использо- вать типы «дата» в Oracle правильно, они все равно могут попытаться избежать значений NULL. Чтобы не хранить в столбцах значения NULL, каждому такому столбцу будет назначено значение по умолчанию (обычно при помощи кода в клиентском приложении, а не с помощью соответствующей возможности в базе данных). Так что же может выбрать средний разработчик приложений, незави- симых от баз данных, чтобы представить дату со значением NULL? Может, дату далеко в будущем, например 31 декабря 4000 года? Но вспомните, как оптимизатор рассчитывает селективность сканирований по диапазону. Для запросов, которые явно должны выбрать 20 % данных (один год из пяти), оптимизатор рассчитывает гораздо более высокую селективность, чем вы думаете. Выполнив расчет селективности сканирования по диапазону, вы подумаете, что селективность равна (31 декабря 2GG3 года - G1 января 2GG3 года) / (31 декабря 2GG4 года - G1 января 20GG года) + 2/1827 = G.2GG44 Но учитывая дополнительное значение, находящееся далеко за пределами диапазона, оптимизатор рассчитывает селективность следующим образом: (31 декабря 20G3 года - G1 января 2GG3 года) / (31 декабря 4GGG года - G1 января 2GGG года) + 2/1828 = G.GG159
Проблемы со значениями по умолчанию 153 С этим неверным значением селективности (которое приводит к абсолютно неверному значению кардинальности) неудивительно, если оптимизатор выбе- рет неверный путь для прохождения по данным. Всего одна строка с плохим значением по умолчанию превращает статистику в мусор. Решением опять же является создание гистограммы, благодаря чему опти- мизатор сможет увидеть странное распределение данных. Тестовый сценарий (см. defauLts.sql в онлайн-хранилище кода) создает таблицу с примерно ста строками на один день за пять лет, причем каждая тысячная строка имеет зна- чение, равное 31 декабря 4000 года, create table tl as /* With generator as ( select --+ materialize • rownum id from all_objects where rownum <= 2000 ) «/ select /*+ ordered use_nl(v2) */ decode( mod(rownum - 1,1000), 0,to_date(131-Dec-4000'), to_date('01-Jan-2000') + trunc((rownum - 1)/100) ) date_closed from generator vl, generator v2 Where rownum <= 1827 * 100 Посмотрите на значения кардинальности двух планов выполнения, сгенери- рованных для следующего запроса. Первый план является результатом сбора Простой статистики. Второй план получен после генерации гистограммы с 75 хэш- Группами по умолчанию на столбце date_closed: select * from tl Where date_closed between to_date('01-Jan-2003','dd-mon-yyyy') and to_date('31-Dec-2003','dd-mon-yyyy') План выполнения (без гистограммы, версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=46 Card=291 Bytes=2328) J 0 TABLE ACCESS (FULL) OF 'Tl' (Cost=46 Card=291 Bytes=2328) План выполнения (с гистограммой, версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=46 Card=36320 Bytes=290560) 1 0 TABLE ACCESS (FULL) OF 'Tl' (Cost=46 Card=36320 Bytes=290560)
154 Глава 6. Вопросы селективности Обратите внимание, как второй план выполнения с гистограммой показывает рассчитанную кардинальность. Ее значение составляет примерно 36 500 строк, как мы и ожидали для ста строк на один день. Первый план выполнения, в от- личие от второго, имеет слишком малую кардинальность. В более сложных за- просах этот тип ошибки может привести к генерации неверного плана выполне- ния. Опасность дискретных значений Существуют и другие проблемы, связанные со значениями, находящимися да- леко за пределами обычного диапазона. Использование экстремального значе- ния для замены значений NULL не является единственной причиной странного распределения данных, из-за которого генерируются неверные планы выполне- ния. Такой же эффект может возникнуть из-за того, что люди иногда использу- ют необычные значения для представления «специальных событий». Например, рассмотрим бухгалтерскую систему, имеющую столбец периода, в котором хранятся данные с первого периода до двенадцатого периода плюс один период (для корректировок} с номером 99. Сценарий discrete_01.sqL в он- лайн-хранилище кода эмулирует следующий пример (он также включает вто- рой пример, в котором специальный период имеет номер 13). create table tl as with generator as ( select --+ materialize rownum id from all_objects where rownum <= 1000 ) select /*+ ordered use_nl(v2) */ mod(rownum-1,13) period_01, mod(rownum-l,13) period_02 from generator vl, generator v2 where rownum <= 13000 update tl set period_01 = 99, period_02 =13 where period_01 = 0; commi t; Как видите, я создал таблицу с двумя столбцами периодов. Оба эти столбца хранят 13 отдельных значений с 1000 записей на каждое значение. Но один
Опасность дискретных значений 155 столбец использует в качестве периода корректировки число 13 (следующее це- лочисленное значение), а другой — 99. Когда мы запрашиваем все данные из таблицы за второй квартал (с четвер- того по шестой период), мы знаем, что мы хотим увидеть 3000 записей. Из стан- дартной формулы, которую использует Oracle, мы получим неверную карди- нальность, потому что формула выглядит так: num_rows * ( (our_high - our_low) / (table_high - table_low) + 2 / num_distinet) ) Для столбца penod_01 в качестве специального значения используется 99, что дает 13 000 х (2/98 + 2/13) = 2265,306. Для столбца penod_02 в качестве специального значения используется 13, что дает 13 000 х (2/12 + 2/13) = » 4166,667. Но когда мы протестируем запрос на столбце period_01, мы получим сле- дующее: select count(*) from tl where period_01 between 4 and 6 План выполнения (версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=5 Card=l Bytes=3) J 0 SORT (AGGREGATE) 7 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=5 Card=1663 Bytes=4989) Кардинальность, которую Oracle расчитал для столбца с периодом 99, равна 1663, а не 2266, как мы ожидали из расчета по стандартной формуле (карди- нальность, которую Oracle рассчитал для столбца с периодом 13, все же равна 4167, что совпадает со стандартной формулой). Мало того, посмотрите, что про- исходит с четырьмя кварталами года: Fenod_01 between 1 and 3 Penod_01 between 4 and 6 Veriod_01 between 7 and 9 Period_01 between 10 and 12 Кардинальность = 1265 -- Кардинальность = 1663 -- Кардинальность = 1929 -- Кардинальность = 2265 -- это правильно! Вполне возможно, что это изменение кардинальности может изменить наши Планы выполнения без всякой причины, просто в зависимости от времени года, ПО которому мы выполняем запрос (в предположении, что распределение ос- тальных данных осталось таким же). Вы можете попробовать применить какой-нибудь трюк, чтобы обойти эту Проблему. Что произойдет, если вы измените предикат на что-либо идентич- ное — по крайней мере, идентичное на вашем уровне знания данных (см. сцена- рий discrete_02.sql в онлайн хранилище кода)? select count(*) ftom tl Where period_01 > 3 and period_01 < 7 JftaH Выполнения (версия 9 2.0.6) — _ _ _ _ — _ _ — _ — — — — — — — — — — — —
156 Глава б. Вопросы селективности 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=5 Card=l Bytes=3) 1 0 SORT (AGGREGATE) 2 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=5 Card=735 Bytes=2205) Предикат в этом запросе также вернет нужные нам 3000 строк, но только по- тому, что мы знали, что в диапазон входят только числа 4, 5 и 6 (я надеюсь, что есть ограничение, обеспечивающее правильность этого высказывания). Но Ora- cle предсказывает кардинальность, равную 735 (и 4333 для столбца, который использует 13 в качестве корректирующего периода). Со строгими границами диапазона (без равенства) формула выглядит следующим образом: num_rows * ( (our_high - our_low) / (table_high - table_low) ) Это значит, что кардинальность должна быть равна (14 000 х 4/98) = 531, а не 735. Мы видим особый случай (с внутренней ошибкой), который я могу привес- ти в табл. 6.3. Запустив цикл, который проверяет результаты выполнения за- просов за каждые три периода на нашем странном столбце, я могу сгенериро- вать эту таблицу, показывающую предсказанную кардинальность и разницу между текущей и предыдущей строкой в таблице для соответствующей пары предикатов (получено из тестов версии 9.2.0.6 — значения из версии 8i немного отличаются из-за обычных проблем с округлением). Таблица 6.3. Предикаты на основе диапазонов со странным неверным шаблоном Наименьшее Наибольшее Кардинальность Наименьшее Наибольшее Кардинальность значение значение выражения «Between Наименьшее значение and Наибольшее значение» (изменение) в па «s п и значение выражения «> Наименьшее значение and < Наибольшее ЙПСПСПЛС" (изменение) -2 0 1000 -1 1 1000 -2 2 1000 0 2 1133 (+133) -1 3 1000 1 3 1265 (+132) 0 4 1000 2 4 1398 (+133) 1 5 1 3 5 1521 (+133) 2 6 867 (-133 от 1000) 4 6 1663 (+132) 3 7 735 (-132) 5 7 1796 (+133) 4 8 602 (-133) 6 8 1929 (+133) 5 9 531 (-71) 7 9 2061 (+132) 6 10 531 8 10 2194 (+133) 9 11 2265 (+71) 89 91 2265 90 92 2194 (-71) 91 93 2061 (-133) 90 94 531 92 94 1929 (-132) 91 95 531 (+71) 93 95 1796 (-133) 92 96 602 (+133) 94 96 1663 (-133) 93 97 735 (+133)
Опасность дискретных значений 157 Наименьшее Наибольшее Кардинальность Наименьшее Наибольшее Кардинальность Значение значение выражения значение значение выражения «Between «> Наименьшее Наименьшее значение and < значение and Наибольшее Наибольшее значение» значение» (изменение) (изменение) 95 97 1531 (-133) 94 98 867 (+132) 96 98 1398 (-133) 95 99 1 97 99 1265 (-133) 96 1000 (+133 от 867) 98 100 1133 (-132) 97 1000 99 101 1000 (-133) 100 102 1000 Я упорядочил строки так, чтобы в любой из них два набора предикатов име- одинаковый смысл. Например, строка с числами (4, 6, 1663, 3, 7, 735) пред- ставляет результаты следующих предикатов: $here period_01 between 4 and 6 -- кардинальность = 1663 where period_01 > 3 and period_01 < 7 -- кардинальность = 735 Я включил в таблицу только те строки, в которых были изменения в карди- нальности во время перемещения диапазона предикатов по набору данных. Если вы выполните тест и построите такую диаграмму, сразу станут понятны Некоторые детали, которые я суммировал следующим образом. 0 Для большого количества диапазонов между столбцом максимума и столб- цом минимума по стандартной формуле мы получаем рассчитанные карди- нальности, равные 2265 и 531 (конечно, мы знаем, что ни одно из этих значе- ний не совпадает с количеством строк, которое мы получаем на самом деле, но значение по крайней мере согласуется со стандартной формулой). 0 Когда предикат выходит за пределы диапазона значений столбца, карди- нальность опять становится равной num_rows / num__distinct (13 000/13 = , « 1000). О Значения в столбцах «больше чем / меньше чем» для диапазонов от 1 до 5 и от 95 до 99 (две точки, в которых наши диапазоны соприкасаются со столб- цом наименьшего значения или столбцом наибольшего значения, соответст- венно) являются результатом либо выполнения части кода для особых слу- чаев, либо ошибки. Q Все другие значения являются линейной интерполяцией (с добавлением 133 за шаг) между стандартным значением и граничным значением num_rows/ num_d1stinct. О Шаг увеличения кардинальности (133) получается следующим образом: num_ rows / (столбец с наибольшим значением - столбец с наименьшим значени- ем) = 13 000/98. Хорошо, что создание теста с такими понятными результатами является на- столько простым, — но даже с учетом этого нет большого смысла точно знать,
158 Глава 6. Вопросы селективности когда меняются правила расчета или даже что стандартная формула на самом деле более сложна, чем предложенная мною. Возможно, стандартная формула включает компонент, который очень мал почти во всех случаях, но становится значимым на границах и только во время работы с небольшим количеством уникальных значений. Кто сможет разумно объяснить такое странное поведе- ние? Я не знаю, почему мы получили такие результаты, но если рассматривать пользу лично для меня, то я узнал, что в случаях, когда в важном столбце хра- нится небольшое количество уникальных значений, а диапазон значений по сравнению с количеством уникальных значений велик, то предикаты между и больше чем или меньше чем на этом столбце будут работать неправильно, осо- бенно около минимального и максимального значений в столбце. Хорошей новостью является то, что если вы создадите гистограмму на столб- це в примерах (см. сценарии discrete_Ola.sqL и discrete_.02a.sqL в онлайн-хранили- ще кода), то проблемы исчезнут и Oracle будет всегда выдавать правильные ре- зультаты. Изменения в версии 10g В главе 3 я рассказал об изменении (очень специфичном), появившемся в Oracle версии 10.1.0.4, связанным с предикатом столбец = константа, когда известно, что константа выходит за пределы диапазона значений столбца. Тот же эффект присутствует и в запросах с диапазонами — он проявился, когда я выполнил тесты с дискретными значениями еще раз. В результатах, показанных в табл. 6.3, вы видели, что рассчитанная карди- нальность достигает 1000, а потом остается неизменной, когда Oracle пересека- ет минимальную или максимальную границу диапазона в версии 9.2.O.6. Таблица 6.4. Значения кардинальности в версии 10.1.0.4, изменяющиеся вне диапазона столбца Наименьшее Наибольшее Кардинальность Наименьшее Наибольшее Кардинальность значение значение выражения «Between Наименьшее значение and Наибольшее значение» (изменение) значение значение выражения «> Наименьшее значение and < Наибольшее значение» (изменение) 96 98 1398 95 99 1 97 99 1265 96 100 1000 98 100 1133 97 101 1000 99 101 1000 98 102 1000 100 102 990 99 103 1000 101 103 980 100 104 990 102 104 969 101 105 980
Удивительное поведение sysdate 159 В версии 10.1.0.4 во время выхода значения предиката все дальше и дальше границы диапазона кардинальность уменьшается примерно на 10 единиц за одну единицу изменения диапазона предиката. В табл. 6.4 собрана краткая ин- формация о верхней границе диапазона после запуска тестов сценария discrete_ Ql.sqL на базе данных версии 10.1.0.4. Степень изменения представляет собой прямую линию от кардинальности, равной 1000, до единицы — которая будет достигнута во время выхода за грани- цы диапазона на расстояние, равное самому диапазону. Другими словами, из-за гого, что (максимум - минимум) == 98, модель Oracle позволяет данным иметь разброс от (минимум - 98) до (максимум + 98), но чем дальше вы будете от Диапазона, тем меньше данных найдете. Живительное поведение sysdate Jp время простых расчетов селективности вы можете столкнуться с одной из рдмых больших неожиданностей, когда будете использовать один из самых по- пулярных псевдостолбцов в Oracle — sysdate. о В дне 1440 минут, так что если вы запустите следующий код SQL, то полу- чите таблицу, хранящую 4,5 дня в минутах (см. сценарий sysdate_01.sql в он- йайн-хранилище кода). create table tl as select rownum id, trunc(sysdate - 2) + (rownum-l)/1440 minutes, Ipad(rownum,10) small_vc, rpad(’x’,100) padding from all_objects Ле re rownum <= 6480 Запросы с предикатами 1-3 в следующем списке должны возвращать 1440 1441 запись (один день плюс минута), а с предикатами 4-6 должны возвра- щать 2880 или 2881 запись (два дня плюс минута). J) Where minutes between sysdate and sysdate + 1 5) Where minutes between trunc(sysdate) and trunc(sysdate) + 1 S) where minutes between sysdate - 1 and sysdate 4) where minutes between trunc(sysdate) - 1 and trunc(sysdate) 51 where minutes between sysdate - 1 and sysdate + 1 fl where minutes between trunc(sysdate) - 1 and trunc(sysdate) + 1
160 Глава 6. Вопросы селективности Итак, почему же, проверив значения кардинальности из плана выполнения, вы получите значения, показанные в табл. 6.5 (которые будут различаться в за- висимости от времени дня) при выполнении запросов в полдень (12:00 р.щ.) в версии 9i (с учетом обычной разницы в округлениях по сравнению с версией 81)? Таблица 6.5. Выражения, использующие sysdate, ведут себя странно Диапазон Значение кардинальности (версия 9.2.0.6) в полдень 1 Sysdate and sysdate + 1 133 2 Trunc(sysdate) and trunc(sysdate) + 1 180 3 Sysdate -1 and sysdate 180 4 Trunc(sysdate) -1 and trunc(sysdate) 144 5 Sysdate -1 and sysdate + 1 16 6 Trunc(sydate) - 1 and trunc(sysdate) + 1 16 Эти результаты очень неточные; более того, они даже не самосогласованы — например, вы, скорее всего, ожидаете, что значение кардинальности предиката 5 должно быть практически таким же, как сумма значений кард инальностей предика- тов 1 иЗ. Причины просты. Оптимизатор рассматривает sysdate (и trunc(sysdate), и некоторые другие функции с sysdate) во время анализа запроса как извест- ные константы, но sysdate + N является неизвестным значением и рассматри- вается так же, как переменная связывания, что приводит к фиксированному значению селективности, равному 5 % (обратите внимание, что sysdate + 0 вы- даст отличную от sysdate кардинальность). Более того, если оптимизатор не может понять, что такое sysdate + N, он обычно превращает between в комбинацию двух независимых предикатов, по- этому предикат minutes between sysdate - 1 and sysdate + 1 превращается в minutes >= :bindl and minutes <= :bind2 В этом случае значение селективности фиксировано и равно 0,25 %, то есть равно селективности предикатов 5 и 6. Для дальнейшего рассмотрения давайте определим, во что превратится предикат between в примере 1: where minutes >= sysdate and minutes <= {unknown bind value} В таблице у нас хранится 4,5 дня в минутах, из которых два дня являются минутами в будущем (предполагая, что вы запускаете сценарий точно в пол- день), поэтому селективность первого предиката (с его одной нестрогой грани- цей диапазона) равна (необходимый диапазон) / (общий диапазон) + 1 / количество уникальных значений = (2 * 1440) / (4,5 * 1440) + 1/6480 = 0,4445987654321 О Селективность второго предиката равна 0,05 (как у переменной связыва- ния).
Шзультаты использования функций Ahi' , ' ' “.............. "*" " ' 161 <3 Общая селективность равна 0,05 х 0,4445987654321 = 0,02222994. ф Таким образом, кардинальность равна 0,02222994 х 6480 = 144,05, что соот- ' ветствует полученному результату. ? Так что даже когда вы храните информацию с датами правильно, оптимиза- тор может использовать неверные расчеты для получения селективности и кар- динальности. Запросы, получающие данные за «последнюю неделю», «последние сутки» и т. п., являются одними из самых популярных запросов, использующих |фты. Именно в них можно получить большое преимущество от использования Символьных констант вместо переменных связывания или выражений, таких ЩК trunc(sysdate) - 7. Также существуют трудности при обновлении до версии 10g, потому что С этой версии проблема была идентифицирована и исправлена, и все примеры табл. 6.5 возвращают правильную кардинальность. Что же плохого в том, что проблема была исправлена? Спросите себя, сколь- у вас есть правильно работающего кода, потому что оптимизатор недооцени- Йет кардинальность раз в 10 (примеры 2 и 3) или раз в 80 (примеры 5 и 6). Что произойдет с этим кодом, когда оптимизатор неожиданно станет рассчиты- вать правильную кардинальность? Сколько изменится порядков соединений; Сколько операций индексного доступа станут табличными сканированиями; «Только соединений с использованием вложенных циклов станут соединения- ми хэширования (и, возможно, прекратят выполнять исключение секций — partition elimination — в секционированной таблице)? Побочные эффекты от исправления ошибок оптимизатора могут потребовать тщательного тестирова- ния. результаты использования функций В качестве примеров, рассматривавшихся нами до этого момента, были только предикаты на хранимых столбцах, — но что произойдет, если в вашем коде бу- дут такие предикаты: £per(name) like 'SMITHS' d(number_col,10) = 0 pl._sql_func(last_name, first_name) ® 'SMITH_JOHN' « В основном в этих случаях расчеты будут использовать фиксированные про- центы селективности, которые оптимизатор использует для переменных связы- вания, с парой небольших вариаций, как показано в табл. 6.6. Смотрите сцена- рии like _test.sql и fun_sel.sql в онлайн-хранилище кода. Таблица 6.6. Фиксированные проценты селективности символьных выражений димер предиката |«Кйоп(оо1х) = ’SMITH’ Mfunction(colx)«’SMITH' fcnc«on(colx) > ’SMITH’ ftt fiinction(colx) > 'SMITH' Результат Фиксированная селективность, равная 1 % Фиксированная селективность, равная 5 % Фиксированная селективность, равная 5 % Фиксированная селективность, равная 5 % продолжение
162 Глава 6. Вопросы селективности Таблица 6.6 (продолжение') Пример предиката Результат functlon(colx) >= 'SMITH1 and function(colx) <'SMITI" function(colx) between 'SMITHA' and 'SMITH? not function(colx) between 'SMITHA' and 'SMITH? function(colx) like 'SMITH%' Производная селективность, равная 0,25 % (5 % х 5 %) Производная селективность, равная 0,25 % (5 % х 5 %) Производная селективность, равная 9,75 % (5 % + 5 % - - (5 % х 5 %)) Фиксированная селективность, равная 5 % — противоположность очевидному значению 0,25 % not function(colx) like 'SMITH%' function(colx) in ('SMITH','JONES') Фиксированная селективность, равная 5 % Производная селективность, равная 1,99 % (1 % + 1 % - - (1 % х 1 %)). Даже в версии 10g этот расчет селективности входного списка при использовании функции использует неверную формулу из версии 8i Я привел несколько примеров, которые являются сравнениями символов, но расчеты для числового типа и типа «дата» будут такими же. Помните, что если вы создаете индексы на базе функций, вы на самом деле создаете индексы на виртуальных столбцах, и когда вы собираете статистику по таблице и ее индексам, вы можете также собрать статистику и по виртуаль- ным столбцам. В этих случаях такие предикаты, как function(colx) = 0 будут оптимизированы и представлены в виде SYS_NC00005$ = 0 Oracle имеет статистику для столбца SYS_NC00005$ (он виден в представле- нии user_tab_cols в версии 9i и выше), так что к нему применяются обычные расчеты селективности, и предыдущая таблица к нему не имеет отношения (было бы неплохо, если бы корпорация Oracle добавила такую возможность для неиндексированных виртуальных столбцов, чтобы мы могли использовать преимущество правильной статистики для часто используемых выражений без необходимости создания индекса). Коррелированные столбцы До настоящего времени мы рассматривали данные, полученные с помощью ге- нератора случайных чисел, и любые два индексированных столбца были неза- висимы. Мы избежали риска возникновения проблем при использовании зави- симых (коррелированных) столбцов в наших предикатах, и оптимизатор предпо- лагает, что так и должно быть. Когда у вас есть данные, имеющие некоторую статистическую связь между двумя столбцами в одной и той же таблице, могут возникнуть неожиданные результаты. Чтобы разобраться в проблеме, мы начнем с набора данных, который мы ис- пользовали в первом тесте в главе 4.
Коррелированные столбцы 163 greate table tl 4$ select ’ trunc(dbms_random.value(G,25)) nl, -- 25 значений rpad('x',40) trune(dbms_random.value(0,20)) ind_pad, n2, -- 20 значений ) Ipad(rownum,10,> 0') rpad('x',200) from all_objects where rownum <= 1000Q small_vc, padding У нас есть два Столбца, nl и п2, которые содержат 500 разных комбина- ций значений. Перед созданием индекса и статистики на этом наборе дан- ных запустите простую команду update (измененный код находится в сцена- рйй dependentsql в онлайн-хранилище кода): Iodate tl set n2 = cl; Теперь у нас есть набор данных, в индексе которого находится только 25 различных уникальных ключей: f е, 'х ', о) ( 1, 'X ', 1) (24, 'х 24) ’ Изменение данных не сильно повлияло на размер индекса (в котором храни- лось ИИ листовых блоков в исходном примере, а теперь хранится 1107 листо- блоков). Так как мы знаем, что сделали с данными, мы можем рассчитать, адо запрос, показанный ниже, возвратит 400 записей после прохождения при- мерно по 45 листовым блокам индекса (1/25 часть всего индекса) и по большей ЦШИ таблицы. Но посмотрите на результаты автотрассировки: select г /*+ index(tl) */ small_vc from tl Where ind_pad = rpad('X',40) and nl =2 ihd n2 = 2 A План выполнения (версия 9.2.0.6) I SELECT STATEMENT Optimizer=ALL_ROWS (Cost=14 Card=16 Bytes=928) 1 0 TABLE ACCESS (By INDEX ROWID) OF 'Tl' (Cost=14 Card=16 Bytes=928) 2 1 INDEX (RANGE SCAN) OF 'T1_I1' (NON-UNIQUE) (Cost=4 Card=16) ч Согласно плану выполнения, этот запрос возвратит 16 записей, затратив на 1ЙХ 14 операций ввода-вывода! Огромная недооценка стоимости и кардиналь- ИОрти очевидно вызовет проблемы с производительностью. Не только этот про- чий запрос будет работать медленнее или окажется более затратным, чем пред- Жйагает оптимизатор, но если оптимизатор будет искать управляющую
164 Глава б. Вопросы селективности таблицу в соединении с множеством таблиц, эта таблица окажется хорошим претендентом на роль управляющей таблицы в соединении с использованием вложенных циклов, потому что оптимизатор считает, что следующая стадия за- проса будет выполнена 16 раз, а не 400. Этот тип ошибки также работает (или, скорее, вызывает проблемы) и по- другому — посмотрите, что произойдет, когда вы измените выражение where на следующее: where ind_pad = rpad('x',40) and nl =1 and n2 =3 Благодаря нашему знанию данных мы можем сказать, что Oracle должен пройти по индексу всего один раз (по трем блокам), чтобы во время выполне- ния обнаружить, что записей для возврата нет. Но оптимизатор все так же сооб- щает, что в результирующем наборе будет 16 записей со стоимостью, равной 14 операциям ввода-вывода. Переоценка стоимости и кардинальности так же плоха, как и недооценка, потому что из-за них оптимизатор может неправильно выбрать управляющую таблицу. КОРРЕЛИРОВАННЫЕ СТОЛБЦЫ И ДИНАМИЧЕСКАЯ ВЫБОРКА Корреляция между столбцами в одной и той же таблице всегда вызывает проблемы, если оба столб- ца используются в выражении where; и эти проблемы относятся не только к индексам. Иногда мож- но обойти проблему, используя параметр opb'mizer_dynamic_sampling или подсказку dynamic, sampling (они оба появились в версии 91), чтобы Oracle во время выполнения произвел динамиче- скую выборку 32 или более блоков из критически важных таблиц — это позволит увидеть, какая часть записей соответствует вашему выражению where. Одной из очень странных особенностей этой проблемы, когда она появляет- ся в индексах, является то, что база данных хранит правильную Информацию (для некоторых запросов), а оптимизатор игнорирует ее. Если вы посмотрите представление user_indexes для этого индекса, вы обнаружите значения, по- казанные в табл. 6.7. Таблица 6.7. Статистика индекса из тестового примера в сценарии dependentsql Статистика Значение Blevel leaf_blocks distinct_keys clustering-factor avg_leaf_blocks_per_key avg_data_blocks_per_key 2 1107 25- 6153 44 246 Во время прохождения по индексу Oracle подробно проанализировал данные. Он действительно сохранил информацию о том, что существует только 25 уни- кальных ключей. Более того, в представлении также есть информация о том, что
Коррелированные столбцы 165 когда вы выполняете запрос для одного полного ключа, вы пройдете примерно до 44 блокам, чтобы получить идентификаторы записей для этого ключа, а по- том обратитесь к примерно 246 табличным блокам для получения записей — если данные для этого ключа существуют. Ранее мы не рассматривали avg_leaf_blocks_per_key или avg_data_ blocks_per_key, но для индексов на одном или более столбцах, где столбцы Независимы, вы обнаружите, что запросы, использующие условие равенства на всех индексированных столбцах, имеют стоимость, примерно равную blevel + avg_leaf_blocks_per_key + avg_data_t>locks_per_key Это самый простой способ оценки индекса на одном столбце, потому что: О селективность равна 1 / (количество уникальных ключей); О avg_leaf_blocks_per_key рассчитывается как round(leaf_blks / distinct_ ' keys).; ® avg_data_blocks_per_key рассчитывается как round(clustering_factor / distinct_keys). Результат, полученный таким сокращенным способом оценки, а не полным расчетом, не обязательно будет абсолютно точным, потому что (как видно в оп- ределении) хранимые значения рассчитаны с помощью функции round(), в то время как расчеты стоимости используют функцию ceilingO; но полученное значение часто является достаточно близким, чтобы дать вам информацию “О полезности индекса в вашей системе. * Когда вы дойдете до главы И, вы обнаружите, что есть случаи, когда опти- мизатор использует некоторые из этих хранимых значений, а не формулу В, Брейтлинга. Динамическая выборка Давайте попробуем выполнить наш запрос с коррелированными столбцами, вставив в код SQL подсказку dynamic_sampling(), и посмотрим, что произой- дет с планом выполнения (это опять сценарий dependent.sqt): select /*+ index(tl) dynamic_sampling(tl 1) */ small_vc from ti where ind_pad = rpad('x',40) «4 nl =2 and n2 =2 » План выполнения (версии 9i/10g) без динамической выборки ,1 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=14 Card=16 Bytes=928) 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=14 Card=16 Bytes=928) 1 1 INDEX (RANGE SCAN) OF 'T1_I1' (NON-UNIQUE) (Cost=4 Card=16) Лан выполнения (версия 9.2.0.6) с динамической выборкой ^ч»^-1^Л....................................................
166 Глава 6. Вопросы селективности 0 SELECT STATEMENT 0ptimizer=ALL_R0W5 (Cost=14 Card=442 Bytes=25636) 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=14 Card=442 Bytes=25636) 2 1 INDEX (RANGE SCAN) OF 'T1_U' (NON-UNIQUE) (Cost=4 Card=16) План выполнения (версия 10.1.0.4) с динамической выборкой 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=288 Card=393 Bytes=22794) 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (TABLE) (Cost=288 Card=393 Bytes=22794) 2 1 INDEX (RANGE SCAN) OF 'T1_I1' (INDEX) (Cost=46 Card=393) Видно, что есть некоторые различия в оценке значений кардинальности в версиях 9i и 10g, но, по крайней мере, оба эти значения гораздо ближе к 400, чем мы ожидали. Однако более важной особенностью этого примера является то, что 10g вывел подходящую оценку стоимости. Здесь происходит следующее: с помощью подсказки dynamic_sampling(tl 1) мы дали указание оптимизатору, чтобы он выбрал случайный набор записей из набора данных и проверил, какая часть записей соответствует условию запроса. Если вы при этом сгенерируете файл трассировки 10053, то для версии 9.2.0.6 получите следующее (после небольшого форматирования): ** Сгенерированный запрос с динамической выборкой SELECT /*+ ALL_ROWS IGNORE_WHERE_CLAUSE */ NVL(SUM(C1),0), NVL(SUM(C2),0) FROM ( SELECT /*+ IGNORE_WHERE_CLAUSE NOPARALLEL("Tl") */ 1 AS Cl, CASE WHEN "T1"."N1"=2 AND "Tl"."IND_PAD"='X' AND "T1".''N2"=2 THEN 1 ELSE 0 END AS C2 FROM "Tl" SAMPLE BLOCK (8.355795) "Tl" ) SAMPLESUB ** Выполненный запрос с динамической выборкой: level : 1 sample pct. : 8.355795 actual sample size : 837 filtered sample card. : 37 orig. card. : 10000 block ent. : 371 max. sample block ent. : 32 sample block ent. : 31 min. sei. est. : 0.0016 ** Using dynamic sei. est. : 0.04420550 Подсказка была использована, чтобы произвести выборку из определенной таблицы на уровне 1, который содержит 32 блока. Oracle знает из табличной статистики, что размер таблицы равен 371 блоку, то есть размер выборки равен (100 х 32/371) %: к сожалению, это равно 8,625337 %, а мы видим, что Oracle
Коррелированные столбцы 167 использовал 8,355795 % — 31 блок, а не 32, как написано в руководствах (обра- тите внимание, что число 31 появляется в последующем блоке как значение в виде sample block ent). Видно, что код SQL в подставляемом представлении выбирает два значения для подсчета количества во внешней выборке. Первое значение равно 1, так что 5um(cl) просто суммирует записи в выборке. Второе значение содержит выра- жение case, которое возвращает значение 1, если запись соответствует нашему Предикату, и 0, если не соответствует — таким образом, sum(c2) подсчитывает количество записей, удовлетворяющих условию нашего предиката. f На странице итогов мы видим, что Oracle рассмотрел 837 записей и нашел 37 соответствий, то есть 0,04420550. Oracle использовал это значение как селек- тивность нашего предиката (возможно, после сравнения его с исходным теоре- тическим значением селективности, показанным здесь как min sei. est.). ' К сожалению, хотя Oracle использовал эту селективность для расчета карди- нальности (10 000 х 0,04420550 = 442), он не использовал ее для расчета стои- мости. < Oracle версии 10g ведет себя очень похоже, хотя итоговых результатов не- много больше. Результаты включены в сценарий dependent.sql, но важным зна- чением здесь является dynami с sei. est., которое равно 0,03928171. Это приво- дит к значению кардинальности, равному 393 (умножьте на 10 000 и округлите), Которое может потом использоваться для расчета стоимости стандартным спо- собом: Стоимость = blevel + ©eiling(leaf_blocks * эффективная селективность индекса) + celling(clustering_factor * эффективная селективность таблицы) = 2 + ceiling(l 107 * G.03928171) + Ceiling(6 153 * 0.G3928171) = t + 44 + 242 = 288 Здесь нужно заметить, что версия 10.1.0.4 ведет себя не так, как версия 10.1.0.2, которая выводит следующее в примере с уровнем 2: ** Dynamic sampling initial checks returning TRUE (level = 2). ** Dynamic sampling index access candidate : T1_I1 SELECT /* OPT_DYN_SAMP */ /*+ ALL_ROWS NO PARALLEL(SAMPLESUB) NO_PARALLEL_INDEX(SAMPLESUB) */ NVL(SUM(C1),G),~NVL(SUM(C2),0), NVL(SUM(C3),G) ROM ( SELECT /*+ NO_PARALLEL("T1") INDEXC'Tl" T1_I1) NO_PARALLEL INDEXC'Tl") »/ 1 AS Cl, 1 AS C2, 1 AS C3 FROM "Tl" "Tl" WHERE "T1"."N1"=2 AND "Tl".''IND_PAD"=' x AND ”T1"."N2"=2 AND ROWNUM <= 25GG ) SAMPLESUB
168 Глава 6. Вопросы селективности Этот подход интересен, так как в нем проверяется только статистика индек- са, а таблица не проверяется. Возможно, этот подход был изменен или улучшен, но для моего примера он теперь неприменим. Я считаю, что еще есть потенциал для дальнейшей разработки динамической выборки, — и людей, работающих с ней, ожидает еще множество сюрпризов. Профили оптимизатора Если вы использовали версию 10g, то вы, вероятно, проводили некоторые экс- перименты с SQL Tuning Advisor и обнаруживали, что иногда совет состоял в использовании профиля. SQL Tuning Advisor можно настроить таким образом, чтобы он тратил много времени на анализ команды и выяснял, как можно уско- рить ее выполнение (этот процесс я иногда называю оптимизацией в автоном- ном режиме, offline optimization). Одним из возможных действий, применяю- щихся в аналитическом процессе, является детальное статистическое изучение текущих данных, запрашивание базовой таблицы и тестирование частичных со- единений с динамической выборкой. Конечно, вам не потребуется делать этого, но если вам будет дан совет ис- пользовать профиль и вы обратите внимание на идентификатор задачи оптими- зации, которая дала этот совет, то можете понять, что входит в этот профиль, посмотрев содержание таблиц wr 1 $. В частности, следующий запрос может вер- нуть некоторые интересные результаты: select attrl from wri$_adv_rationale where task_id = &&m_task • ATTR1 OPT.ESTIMATE (@"SEL$1" . JOIN, ("T2"@"SEL$1", "T1”@''SEL$1''), SCALE_R0WS=15) OPT_ESTIMATE(@"SEL$1", TABLE, "T2"@"SEL$1". SCALE_ROWS=200) OPTIMIZER_FEATURES_ENABLE(default) Выбрано три строки. Профили являются всего лишь наборами хранимых подсказок, которые пре- доставляют дополнительную информацию Oracle во время оптимизации. И хотя вы не должны этого делать, вы можете вставлять такие подсказки в ко- нечный код SQL. В следующем примере я убрал двойные кавычки и ссылки query block (этого примера нет в онлайн-хранилище кода). select /*+ OPT_ESTIMATE(TABLE, Т2, SCALE_ROWS=200) OPT_ESTIMATE(JOIN, (T2, Tl), SCALE_R0WS=15) */ count(tl.vl) ct_vl, count(t2.vl) ct_v2 from tl, t2 where t2.n2 = 15 and t2.nl = 15
Переходное замкнутое выражение 169 and tl.n2 = t2.n2 + 0 and tl.nl = t2.nl С помощью этих подсказок Oracle дается информация, что единовременное обращение к таблице Т2 возвратит в 200 раз больше записей, чем показывает Хранимая статистика, и что соединение таблиц Т2 и Т1 возвратит в 15 раз боль- ше записей, чем ожидалось (однако заметьте, что это внутренние, недокументи- рованные подсказки, и методы их использования могут меняться, так что если бы я был на вашем месте, я бы не использовал их в рабочем коде, но если хоти- те, можете время от времени с ними экспериментировать). Если у вас есть проблемы с зависимыми столбцами, вам могут помочь про- фили, давая возможность Oracle получить некоторую важную информацию о сложном распределении данных и его возможном влиянии. ВНИМАНИЕ Мои эксперименты с подсказкой opt_estimate показали изменения в функциональности в разновид- ностях версии 10.1. Не пытайтесь использовать эту подсказку в рабочей системе, пока она не будет документирована для общего использования. Переходное замкнутое выражение Одной из проблем, которая может возникнуть при работе с оптимизатором, мо- щт быть излишняя сложность его кода. Когда план выполнения кажется не со- всем правильным и вы пытаетесь выяснить, где была неверно рассчитана селек- тивность, помните, что могут существовать некоторые предикаты, которые вы Ж указывали и которые вы даже не можете увидеть (если только не используе- те последнюю версию плана выполнения). Оптимизатор может использовать механизм под названием переходное замк- нутое выражение (transitive closure) для генерации нескольких предикатов, ко- торые будут видны только при использовании нужного инструмента, показы- вающего весь план выполнения. Переходное замкнутое выражение вмешивается в логику работы. Предполо- жим, у вас есть два предиката (см. сценарий trans_dose_01.sql в онлайн-храни- Дище кода): М « 100 and n2 = nl » атом случае оптимизатор может создать предикат Й • 100 И включить его в расчеты. Но здесь есть ловушка. Поскольку в этом предикате Имеется константа, оптимизатор может убрать предикат без константы, так что в результате выражение where будет выглядеть следующим образом: Itt » 100 «nd п2 - 100
170 Глава 6. Вопросы селективности В некоторых случаях это может быть полезным, но может и вызвать странные побочные эффекты. Рассмотрим следующий пример (сценарий trans_close_02. sql в онлайн-хранилище кода): create table tl as select mod(rownum,10) nl, mod(rownum,10) n2, to_char(rownum) small_vc, rpad('x',100) padding from all_objects where rownum <= 1000 create table t2 as select * from tl; Сбор статистики с помощью dbms_stats select count(*) from tl, t2 where tl.nl = 5 and t2.nl = tl.nl Структура таблиц tl и t2 идентична. Они имеют по 1000 записей, и в столбце nl хранится по 100 копий каждого из значений от 1 до 10. Когда мы выполняем запрос, мы должны выполнить соединение 100 записей из tl со 100 записями из t2 для вывода 10 000 записей. Ниже показан план выполнения, который по- лучен с помощью dbms_xplan.display() в версии 9.2.0.6: 1 Id Operation | Name Rows Bytes Cost | 1 0 SELECT STATEMENT 1 1 6 404 | 1 1 SORT AGGREGATE 1 1 6 1 1 2 MERGE JOIN CARTESIAN 1 10000 60000 404 | 1 * 3 TABLE ACCESS FULL 1 Tl 100 300 4 1 1 4 BUFFER SORT 1 100 300 400 | 1 * 5 TABLE ACCESS FULL 1 T2 100 300 4 1 Predicate Information (identified by operation id): 3 - filter("Tl" .''Nl"=5) 5 - filter("T2"."Nl"=5) Кардинальность верна, но посмотрите на стоимость. Обратите внимание на соединение слияния с декартовым произведением в строке 2 и на то, что сорти- ровка буфера в строке 4 получила значение стоимости, равное 400 (что очень похоже на стоимость стократного сканирования таблицы t2 — по разу на каж- дую строку в таблице tl). Рассматривая информацию о предикатах, вы не обна-
Переходное замкнутое выражение 171 ружите предиката, выполняющего соединение двух таблиц, — предикат соеди- нения исчезает, когда создается второй предикат с константой. Но если вы примените алгоритм замкнутого выражения нужным образом, план изменится (обратите внимание, что значение стоимости теперь выглядит корректным, а значение кардинальности — столбец Rows — теперь очень мало): 1 Id | Operation | Name| Rows Bytes Cost | 1 0 | SELECT STATEMENT 1 1 1 6 9 1 1 1 | SORT AGGREGATE 1 1 1 6 1 |* 2 | HASH JOIN 1 1 1000 6000 9 1 ♦ 3 | TABLE ACCESS FULL 1 Tl | 100 300 4 1 I* 4 | TABLE ACCESS FULL 1 T2 | 100 300 4 1 Predicate Information (identified by operation id): 2 - access("T2"."N1"="T1"."Nl") > - filter("Tl"."Nl"=5) 4 - filter("T2"."Nl"=5) Такого эффекта можно достичь двумя способами: О явно добавить предикат t2.nl = 5; О добавить копию предиката t2.nl = tl.nl. Третьим (нежелательным) способом является старый трюк на основе пра- вил — изменение предиката соединения Hat2.nl = tl.nl + 0. К сожалению, ко- гда вы будете использовать этот наихудший способ, оптимизатор рассчитает правильную кардинальность соединения (10 000) и практически верную стои- мость (9). Так что будьте осторожны с соединениями, приводящими к странным пла- нам выполнения и значениям кардинальности; вам понадобится их исправить (и задокументировать эти изменения для дальнейших обновлений версии), что- бы оптимизатор стал правильно их обрабатывать. Конечно, проще всего рассматривать переходное замкнутое выражение для знаков равенства, но оптимизатор может оказаться умнее. В более общем слу- чае оптимизатор может сделать такое предположение: если nl оператор кон- станта и п2 = nl, то и п2 оператор константа. Например: если nl < 10 и п2 = “ nl, то п2 < 10. Также, если coll > со12 и со12 > {константа К}, то Oracle может использо- вать предикат coll > {константа К}. Вы даже можете столкнуться с одной из таких ситуаций: если nl > 10ип1 < < 0, то 0 > 10. Это выражение будет всегда принимать значение false и, таким образом, может закрыть целую ветвь плана выполнения. Предикаты даже могут быть сгенерированы из ограничений (несколько примеров вы можете посмот- реть в сценарии trans_close_03.sql в онлайн-хранилище кода). Рассмотрение переходных замкнутых выражений с более общими предика- тами (такими, как nl < 10) без соединений указывает на небольшую ошибку. Механизм не является самосогласованным. Посмотрите на табл. 6.8.
172 Глава 6. Вопросы селективности Таблица 6.8. Переходное замкнутое выражение не согласовано на все 100 % Ваши предикаты Предикаты, которые использует Oracfe после применения механизма переходных замкнутых выражений nl = 5 and nl = п2 nl < 5 and nl = п2 nl between 4 and 6 and nl = п2 nl = 5 and п2 = 5 nl < 5 and п2 < 5 and nl = п2 п! >« 4 and nl <= 6 and п2 >= 4 and п2 <= 6 and nl = п2 Конечно, нужно оставлять условие соединения, когда условие не относится к соединению, не содержит проверки на равенство, иначе можно получить не- верные результаты. Также является правильным (в общем случае) убирать условие на равенство из соединения — по крайней мере, из-за расчетов карди- нальности, даже если это может помешать генерации эффективного плана вы- полнения. Во втором выпуске версии 10g вы обнаружите новый скрытый пара- метр, который позволяет вам определить, сохраняется ли условие соедине- ния, — по умолчанию оно может сохраниться, при этом опять меняются расчеты кардинальности. Помощь (или проблема) находится под рукой, хотя и в форме недокументи- рованного влияния неподходящего параметра. Вспомните параметр query_ rewri te_enabled тех времен, когда вам нужно было устанавливать для него значение true, чтобы работали индексы на базе функций. В последних версиях Oracle этот параметр больше не влияет на индексы на основе функций. Однако, если вы установите для параметра query_rewrite_enabled значение true, то правила для переходных замкнутых выражений меняются, пока вы не станете использовать версию 10g. Запустите еще раз мой запрос из сценария trans_dose_02.sql в версии 8i или 91, установив для этого параметра значение true. Полученные план выполне- ния, стоимость и кардинальность совпадают с планом из первых двух предло- женных мною изменений (что, конечно, означает, что полученная кардиналь- ность неверна). Предикаты, сгенерированные из ограничений Вот небольшая загадка: у меня есть таблица со столбцом епате, объявленным как varchar2 (30). Я создаю простой (без функций) индекс со структурой би- нарного дерева на этом столбце (и собираю по этому индексу статистику): create index tl_il on tl(ename); Пользователь выполняет следующий запрос: select * from tl where upper(ename) = 'SMITH': Возможно ли для оптимизатора эффективно использовать простой индекс (со строго определенным сканированием по диапазону) для получения пра- вильных данных?
Предикаты, сгенерированные из ограничений 173 Ответом будет «да» — но только начиная с версии 9i и только с опреде- ленными ограничениями (см. сценарий constraint_01.sql в онлайн-хранилище кода). Create table tl ( id number, vl varchar2(40) not null, constraint tl_ck_vl check (vl=upper(vl)) ); begin dbms_random.seed(0); for n in 1..10000 loop insert into tl (id, vl) values (n, dbms_random.string('U', 30)): end loop; end; / create index tl_il on tl(vl); -- не индекс на базе функций (FBI) Сбор статистики с помощью dbms_stats select * from tl where upper(vl) = 'SMITH' f Секрет заложен в ограничениях. Оптимизатор загрузил ограничения в па- мять, применил их в запросе, а потом использовал переходные замкнутые выра- жения для создания новых предикатов. Поэтому vl = upper(vl) -- ограничение upper(vl) = 'SMITH' -- сам предикат Означает Where vl = 'SMITH' -- Замкнутое выражение, может использоваться индекс Как и с обычными примерами переходных замкнутых выражений из преды- дущего раздела, вы можете увидеть эти сгенерированные предикаты в полном Плане выполнения или из d bm s_x р I а п. В этой функциональности есть несколько небольших странностей и ошибок. В ранних версиях 9i и 10g механизм мог выводить неправильные результаты й некоторых особых случаях, но, я думаю, ошибки были исправлены в версиях 9.2.0.6 и 10.1.0.4. В версии 10g все еще остается необычное ограничение (кото- рого не было в версии 9i) — тестовый пример в сценарии constraint_01.sql не ис- пользует индекс для следующего типа предиката: Where upper(vl) = :bind_var Конечно, возможно, что поведение версии 9i вызвано ошибкой, а в версии 10g она исправлена (я не знаю, почему это так работает в данном случае, но иногда эта особенность исчезает, потому что она становится логически небезопасной
174 Глава 6. Вопросы селективности для реализации — в эту категорию часто попадают риски побочных эффектов из-за значений NULL). Проблема с неверными результатами была связана с ограничениями check, включающими встроенные функции, которые могут вернуть значение NULL для переданного значения столбца, не равного NULL. Обратите внимание, что мое объявление столбца включает ограничение NOT NULL (странно, что оно должно объявляться на уровне столбца, а не как ограничение check на уровне табли- цы). Если у вас не получилось сделать это, есть некоторые классы ограничений, при которых механизм замкнутых выражений для предикатов просто не срабо- тает, хотя вы и можете попытаться использовать его, включив явно выражение NOT NULL в ваш запрос. Вы также можете обнаружить, что этот механизм не бу- дет работать, если ограничения объявлены как отложенные {deferrable). Механизм настолько гибок, что он может быть причиной неожиданных эф- фектов, помещая сгенерированные предикаты туда, куда вы не ожидали. Рас- смотрим следующий пример (см. сценарий constraint_02.sql в онлайн-хранили- ще кода): create table tl as select trunc((rownum-l)/15) nl, trunc((rownum-l)/15) n2, rpad(rownum,215) vl from all_objects where rownum <= 3000 create table t2 as select mod(rownum,200) nl, mod(rownum,200) n2, rpad(rownum,215) vl from all_objects where rownum <= 3000 create index t_il on tl(nl); create Index t_i2 on t2(nl); alter table t2 add constraint t2_ck_nl check (nl between 0 and 199); Сбор статистики с помощью dbms_stats select count(tl.vl) ct_vl, count(t2.vl) ct_v2 from tl, t2 where t2.n2 = 15 and tl.n2 = t2.n2 and tl.nl = t2.nl
Предикаты, сгенерированные из ограничений 175 Мы можем предположить, что оптимизатор мог совместить два первых пре- диката в этом запросе для генерации предиката tl.n2 = 15 (удалив предикат tl. n2 = t2. п2), что он и сделал. Но посмотрите, что еще произойдет, когда мы запустим этот запрос через dbms.xplan. I Id I Operation Name| Rows 1 Bytes | Cost | I 0 1 SELECT STATEMENT 1 1 | 444 | 33 | 1 1 1 SORT AGGREGATE 1 1 | 444 | 1 1* 2 | HASH JOIN 1 15 | 6660 | 33 | I* 3 | TABLE ACCESS FULL Tl I 15 | 3330 | 16 1 I* 4 | TABLE ACCESS FULL T2 | 15 I 3330 | 16 1 Predicate Information (identified by operation id): 2 - access("Tl"."N1"="T2"."Nl") 3 - filter("Tl"."N2"=15 AND "T1"."N1">=O AND "Tl"."Nl"<=199) 4 - filter(”T2"."N2”=15) На самом деле мы потеряли один предикат соединения и получили другой Предикат с константой, но посмотрите внимательно на предикат фильтра й третьей строке: он содержит проверку диапазона, которая являлась проверкой ограничения check в таблице t2, но эта проверка применяется к таблице tl. Из-за равенства tl.nlHt2.nl оптимизатор может увидеть, что строки из таб- лицы 11, которые могут участвовать в соединении, должны соответствовать ог- раничению check таблицы 12, так что он перенес текст ограничения в предикат, Относящийся к таблице 11, так как это позволяет выполнить более точный рас- пет кардинальности соединения (если таблица tl уже имеет такое ограничение, to предикат создан не будет). ОГРАНИЧЕНИЯ, ПРЕДИКАТЫ И ДИНАМИЧЕСКАЯ ВЫБОРКА Первый раз я столкнулся с ограничением на одной таблице, которое становится предикатом на дру- гой, когда готовил презентацию по динамической выборке. Если значение параметра optimizer, dynamic.sampling равно 4 или больше, оптимизатор запросит динамическую выборку из любой таб- лицы с двумя или более предикатами, относящимися к одной таблице, перед генерацией полного плана выполнения. Когда я первый раз тестировал результаты динамической выборки, я написал запрос с двумя табли- цами, каждая из которых имела только один предикат, относящийся к одной таблице, — и Oracle удивил меня, выполнив динамическую выборку одной из таблиц. Механизм переходных замкнутых выражений добавил к одной из таблиц два дополнительных предиката. Oracle часто так делает — когда вы разбираетесь с одной особенностью, неожиданно может проявиться другая, что приведет К Путанице. Вы можете подумать, что механизм со временем усложняется. Два примера, показанные ранее, продемонстрировали случаи, где было простое ограничение На одном столбце. Более сложный пример демонстрирует сценарий constraint. O3.sql в онлайн-хранилище кода, в котором Oracle использует ограничение Уровня таблицы вида check (n2 >=nl), чтобы создать ограничение, позволяю- щее запросу изменить свой план выполнения с табличного сканирования на ин- дексный доступ.
176 Глава 6. Вопросы селективности Заключение Во время расчета кардинальности оптимизатор может сделать что-нибудь не- ожиданное, но на это есть достаточные основания. Если вы знаете, почему мо- гут возникнуть ошибки в работе оптимизатора, у вас есть шанс исправить их. Критическими участками являются следующие. О Оптимизатор не очень хорошо справляется со сканированиями по диапазо- ну применительно к символьным строкам. Если в вашем столбце очень не- равномерное распределение среди первых шести или семи символов, то оп- тимизатор будет довольно часто неверно оценивать селективность (и карди- нальность). Быстрого и простого решения этой проблемы не существует, но может помочь гистограмма с множеством хэш-групп. о Хранение данных в неверном типе данных, особенно хранение дат в число- вых или символьных столбцах или превращение последовательности чисел в строки с лидирующими нулями, может вызвать проблемы. Оптимизатор может получить более точные оценки селективности, если вы сможете соз- дать гистограмму с достаточно большим количеством Хэш-групп. Также вы можете обойти некоторые проблемы с помощью создания индексов на базе функций, в которых данные возвращаются с правильными типами, если су- меете соответственно поменять код SQL. о Приложения, использующие в столбцах специальные значения вместо зна- чений NULL, могут приводить к созданию неэффективных планов выполне- ния, если столбцы используются в предикатах на основе диапазонов. Опять же, гистограмма на важном столбце может привести к гораздо более эффек- тивному плану выполнения. о Применение функций к столбцам, не участвующим в индексах на базе функ- ций, может привести к неожиданным значениям селективности. Вы даже мо- жете столкнуться с тем, что выполнение одного и того же выражения по-раз- ному может привести к значительным изменениям в селективности из-за не- которой несогласованности фиксированных констант, которые используются для селективности в этих случаях. О Механизм переходных замкнутых выражений может создавать и удалять для вас предикаты, и обычно эффект бывает положительным. Но побочные эффекты могут оказаться катастрофическими или просто неожиданными. И хо- тя иногда настойчиво советуют избегать ограничений ссылочной целостно- сти в хранилищах данных из-за дополнительных затрат ресурсов, помните, что ограничения могут помочь оптимизатору найти лучший план выполнения. И не удивляйтесь, когда Oracle берет ограничение, превращает его в преди- кат, а затем, с помощью механизма переходных замкнутых выражений, пере- носит его на другую таблицу. Тестовые сценарии Файлы к этой главе, доступные для загрузки, перечислены в табл. 6.9.
Тестовые сценарии 177 Таблица 6.9. Тестовые сценарии к главе 6 сценарий Комментарии 4ar_types.sql Простой сценарий, показывающий числовое представление символьных строк nchar_types.sql date_oddity.sql charjun.sql Предыдущий сценарий с многобайтовым набором символов Демонстрация проблем с кардинальностью из-за неверных типов данных Создает простую функцию, рассчитывающую числовое значение, которое используется оптимизатором как числовое представление строки char„seq.sql defaults.sql Проблемы с лидирующими нулями строк для представления последовательности чисел Демонстрация использования неэффективного значения по умолчанию вместо значения NULL discrete_01.sql Демонстрация эффекта использования дискретных значений в выражении between sysdateJH.sql Демонстрация ошибки в расчете кардинальности, связанной с типом sysdate like_test.sql p_sel.sql dependent.sql transj:lose_01.sql trans_close_02.sql Символьный столбец типа 'ХХХ%' Селективность для функции (colx) Демонстрация результатов использования зависимых столбцов в предикате Простой пример переходного замкнутого выражения Автотрассировка странного побочного эффекта переходных замкнутых выражений trans_close_03.sql constraint-Ol.sql ^nstraint_02.sql &nstraint_03.sql Автотрассировка замкнутого выражения с ">" Использование ограничений для генерации эффективных предикатов Неожиданное поведение предикатов на основе ограничений Демонстрация того, что могут помочьдаже ограничения уровня таблицы между столбцами discrete_02.sql Демонстрация результатов использования дискретных значений в выражениях «больше чем» или «меньше чем» discreteJHa.sql discrete_02a.sql char_value.sql То же самое, что discrete_01.sql, с гистограммой для решения проблемы То же самое, что discrete_02.sql, с гистограммой для решения проблемы Создание функции, возвращающей числовой эквивалент входного параметра типа varchar2() trans_close_02a.sql trans_dose_03a.sql |gt«v.sql То же самое, что trans_close_02.sql, с использованием dbms_xplan То же самое, что trans_close_03.sql, с использованием dbms_xplan Установка стандартизированной тестовой среды для SQL*Plus
Г истограммы В Интернете есть много неверной информации о гистограммах', что они делают, как они выглядят, как они работают и когда они не работают. Также среди до- ступной информации есть много пробелов. В этой главе я постараюсь заполнить некоторые из этих пробелов и испра- вить некоторые ошибки в понимании гистограмм. К сожалению, трудно ска- зать, насколько это заполнение пробелов и исправление ошибок будет успеш- ным, потому что недавно на MetaLink я увидел ошибку 2 757 360 от января 2003 года, которая включала замечательный комментарий: «Количество оши- бок в стоимостном оптимизаторе и гистограммах абсолютно неизвестно...» Можно задать множество вопросов о гистограммах, но самыми важными, скорее всего, являются следующие: что такое гистограммы, как Oracle исполь- зует их, когда Oracle использует их, когда Oracle их игнорирует, какие пробле- мы они решают, какие проблемы они привносят и как построить полезную гис- тограмму, если Oracle этого не делает? Ответы на все эти вопросы содержатся в этой главе. Введение Всем известно, что гистограммы предназначены для случаев, когда в столбце есть несколько определенных значений, которые встречаются гораздо чаще, чем все остальные; также всем известно, что гистограммы можно создавать только на индексированных столбцах. Эта информация настолько неверна, что лучше для начала ее вообще проигнорировать и начать объяснение гистограмм с при- мера, в котором нет очень часто встречающихся значений и индексов. Тестовый сценарий в онлайн-хранилище кода называется histjntro.sql, и, как обычно, в моей демонстрационной среде используются блоки размером 8 Кбайт, ло- кально управляемые табличные пространства с экстентом размером в 1 Мбайт и ручное управление размером сегмента; системная статистика (CPU costing) отключена. execute dbms_random,seed(0) create table tl as
Введение 179 With kilo_row as ( select /*+ materialize */ rownum from all_objects where rownum <= 1000 select trunc(7000 * dbms_random.normal) normal from kilo_row kl, kilo_row k2 Where rownum <= 1000000 A Обратите внимание, что этот код включает механизм выноса подзапроса (subquery factoring mechanisin'), добавленный в версии 9i, поэтому этот код не будет работать в версии 8i. Результатом работы сценария будет таблица, содер- жащая 1 000 000 случайных значений с нормальным распределением. Для гене- рации данных был использован пакет dbms_random, и первая строка кода, вызов seed (), очень важна для получения повторяемых примеров. Этот тестовый сценарий должен генерировать 42 117 различных значений от 432 003 до 34 660. Так как в таблице 1 000 000 строк, то на одно значение будет р Среднем 24 записи. Конечно, если проверить некоторые значения, то обнару- жится, что это среднее значение достаточно обманчиво. Например, три записи щеют значение -18 000, только одна запись имеет значение +18 000 и 109 за- рисей имеют значение 0. Конечно, у тех, кто знает колоколообразную кривую нормального распределе- ния, такое распределение результатов не вызовет удивления. Чтобы подчерк- нуть это распределение, можно выбрать результаты следующим запросом отобразить их графически: select normal, count(*) ct from tl group by normal; Графическое представление распределения будет выглядеть примерно так, Мак показано на рис. 7.1. Вместо того чтобы выводить 42 000 отдельных точек, я отсортировал набор ^щных по порядку, разделил его на десять блоков по 100 000 записей (кто зна- ум с квартилями и процентилями, узнает этот метод), после чего вывел прямо- ^ОЛьник для каждого из десяти блоков (или децилей — decile, как они еще назы- ваются). Это можно сделать с помощью простого SQL-кода, который я покажу мйа этапа: «rtect normal, ntile(10) over (order by normal) tenth from tl На первом шаге я использую выражение over О аналитической функции НПе() для сортировки данных по порядку и разделения отсортированного Шиска на десять секций одинакового размера — по 100 000 записей в каждой, функция nt ile О расширяет каждую запись, добавляя новый столбец, содержащий
180 Глава 7. Гистограммы Значение столбца 23,000 16,000 25,600 Рис. 7.1. Графическое представление распределения данных номер секции, которой принадлежит запись. В данном случае для этого Столбца было указано имя tenth. Если бы была нужна более точная картина распределе- ния данных, можно было бы просто сделать передаваемое в функцию ntile() значение больше 10. Разобравшись, как использовать функцию ntile() для сортировки и сек- ционирования моих данных, на следующем шаге мы обернули начальный за- прос в подставляемое представление и сгенерировали наименьшее и наиболь- шее значения для каждой секции, что позволило определить ширину и высоту прямоугольников в графическом представлении, select tenth min(normal) max(normal) max(normal) - min(normal) round(100000 / (max(normal) - min(normal)),2) from ( select normal, ntile(10) over (order by normal) tenth from tl ) group order by tenth by tenth tenth, low_val, high_val, width, height Ширина каждого прямоугольника определяется границами децилей, высота каждого прямоугольника равна (100 000 / ширина), что приводит к усреднению распределения строк по доступному диапазону в децили и обеспечивает одина- ковую площадь у всех прямоугольников. Результаты показаны ниже: TENTH L0W_VAL HIGH_VAL WIDTH HEIGHT 1 -32003 -B966 23037 4.34 2 -B966 -5BB3 30B3 32.44 3 -5BB3 -3659 2224 44.96 4 -3659 -1761 1B9B 52.69
Введение 181 5 -1761 17 177В 56.24 6 17 1792 1775 56.34 7 1792 367В 1ВВ6 53.02 8 367В 5В97 2219 45.07 9 5В97 В974 3077 32.50 10 В974 34660 256В6 З.В9 > Выберите любое значение от -32 003 до -8966 (первая десятка), и высота прямоугольника этой десятки покажет, что в таблице немного одинаковых за- писей (высота приблизительно равна 4). Для значений от 8974 до 34 660 (деся- ти десятка) также будет немного одинаковых записей. Большинство данных ^рбрано в средней части графического представления. Фактически 80 % данных J8 прямоугольников из 10) сконцентрированы всего лишь в 27 % от общего диапазона значений (от -8966 до +8974). Рассмотрим теперь один важный вопрос. Если бы это был ваш рабочий на- данных и вы часто запрашивали его данные, вы бы хотели, чтобы Oracle вел Ия так, будто все запросы обращаются к данным на противоположных концах Набора данных или к сгруппированным в середине набора? Или у разных поль- зователей были бы разные требования, покрывающие весь диапазон значений? Помните, что это не технологический вопрос, а вопрос бизнеса. Если ваш бизнес заинтересован только в значениях в середине диапазона, ^ргда вы, вероятно, захотите, чтобы Oracle считал, что существует около 45 за- писей для любого значения. Если же ваш бизнес всегда заинтересован в край- них значениях, тогда вы захотите, чтобы Oracle считал, что существует около 4 даписей для любого значения. И что же будет делать Oracle, если ваши запросы выполняются по всему набору данных? Помните, что на каждом шаге плана выполнения важно правильно рассчи- тывать кардинальность, потому что кардинальность в любой точке плана может влиять на порядок соединений, методы выполнения соединений и выбор ин- дексов. Но этот набор данных показывает, что правильная оценка кардинально- сти для простого условия равенства может зависеть в большей степени от целей бизнеса, чем от самих данных. „ Если графическое представление данных не является гладким, то тогда именно бизнес-требование определяет, какая часть графического представле- ния является важной; таким образом, оно также определяет и «правильную» кардинальность. Каждый раз, когда вы строите графическое представление ва- данных и находите в них пики, провалы или что-либо не плоское и не глад- Ж вы можете столкнуться с проблемами, возникающими при определении оп- ^(МИзатором нужной кардинальности для большинства ваших запросов. А, как знаем, неверные оценки кардинальности приводят к неверным планам вы- полнения. К Перед завершением этого раздела я покажу вам альтернативный метод полу- чения значений, требующихся для построения моего графического представле- ния. Но сейчас давайте на некоторое время вернемся к гистограммам. Начнем «^Создания гистограммы из десяти прямоугольников (или групп — buckets, как М называют в Oracle) по столбцу normal:
182 Глава 7. Гистограммы begin dbms_stats.gather_table_stats( user, 'tl', cascade => true, estimate_percent => null, method_opt => 'for columns normal size 10' ); end; / Затем мы выполняем запрос на представлении user_tab_hi stograms для получения хранящихся в нем 11 значений (чтобы вывести N прямоугольников, нам понадобится N + 1 точка). select rownum prev curr curr - prev round(100000 / (curr - prev) , 2) from ( select endpoint_value lag(endpoint_value,1) over ( order by endpoint_number ) from user_tab_hi stograms where table_name = 'Tl' and column_name = 'NORMAL' ) where prev is not null order by curr tenth, low_val, high_val, width, height curr, prev Здесь я опять использовал аналитическую функцию, на этот раз lag(), ко- торая позволяет нам помещать значения из предыдущих строк в текущую стро- ку, если мы используем соответствующий порядок сортировки в выражении over (). В этом случае я делаю задержку данных на одну запись, чтобы найти разницу (значение в текущей записи минус значение в предыдущей записи) ме- жду соседними записями в представлении user_tab_hi stograms. Имея разности между значением в текущей записи и в предыдущей, я могу завершить мою SQL-команду — и когда я ее выполню, она выведет точно такие же значения, какие я получил при выполнении моего исходного запроса к набо- ру данных. Я говорил раньше, что покажу другой способ получения значений для вывода моего графического представления — это он и есть, элегантный, бы- стрый запрос к представлению user_tab_hi stograms. Удивительное совпадение? На самом деле нет, потому что начиная с версии 9i вы можете включить трассировку SQL-кода при использовании пакета d bm s_s tats
^едение 183 ддя генерации гистограммы и обнаружить, что пакет выполняет следующий gQL-код: select min(minbkt), maxbkt, substrb(dump(min(val),16,0,32),1,120) mi nval, substrb(dump(max(val),16,0,32),1,120) maxval, sum(rep) sumrep, sum(repsq) sumrepsq, max(rep) maxrep, count(*) bktndv, sum(case when rep=l then 1 else 0 end) unqrep Wbffl ( select val, min(bkt) minbkt, max(bkt) maxbkt, count(val) rep, count(val) * count(val) repsq from ( select /*+ cursor_sharing_exact dynamic_sampling(0) no__moni tori ng */ "NORMAL" val, ntile(10) over(order by "NORMAL") bkt from "TEST_USER”."Tl" t Where "NORMAL" is not null ) group by val ) group by maxbkt order by maxbkt Посмотрите очень внимательно на внутреннюю часть встроенных представ- фИИЙ. Обратите внимание на строку, в которой находится ntile(10). Не счи- рЖ некоторых незначительных изменений, некоторых дополнительных значе- ^й, использующихся для работы в граничных условиях, и не считая значения ^>r_tab_columns.density, SQL-код, генерирующий значения, хранящиеся j|Mfer_tab_hi stograms, работает так же, как мой исходный SQL-код, выводя- щий графическое представление. Гистограмма является всего лишь изображе- Шем вашего набора данных. Посмотрите на графическое представление еще раз и скажите себе: «Корпо- рация Oracle называет такие графические представления сбалансированными по Касоте гистограммами (height balanced histograms)». Если у вас когда-либо воз- миЖали проблемы с пониманием идеи сбалансированных по высоте гистограмм
184 Глава 7. Гистограммы в Oracle, теперь вы знаете почему... потому что они не являются «сбалансиро- ванными по высоте», они всего лишь являются обычными гистограммами, о ко- торых, вероятно, большинство людей узнали в возрасте 12 лет и больше нико- гда не вспоминали. СБАЛАНСИРОВАННЫЕ ПО ВЫСОТЕ ГИСТОГРАММЫ Когда я искал термины «height balanced» (сбалансированный по высоте) и «histogram» (гистограм- ма) в поисковой системе Google, каждая найденная ссылка указывала на Oracle. Только после под- сказки Вольфганга Брейтлинга я начал искать термины «equl-depth» (равная глубина) и «histogram» (гистограмма) и обнаружил, что гистограммы, описываемые в Oracle как сбалансированные по вы- соте, обычно называются гистограммами равной глубины (equl-depth) или иногда гистограммами равной высоты (eqiii-height) — в обычной литературе. Ни один из терминов не дает мне интуитивного понимания того, как графическое представление выводит данные, так что я избегаю выражения «сбалансированные по высоте» и просто применяю термин «гистограммы». Общие гистограммы Oracle использует гистограммы для улучшения расчетов селективности и кар- динальности при неоднородных распределениях данных. Но вы на самом деле можете использовать два разных подхода: один для наборов данных с неболь- шим количеством уникальных значений (меньше 255), а другой — для наборов данных с большим количеством уникальных значений. Oracle называет первый подход частотной гистограммой (frequency histogram), хотя технически она скорее должна быть кумулятивной частотной гистограммой, а второй подход — сбалансированной по высоте гистограммой (хотя, как вы видели ранее, высоты несбалансированы с точки зрения любого человека, не занимающегося матема- тикой). Хотя эти два типа гистограмм имеют свои особенности, существует множе- ство общих областей их применения, которые я хочу рассмотреть в этом разде- ле. На самом простом уровне, вне зависимости от типа, гистограмма является просто коллекцией пар чисел (хранящихся в таких представлениях, как user_ tab_histograms, user_part_histograms и user_subpart_histograms), кото- рые могут использоваться для вывода графических представлений данных. Од- нако во время сбора данных для гистограммы Oracle также собирает некоторую дополнительную информацию, которую он использует для расчетов изменен- ной плотности значений столбца. После генерации гистограммы для столбца вы обнаружите, что значение плотности, выведенное в user_tab_columns (и других представлениях), больше не равно 1 / num_distinct. Во время вычисления селективности и кардинальности Oracle может ис- пользовать полную информацию о гистограмме, но иногда он вынужден ис- пользовать только плотность (density). Это может стать причиной странного поведения, так что остальная часть этого раздела является кратким описанием особенностей, которые мешают эффективному использованию гистограмм.
Общие гистограммы 185 {^стограммы и переменные связывания 3 Предыдущих главах мы видели, что оптимизатор запросов использует плот- ность в качестве селективности предикатов вида столбец = константа или столбец = :bind_vartable. Поэтому при наличии гистограммы селективность простого предиката равенства изменяется даже для запроса, использующего пе- ременную связывания (bind variable). Если вы когда-либо слышали, что гисто- граммы теряют свою значимость при использовании переменных связывания (без считывания значений), то это не совсем верно — подробная информация 0 гистограмме не может использоваться, но влияние значения плотности может |ыть очень важным. К сожалению, как я отметил ранее, если на графическом представлении есть область, которая представляет для вас интерес, вам может потребоваться изменить плотность, рассчитанную Oracle, на плотность, важную ^я бизнеса. Считывание значений переменных связывания Шнечно, путаница возросла, когда в версии 9i появилось считывание значений переменных связывания (bind variable peeking). Каждый раз во время оптимиза- ции команды Oracle (в большинстве случаев) проверяет текущее значение каж- дой переменной связывания и оптимизирует команду под эти определенные |Шдения. Шализ И ОПТИМИЗАЦИЯ Когда оператор SQL выполняется в первый раз, он должен быть проверен на синтаксис, интерпре- тирован и оптимизирован. После этого, если тот же текст повторно выполняется в базе данных, он может быть распознан как уже использованный, и в этом случае может быть найден и повторно ис- пользован уже существующий план выполнения. Однако, даже когда оператор еще находится в памяти, некоторая информация о плане выполнения может стать неактуальной или может быть удалена из памяти стандартным механизмом управления памятью, основанным на удалении страниц, которые дольше всего не использовались (last recently used, LRU). Когда это происходит, оператор должен быть повторно оптимизирован (вы можете от- следить это в столбцах, отражающих загрузки и неактуальности в v$sql, суммированных в столбцы загрузок и неактуальности в v$librarycache. Повторные загрузки возникают при отсутствии инфор- мации в памяти; неактуальности возникают при изменении некоторой информации, от которой за- ЙЙит план выполнения). Повторное использование (или совместное использование) SQL-кода в общем случае является эф- фективным, но если план выполнения, сгенерированный во время первого запуска оператора, был Основан на неудачном наборе значений, то каждое последующее выполнение этого оператора при- ведет к тому же неудачному плану выполнения, пока вы не удалите оператор из памяти — возмож- но, специальным методом очистки разделяемого пула. Шйтмаание значений переменных связывания имеет нежелательный побочный эффект, потому что любой пользователь может создать план выполнения, который будет плох для других пользовате- fi, и этот план выполнения будет использоваться, пока он не будет удален из разделяемого пула W или иным причинам. Конечно, этот механизм довольно хорошо работает в системах оперативной ^работки транзакций (OLTP-системах), потому что такие системы постоянно Шюльзуют один и тот же небольшой набор запросов, которые выбирают дан- ные не по диапазону значений, а по конкретным значениям. Скорее всего, каждое
186 Глава 7. Гистограммы выполнение данной команды требует одного и того же плана выполнения. Хотя возможно, что во время первой оптимизации команды через переменные связыва- ния будет передан необычный набор значений. Если такое произойдет, оптимиза- тор может создать план выполнения, эффективный для этого набора значений, но неэффективный для других, более распространенных случаев использования команды. Параметр cursor_sharing Другой особенностью, относящейся к переменным связывания, является ис- пользование параметра cursor_sharing. Стоимость анализа и оптимизации на- столько велика и настолько уменьшает масштабируемость, что корпорация Oracle создала параметр cursor_shar1ng, чтобы дать разработчикам возмож- ность работать с курсорами общего доступа (sharable cursors). Когда значение параметра cursor_sharing установлено в состояние прину- дительной замены, оптимизатор будет заменять почти все константы в вашем SQL-коде (и в некотором PL/SQL-коде) сгенерированными системой перемен- ными связывания и проверять, есть ли заранее сгенерированный курсор общего доступа, который может использоваться для этой измененной команды. Из-за использования переменных связывания cursor_sharing оказывает большое влияние на эффективность гистограмм (конечно, после конвертирования сим- вольных констант в переменные связывания оптимизатор запросов в Oracle версии 9i подставляет в команду текущие значения, если ему нужно оптимизи- ровать эту команду). CURSOR-SHARING И PL/SQL В большинстве случаев Oracle будет заменять константы переменными связывания со специальны- ми именами, такими как :SYS_B_O; однако если вы напишете код, который вызывает неименован- ные блоки PL/SQL-кода, используя старый синтаксис begin имя_процедуры (...)end;, то Oracle не подставит значения переменных связывания. Так как блоки PL/SQL-кода не требуют оптимизации (хотя они по-прежнему требуют подготовленных для них областей курсоров), то дополнительная стоимость, вызванная таким поведением механизма, скорее всего, вас устроит. Если вы можете сконвертировать ваш код в более новый синтаксис call имя_процедуры(...), то Oracle выполнит замену значений переменными связывания. Однако заметьте, что некоторые ста- рые версии Oracle имеют ошибку, которая приводит к прерыванию сессии, если вы при вызове одно- временно указываете и константы и настоящие переменные связывания в вызове, а потом включае- те cursor sharing. В версии 9i появилось два альтернативных способа обойти проблемы, поро- ждаемые установкой cursor_shar1 ng=force. Простым способом является ука- зание подсказки /*+ cursor_sharing_exact */, которая может быть добавлена в команду, чтобы Oracle знал, что в команде не должно быть констант, заменен- ных на переменные связывания. Более тонким и опасным способом является использование значения сиг- sor_sharing=similar. С таким значением cursor_sharing Oracle сначала за- менит константы переменными связывания, а затем будет решать, нужно ли ему считать значения переменных связывания, чтобы он мог оптимизировать входные значения при каждом следующем вызове команды.
Общие гистограммы 187 В описании этой особенности в руководстве к версии 9.2 говорится, что Oracle будет повторно оптимизировать команду, если значения переменных при- ведут к изменениям в плане выполнения. Похоже, что на выполнение этой по- вторной оптимизации влияют две причины: во-первых, если какой-либо из пре- дикатов использует сканирование по диапазону, и, во-вторых, даже с простым равенством, если на столбце, который есть в предикате, имеется гистограмма, tp запрос будет повторно оптимизирован (для демонстрации этого случая см. Сценарий similar.sql в онлайн-хранилище кода). Когда это происходит, увеличивается количество ресурсов, требующееся на оптимизацию, — так же, как и конкурентный доступ, потому что Oracle перепи- сывает запрос под использование переменных связывания, решает, что запрос це должен быть в совместном использовании, и вставляет его в библиотечный дал как новый дочерний курсор в v$sql (где большое количество копий одина- кового текста с подставленными значениями скорее всего будет собираться при выполнении одной и той же внутренней блокировки). ',> В итоге можно сказать, что если вам действительно нужно установить сиг- sor_sharing=simiТаг, то убедитесь, что вы не создадите больше гистограмм, чем вам действительно нужно, иначе количество проблем с производительно- |ГЫо может превысить количество проблем, от которых вы избавитесь (на са- мом деле вам следует всегда избегать создания гистограмм, в которых вы в дей- ствительности не нуждаетесь, — установка cursor_sharing=similar только обостряет проблему). ХРАНИМЫЕ СХЕМЫ ПЛАНА ВЫПОЛНЕНИЯ, ПЛАН ВЫПОЛНЕНИЯ И CURSOR-SHARING При создании хранимых схем плана выполнения (stored outlines) или использовании плана выполне- ния (explain plan), когда значение cursor-sharing равно force или similar, вас может поджидать не- большая ловушка. Когда вы выполняете explain plan for {команда sql} или create outline for {команда sql), Oracle не заменяет константы на переменные связывания. Вместо этого он создает план выпол- нения на основе текущих значений. Результатом является то, что план выполнения, который вы видите, не обязательно будет использо- ван при выполнении запроса. Более того, в случае с хранимыми схемами плана выполнения SQL-код ^Текущими значениями хранится в таблице outln.ol$. Если вы захотите выполнить исходную коман- она будет изменена во время выполнения на использование переменных связывания. Это зна- что SQL-код во время выполнения не будет совпадать с SQL-кодом, хранящимся в таблице |ЙЙП.о1$, так что хранимые схемы плана выполнения даже не будут использоваться в SQL-коде, ко- ||рый их сгенерировал. Вероятно, поэтому на MetaLink были однажды заявления, что хранимые схе- мы плана выполнения могут не работать с включенным значением cursor_sharing. ||кая же проблема присутствует и в первых выпусках версии 10g при создании профиля (profile), но «думаю, что эта проблема была исправлена во втором выпуске версии 10g. Й|гда Oracle игнорирует гистограммы Генерация гистограмм является довольно затратной операцией, так что вы должны хорошо подумать перед ее выполнением. Oracle не очень интенсивно «СПользует гистограммы и очень часто не получает от них никаких преиму- Wctb. На самом деле есть несколько случаев, когда Oracle игнорирует гисто- ваммы, в то время как вы можете думать, что они используются.
188 Глава 7. Гистограммы Гистограммы и соединения Oracle полностью использует гистограммы только в том случае, когда сущест- вуют явно видимые входные значения — другими словами, в случах, где у вас есть предикаты вида столбец оператор константа (хотя константа может быть получена из переменной связывания). Подумайте об этом, и вы поймете, что Oracle может оказаться не в состоянии извлечь какую-либо пользу из информа- ции гистограммы, чтобы эта информация помогла ему оптимизировать соеди- нения (но по этому поводу обратитесь к главе 10). Рассмотрим следующий про- стой запрос: select tl.vl, t2.vl from tl, t2 where tl.n2 = 99 and tl.nl = t2.nl г Даже если у вас есть гистограммы на столбцах tl.nlHt2.nl, как Oracle мо- жет узнать информацию о диапазоне и частоте значений в столбцах соединения для записей, в которых tl. п2 равно 99? С другой стороны, помните, что создание гистограммы на столбце повлияет на сохраненную плотность этого столбца; так что если расчеты селективности или кардинальности соединения используют плотность, то гистограммы на столбцах соединения могут оказать некоторое влияние. К сожалению, как вы увидите в главе 10, похоже, расчет селективности соединения использует num_ distinct в Oracle версий 8i и 9i и переключается на использование плотности только в версии 10g — это значит, что вы можете столкнуться с некоторыми не- приятными сюрпризами во время обновления версий, даже если ничего не изме- нялось] ГИСТОГРАММЫ СОЕДИНЕНИЙ Я видел пару статей, в которых предлагалось создание гистограмм на столбцах, входящих в первич- ные и внешние ключи. К сожалению, статьи не содержали никакого обоснования этого предложе- ния, а также не предлагали объяснения, как это должно помочь. На самом деле соединения первич- ных и внешних ключей являются единственными местами, где оптимизатор запросов действительно хорошо рассчитывает правильную кардинальность соединения без какой-то помощи — создавать гистограммы здесь не обязательно. Возможно, что авторы имели в виду особый случай соединения, когда первичный ключ родительской таблицы присутствует в предикате с константой (которая, как вы видели в главе б, будет переадресо- вана столбцам внешнего ключа дочерней таблицы с помощью переходного замкнутого выражения). Однако в некоторых случаях гистограммы приводят к существенным изменениям (как вы увидите в главе 10) — но только при определенных условиях, в которых первичные и внешние ключи обыч- но не встречаются. Гистограммы и распределенные запросы В Oracle продолжает улучшаться обработка распределенных запросов — но даже в Oracle версии 10g оптимизатор не пытается получить гистограммы с удален-
Общие гистограммы 189 ных серверов для улучшения плана выполнения, и не только из-за игнорирова- ния гистограмм на столбцах соединений. Даже когда у вас есть столбцы из уда- ленной базы данных, которые сравниваются с константами, Oracle не пытается Использовать гистограмму. Он снова начинает использовать num_distinct (или плотность, в зависимости от версии). Например, рассмотрим следующие два за- проса (их можно найти в сценарии dist_hist.sql в онлайн-хранилище кода): select home.skew2, way. skew2, home.padding, away.padding from tl home, tl@d920@loopback away Where home.skew = 5 and away.skew = 5 and home.skew2 = away.skew2 select /*+ driving_site(away) */ home.skew2, away.skew2, home.padding, away.padding Йот tl home, tl@d920@loopback away ‘Where home.skew = 5 tnd away.skew = 5 and home.skew2 = away.skew2 I В этом примере я создал циклическую ссылку на базу данных (loopback data- base link) для эмуляции распределенного запроса, а затем выполнил соединение таблицы с собой же. Столбец skew, присутствующий в предикате skew = 5, |>4еет очень асимметричное распределение данных, и на нем построена гисто- рамма. Когда я генерирую план выполнения для запроса: ^tlect count(*) from tl where skew = 5; Oracle рассчитывает кардинальность как равную 22, если существует гисто- грамма, и 41 без нее. На самом деле что бы я ни делал с локальными или уда- Кйными запросами (например, на одном и том же сервере), Oracle всегда рас- считывает кардинальность равной 22, если есть гистограмма. Однако при создании распределенного запроса Oracle теряет информацию о гистограмме, если таблица находится не на управляющем сервере (driving site). моем примере независимо от сервера, на котором выполняется запрос, план выполнения выглядел следующим образом:
190 Глава 7. Гистограммы План выполнения (версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=33 Card=28 Bytes=9296) 1 0 HASH JOIN (Cost=33 Card=28 Bytes=9296) 2 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=16 Card=22 Bytes=4488) 3 1 REMOTE* (Cost=16 Card=41 Bytes=5248) D920.JLCOMP.CO.UK@LOOPBACK Мы запрашиваем таблицу tl двумя различными способами, но с одним и тем же предикатом. Заметьте, как кардинальность локального запроса отлича- ется от кардинальности удаленного запроса, которая (в зависимости от управ- ляющего сервера) выводится в другом столбце plan_table, как показано ниже: SELECT "SKEW","SKEW2”,"PADDING" FROM "Tl" "AWAY" WHERE "SKEW"=5 SELECT "SKEW","SKEW2","PADDING" FROM "Tl" "A2" WHERE "SKEW"=5 Конечно, если вы посмотрите данные представления v$sql_plan на удален- ном сервере после выполнения запроса, вы обнаружите, что удаленный сервер использовал гистограмму для расчета кардинальности входящего запроса. Но уже слишком поздно: общий план выполнения уже был сгенерирован, и сервер, создавший его, мог выбрать неверный порядок соединений или метод соедине- ний, потому что не имел верной информации о кардинальности удаленной таб- лицы. Частотные гистограммы Я упомянул ранее, что существуют два различных типа гистограмм, которые в Oracle называются частотными и сбалансированными по высоте гистограмма- ми. Сначала мы рассмотрим частотную гистограмму, так как она гораздо проще. В сценарии c_skew_freq.sql в онлайн-хранилище кода создается столбец skew, объявленный так, что значение 1 встречается один раз, значение 2 встречается два раза, и т. д. до значения 80, которое встречается 80 раз, что в сумме дает 3240 записей. Когда мы создаем частотную гистограмму на этой таблице и запрашиваем представление user_tab_hi stograms, мы видим следующее: begi п dbms_stats.gather_table_stats( user, 'tl' , cascade => true, estimate_percent => null, method_opt => 'for all columns size 120' ); end; / select endpoi nt_number, endpoint_value from user_tab_histograms where column_name = 'SKEW' and table_name = 'Tl'
Частотные гистограммы 191 order by endpoi nt_number endpoint_numberendpoint_value 11 32 ез 104 .155 I16G79 324080 .. Это не типичный результат из user_tab_hi stograms, который мы увидим далее в этом разделе во время рассмотрения сбалансированных по высоте гис- тограмм. В значении endpoint_value повторяется значение из таблицы, а в endpoi nt_numbeг выводится количество записей в таблице, в которых значе- ние столбца skew меньше или равно этому значению. Пройдя по списку, мы мо- жем увидеть следующее. Ь Существует одна запись со значением 1 или меньше. р Существует три записи со значением 2 или меньше. 0 Существует шесть записей со значением 3 или меньше. О Существует десять записей со значением 4 или меньше. И так далее. Альтернативным способом просмотра значений является про- смотр разницы значений столбца endpoint_number. Этот способ является вер- 'щ>1М, так как частотная гистограмма показывает, что в таблице присутствует не- большое количество уникальных значений. Таким образом: <9 Существует одна запись со значением 1. О Существует три минус одна = две записи со значением 2. О Существует шесть минус три = три записи со значением 3. О Существует десять минус шесть = четыре записи со значением 4. И так далее. На самом деле, учитывая то, как мы сравниваем текущее коли- Штво записей с предыдущим для определения количества записей для задан- ного значения, мы можем написать простую команду SQL с использованием ^алитической функции для превращения частотной гистограммы из user_ tab_hi stograms обратно в список значений в нашей таблице: select endpoint_value row_value, curr_num - nvl(prev_num,0) row_count frbm ( select endpoint_value, endpoint_number curr_num, lag(endpoint_number,l) over ( order by endpoint_number ) prev_num from
192 Глава 7. Гистограммы user_tab_histograms where column_name = 'SKEW' and table_name = 'Tl' ) order by endpoint_value Вы могли заметить нечто необычное в вызове dbms_stats, который я ис- пользовал для создания частотной гистограммы. Хотя в столбце хранится толь- ко 80 уникальных значений, а в результирующей гистограмме хранится 80 строк, я указал Oracle сгенерировать гистограмму со 120 группами. С dbms_stats и частотными гистограммами может возникнуть проблема. С версии 9i Oracle создает гистограммы с помощью SQL-кода, который я при- вел в конце раздела «Введение». К сожалению, этот SQL-код практически ни- когда не сможет сгенерировать частотную гистограмму, если вы запросите аб- солютно точное количество групп. Я обнаружил: чтобы Oracle создал частотную гистограмму, необходимую для моих 80 значений, мне нужно запросить создание 107 групп. Это шаг назад по сравнению со старой командой analyze, которая построила бы частотную гистограмму, если бы вы задали правильное количество групп. В частности, это означает: при наличии более 180 уникальных значений в столбце вы можете об- наружить, что не можете построить частотную гистограмму на нем, если не ис- пользуете команду analyze или же механизм, который я продемонстрирую в конце этого раздела (насколько я знаю, эта проблема была исправлена в вы- пуске 2 версии 10g). Имея гистограмму, нам нужно проверить, что произойдет в разных случаях, в основном проверяя рассчитанную кардинальность с помощью нашей визуаль- ной оценки для разных типов запроса (сценарий c_skew_freq_01.sql в онлайн- хранилище кода, результаты получены в версии 9.2.0.6, но результаты в версии 8i отличаются только на единицу в нескольких значениях), как показано в табл. 7.1. Таблица 7.1. Расчеты стоимостного оптимизатора по сравнению с оценкой человека Предикат Описание Стоимостный ’ оптимизатор 4еловек skew = 40 Столбец = константа 40 40 skew = 40.5 Столбец = несуществующее значение, но входящее в диапазон 1 0 skew between 21 and 24 Диапазон between по значениям внутри границ 90 90 skew between 20.5 and 24.5 Диапазон between по значениям внутри границ 90 90 skew between 1 and 2 Диапазон between по граничным значениям 3 3 skew between 79 and 80 Диапазон between по граничным значениям 159 159 skew > 4 and skew < 8 Диапазон больше чем/меньше чем 18 18 skew = -10 Меньше наименьшего значения 1 0 _
Частотные гистограммы 193 Предикат Описание Стоимостный Человек оптимизатор Skew = 100 Больше наибольшего значения 1 0 skew between -5 and -3 Диапазон меньше наименьшего значения 1 0 Skew between 92 and 94 Диапазон больше наибольшего значения 1 0 skew between 79 and 82 Диапазон, пересекающий границу 159 159 skew = :bl Столбец = :blnd 41 ??? skew between :bl and :Ь2 Столбец between :bindl and :bind2 8 ??? Как видите, любой запрос, использующий константы, дает правильный резуль- тат — с предположением, что вы принимаете единицу за правильный результат работы оптимизатора, когда знаете, что данных на самом деле нет. НУЛЕВАЯ КАРДИНАЛЬНОСТЬ В общем случае оптимизатор не разрешает использовать 0 в расчетах кардинальности. Даже если рассчитанная кардинальность равна 0, оптимизатор ведет себя осторожно и рассматривает ее как равную 1. В этом правиле есть как минимум одно исключение. В случае, когда предикат является логическим противоречием, таким как 1 = 0, кардинальность будет нулевой. Часто эти предикаты являются Внутренне сгенерированными предикатами, которые позволяют оптимизатору исключать во время Выполнения целые ветви планов выполнения. В двух примерах с переменными связывания мы видим, что стоимостный оптимизатор использовал стандартное значение 0,25 % для диапазона, а прове- рив column = : bi nd, мы обнаружим, что значение 41 получено из num_rows / iwm_di sti net. Наконец, проанализировав значения плотности, мы получим (и подтвердим Из нескольких тестовых наборов данных), что плотность = 1 / (2 х num_ ;'rbws) — это и является причиной того, почему мы перестаем видеть кардиналь- ность, равную единице, когда выходим за диапазон наименьшего/наибольшего Значений или запрашиваем значение, которого нет в гистограмме. Есть одна деталь, которую вы также можете заметить, тщательно изучив гис- тограммы. В версии 10g количество групп частотной гистограммы (user_ tab_columns. num_buckets) совпадает с количеством уникальных значений . Я таблице. Для более ранних версий Oracle это количество слишком мало. Это ’ изменение коснулось всего лишь определения представления, которое не по- зволяло использовать частотные гистограммы в более ранних версиях. Очевидно, что частотные гистограммы являются эффективным инструмен- том — но при их создании или использовании нужно помнить о некоторых осо- бенностях. О Вы можете использовать как значение 254 (максимально возможное), так и количество групп при сборе статистики — Oracle все равно сохраняет только нужное количество записей, так что нет риска потери количества групп, которое будет критичным. О Если важные значения в ваших данных меняются, вам нужно перестроить гистограмму — иначе у Oracle будет устаревшее представление данных.
194 Глава 7. Гистограммы Обратите внимание, что для значения 40,5 кардинальность получилась рав- ной единице: если мы изменим все значения 40 на 40,5, то Oracle все равно будет выводить skew = 40 для 40 записей. о Частотные гистограммы не влияют на выражения с переменными связыва- ния (если значения этих переменных не считаны). Селективность предиката column = : bi nd также равна 1 / num_di sti net, селективность диапазонов равна 5 % и 0,25 % для неограниченных и ограниченных диапазонов. о При наличии гистограмм стоимостный оптимизатор лучше определяет, что предикаты диапазона выходят за пределы наименыпего/наиболыпего значе- ний. Изменение частотных гистограмм Я упомянул, что dbms_stats требует большего количества групп, чем команда analyze, для создания частотной гистограммы. Так что же вы можете сделать для генерации гистограммы, если в вашей таблице 254 уникальных значения, кроме использования команды analyze? Посмотрите внимательно на процедуры prepare_column_values, get_ column_stats и set_column_stats в dbms_stats. В сценарии c_skew_freq_02. sql в онлайн-хранилище кода приводится пример использования этих процедур для создания на столбце частотной гистограммы, которая не может быть сгене- рирована при обычном вызове dbms_stats. Ключевыми моментами являются следующие вызовы: select skew, count(*) bulk collect into m_val_array, m_statrec.bkvals from tl group by skew order by skew m_statrec.epc := m_val_array.count; В этом коде мы собираем значения и количества из столбца в пару значений типа varray и сохраняем размер массива для передачи в вызов ргераге_ column_values: dbms_stats.prepare_column_values( srec => m_statrec, numvals => m_val_array ): Этот код также считывает статистику столбца в локальные переменные, из- меняет переменные, а потом записывает их обратно в словарь данных с помо-
Частотные гистограммы 195 щью get_column_stats и set_column_stats для чтения и записи значений, со- ответственно. К сожалению, set_coiumn_stats при вызове не знает, что мы создаем час- тотную гистограмму, поэтому плотность не меняется — таким образом, нам нужно добавить еще немного кода для расчета правильного значения перед вы- зовом set_column_stats. Последняя мысль — о частотных гистограммах и простоте, с которой вы мо- жете их создать. Хотя я бы советовал осторожно обращаться со статистикой 3 словаре данных, процедуры доступны и их можно использовать. У вас есть больше информации о ваших данных и о способах их использования, чем у оп- тимизатора запросов. Разумным будет добавить некоторые цифры, являющиеся большим приближением к реальности по сравнению с теми, которые рассчиты- вает оптимизатор запросов. Например, предположим, что у вас есть таблица с 1 000 000 записей, но пользователям интересны только 100 000 из этих записей, которые включают определенный столбец, содержащий только 25 различных значений. Абсолютно разумным подходом здесь может быть создание частотной диаграммы, которая использует запрос только по этим 100 000 записям и устанавливает статистику столбца таким образом, чтобы представить остальные 900 000 записей как хра- нящие значения NULL. Другой пример: если у вас есть столбец, в котором хранится 400 различных значений, Oracle сможет создать только сбалансированную по высоте диаграм- му. Этот способ может оказаться слишком грубым для ваших целей. Вместо этого вы можете попробовать создать частотную диаграмму, описывающую 254 наи- более часто встречающихся значения. Предупреждение для тех, кто будет изменять частотные диаграммы Странно, что оптимизатор может использовать различные планы выполнения Только если вы изменили статистику. Также существуют различные планы вы- полнения, которые используются только в конце сложного дерева решений — Например, «В этом месте мы используем l/num_rows, но если X < Y и в другой таблице соединения строк более чем в 100 раз больше, чем в этой таблице, то Мы используем плотность». Так что вам нужно быть очень внимательными С процедурами типа set_xxx_stats(). Вы пытаетесь помочь Oracle, правильно Описывая ваши данные, но ваше описание должно быть согласованным, иначе Могут возникнуть различные неожиданности. Сценарий fake_hist.sql в онлайн-хранилище кода показывает, что может пой- ти «не так» — конечно, вы можете сделать это намеренно. Самой важной осо- бенностью, которую нужно помнить, является то, что, хотя гистограмма и опи- сывает ваши данные в виде списка с указанием, сколько записей хранится для каждого значения, оптимизатор скорректирует эти значения количества запи- сей в соответствии с другой важной информацией о таблице.
196 Глава 7. Гистограммы Максимальное кумулятивное значение в частотной гистограмме, которое находится в столбце endpoi nt_number, должно совпадать с количеством запи- сей в таблице минус количество значений NULL в столбце. Если это не так, Oracle соответственно корректирует расчеты кардинальности. Плотность является важным значением для количества возвращаемых запи- сей. Если у вас есть определенное значение в гистограмме, которое предполага- ет возврат X записей, но X меньше, чем плотность х (user_tabl.es.num_ rows - user_tab_columns. num_nuUs), то оптимизатор будет использовать большее значение. В моем случае у меня было 300 000 записей в таблице и один столбец с деся- тью уникальными значениями. После генерации кумулятивной частотной гис- тограммы плотность столбца составляла 0,000001667 и Oracle знал, что значе- ние 10 встречается точно 1000 раз согласно гистограмме. Поэтому на основе исходного теста следующий запрос показал план выполнения со значением кардинальности, равным 1000: select * from tl where currency_id = 10 Затем я выполнил несколько независимых тестов, используя сценарий hack_ stats.sql из онлайн-хранилища кода для изменения статистики в словаре дан- ных и пересоздавая план выполнения после каждого теста. о Когда я изменил значение user_tables.num_rows на 150 000, оптимизатор рассчитал кардинальность, равную 500. о Когда я изменил значение user_tab_columns. num_nulls на 200 000, опти- мизатор рассчитал кардинальность, равную 333. о Когда я изменил значение user_tab_columns.density на 0,01, оптимизатор рассчитал кардинальность, равную 3000. Результаты последнего теста оказались наиболее неожиданными — когда вы создаете частотную гистограмму, плотность всегда равна 1/ (2 х num_rows). Ви- димо, в оптимизаторе есть код, который обрабатывает возможность того, что кто-то изменил значение плотности и устанавливает его настолько большим, чтобы какое-нибудь из известных значений в частотной гистограмме в любом случае вернуло меньшее количество записей, чем неизвестное значение, кото- рого в гистограмме нет. Очевидно, что этот подход может быть очень удобным для определения до 254 уникальных значений в столбце и дальнейшей установки такого количества записей, какое должен назначать оптимизатор для любого другого значения, которое может быть использовано во время выполнения запроса на равенство этого столбца константе. Конечно, при этом нужно учитывать обычные пробле- мы с переменными связывания, считыванием значений переменных связыва- ния и соединениями.
«Сбалансированные по высоте» гистограммы 197 Сбалансированные по высоте» истограммы Если мы возьмем тестовый набор данных из предыдущего раздела и создадим гистограмму с 75 группами (сценарий c_skew_ht_Ol.sqL в онлайн-хранилище кода), мы можем заметить мелкие, но значимые детали в результатах, храня- щихся в словаре данных: begin dbms_stats.gather_table_stats( user, 'tl', cascade => true, estimate_percent => null, method_opt => 'for all columns size 75' ): end; I select num_distinct, density, num_buckets from user_tab_columns where table_name = 'Tl' and column name = 'SKEW NUM_DISTINCT DENSITY NUM_BUCKETS 80 .013885925 58 select endpoint_number, endpoint_value from user_tab_histograms where column_name = 'SKEW' and table_name = 'Tl' Order by endpoi nt_number 5 PDPOINT_NUMBER ENDPOINT VALUE 59 60 62 64 65 67 69 71 71 72 73 74 75 76 77 78
198 Глава 7. Гистограммы 73 79 75 80 Выбрано 59 записей. Из информации в user_tab_columns мы видим, что Oracle правильно рас- считал, что у нас есть 80 уникальных значений в столбце nl. Плотность не рав- на 1/80, она больше похожа на 1/72 — но она могла бы быть практически лю- бым значением. Мы также видим, что Oracle создал гистограмму с 58 группами, несмотря на то, что мы запросили 75 групп и что в представлении user_ tab_histograms мы действительно видим 75 групп (самый большой номер ко- нечной точки равен 75), выведенных в 59 строках. На самом деле результаты в версиях 8i, 9i и 10g различаются. о В версии 8i выдается плотность, равная 0,006756757, создается 74 группы и выводится 58 групп. о В версии 9i выдается плотность, равная 0,013885925, создается 75 групп и выводится 58 групп. о В версии 10g выдается плотность, равная 0,013885925, создается 75 групп и выводится 75 групп. Количество групп является в принципе несущественным, а разница в плот- ности и действительном количестве групп возникает из-за того, что версия 8i использует команду analyze для генерации гистограммы, в то время как вер- сии 91 и 10g выполняют команду SQL, показанную в конце раздела «Введение» этой главы. Информация о гистограмме в трассировке 10053 (особенно инфор- мация о «сбалансированных по высоте» гистограммах) различается в зависимо- сти от версий, и информация в версии 10.1.0.4 немного отличается от информа- ции в версии 10.1.0.2. Следующие три примера взяты при выполнении одного и того же сценария — как видите, полнота информации растет в последующих версиях: Версия 10.1.0.4: COLUMN: SKEW(NUMBER) Col#: 1 Table: TlAlias: Tl Size: 3 NDV: 80 Nulls: 0 Density: 1.3886e-002 Histogram: HtBal #Bkts: 75 UncompBkts: 75 EndPtVals: 59 Версия 9,2.0.6: Column: SKEW Col#: ITable; Tl Alias: Tl NDV: 80 NULLS: 0 DENS: 1.3886e-002 HEIGHT BALANCED HISTOGRAM: #BKT: 75 #VAL: 59 Версия 8.1.7.4 не выводит никакой детализированной информации о гистограмме: Column: SKEW Col#: ITable: Tl Alias: Tl NDV; 80 NULLS: 0 DENS: 6.7568e-003 Более важным моментом, чем внешние детали, является фундаментальная проблема невезения, которая видна в этом примере. Посмотрите еще раз на список (endpoint_number, endpoint_value). Вы заметите, что некоторых строк явно не хватает — например, нет строки с endpoi nt_number, равным 74. Вот почему в версии 10g мы получили 75 несжатых групп, но только 59 значе- ний конечных точек. Некоторые группы подразумеваются и в базе данных не хранятся. Мы должны видеть строку со значениями (74, 80), но Oracle может
«Сбалансированные по высоте» гистограммы 199 подразумевать эту строку. Расширив предыдущие результаты для вывода всех групп, хранимых и подразумеваемых, мы должны увидеть следующее: ENDPOINT_NUMBER ENDPOINT_VALUE 59 71 60 72 61 73 62 73 63 74 64 74 65 75 *** 66 76 67 76 68 77 69 77 70 78 71 78 72 79 73 79 74 80 75 80 Если вы выведете графическое представление этих данных, вы увидите, что должны объединить по два прямоугольника с одинаковой высотой для каждого из значений 73, 74, 76, 77, 78, 79 и 80, но примерно в середине графического представления есть небольшая впадина в области значения 75 (отмечено зна- ком ***). Oracle распознает «часто встречающиеся» значения в ваших данных, осно- вываясь на том, что несжатый список конечных точек показывает повторяю- щиеся значения. И, согласно статистике, которую собрал Oracle, 75 не является часто встречающимся значением, а 74 является — несмотря на то, что мы так распределили данные, чтобы записей со значением 75 было больше, чем запи- сей со значением 74. При использовании сбалансированных по высоте гистограмм вам может просто не повезти — вы можете удостовериться в этом, запустив и другие тесты на этом простом наборе данных. Создайте гистограмму с 81 группой — и вы об- наружите, что каждое значение начиная с 72 и выше выводится как часто встре- чающееся. Увеличьте количество групп до 82 — и значение 71 станет часто встречающимся. Это хорошо, но в то же время значение 76 перестанет быть час- то встречающимся — а это плохо. Не существует такого подхода, чтобы вы были уверены, что какое-то опреде- ленное количество групп включает все ваши часто встречающиеся знания, хотя есть такой вариант: сразу создать 254 группы и затем проверить, что в них Включены все ваши часто встречающиеся значения. Конечно, вы заметите, что максимальное количество групп равно всего лишь 254 — так что при самой лучшей гранулярности любая группа содержит 1/250 (0,4 %) от количества записей, содержащих значение. Если у вас есть более 250 уни- кальных значений, тогда в гистограмме некоторых из них точно не будет. На са- мом деле ситуация хуже: одно значение может охватывать почти две группы
200 Глава 7. Гисгограммы (0,8 % от количества записей) и не будет распознано Oracle как часто встречаю- щееся значение. Что еще хуже, каждое «очень часто встречающееся» значение содержится в большем количестве групп, чем его реальная доля, то есть неболь- шое количество широко распространенных значений будет означать, что Oracle пропустил десятки значений, которые вы бы классифицировали как часто встре- чающиеся. Бывают случаи, когда нужно вернуться к созданию искусственных частот- ных гистограмм, потому что так лучше всего можно описать ваш набор данных в Oracle. Расчеты Когда оптимизатор пользуется преимуществами сбалансированной по высоте гистограммы для расчета кардинальности, он использует одну из трех основ- ных стратегий. Первая стратегия для расчета селективности и кардинальности: для преди- ката столбец = константа константа может быть часто встречающимся значе- нием. Часто встречающееся значение — это значение, которое заполняет как минимум одну группу и, следовательно, приводит к появлению пробела в пред- ставлении user_tab_hi stograms. Если Oracle обнаруживает такое значение, он выполняет расчеты для него с помощью групп. Например, как было упомянуто выше, значение 77 появляется при значении конечной точки, равном 69, а пре- дыдущее значение (76) появляется при значении конечной точки, равном 67, — следовательно, Oracle рассматривает 77 как часто встречающееся значение, ко- торое распространяется на две группы из 75. Таким образом, селективность значения 77 равна 2/75, а кардинальность равна 3240 х 2/75 “ 86,4 — и из авто- трассировки мы видим: План выполнения (версия 9.2.0.6 - select count(*) from tl where skew = 77) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=l Card=l Bytes=3) 1 0 SORT (AGGREGATE) 2 1 INDEX (RANGE SCAN) OF 'T1_I1' (NON-UNIQUE) (Cost=l Card=86 Bytes=258) Вторая стратегия для расчета селективности и кардинальности: мы уже отметили, что 75 не является специализированным физическим значением, так что же случится, когда мы выполним запрос, выбирающий все записи для этого значения? Oracle использует плотность (заметьте, не l/num_distinct). Плот- ность определена как 0,013885925, поэтому ожидаемая кардинальность равна 3240 х 0,013885925 - 44,99, и план выполнения опять показывает План выполнения (версия 9.2.0.6 - select count(*) from tl where skew = 75) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=l Card=l Bytes=3) 1 0 SORT (AGGREGATE) 2 1 INDEX (RANGE SCAN) OF 'T1_I1’ (NON-UNIQUE) (Cost=l Card=45 Bytes-135) Конечно, вы можете спросить, как Oracle рассчитывает плотность, — на этот вопрос я не хочу тратить много времени, Так как он требует довольно длинного примера для объяснения, да это и не очень важно. Поэтому сейчас я дам корот- кий ответ, а длинный ответ оставлю для второго тома.
«Сбалансированные по высоте» гистограммы 201 Если кратко, то плотность рассчитывается следующим образом: сумма квадратов частоты нечасто встречающихся значений / (количество записей, значение которых не равно NULL * количество записей с нечасто встречающимися значениями, не равными NULL) Плотность можно расчитать на основе большого запроса для гистограммы в кон- це раздела «Введение». Используя столбцы запроса, можно получить следую- щее: ( sum(sumrepsq) - sum(maxrep(i) * maxrep(i) ) I ( sum(sumrep) * ( sum(sumrep) - sum(maxrep(i) ) ) Индексы (1) указывают, что из результатов запроса для расчета значения шах г ер выбираются только определенные записи. Эти записи удовлетворяют следующему условию: maxbkt > min(minbkt) + 1 or min(val) = max(val) В особом случае, когда нет часто встречающихся значений (или, по крайней мере, они не были обнаружены запросом), эта формула упрощается до sum(sumrepsq) / (sum(sumrep) * (sum(sumrep)) Эта формула может быть описана как сумма квадратов частот, разделенная на квадрат суммы частот. Если вы тщательно проанализируете формулу, то поймете, что существует потенциальная ловушка, которая может вызвать проблемы, если вам не повезет С вашим набором данных. Суть этого расчета плотности состоит в том, чтобы убрать влияние часто встречающихся значений. Например, если у вас есть 2000 строк, из которых 1000 содержит одно и то Же значение, то скорректированная плотность будет равна 1/2000 вместо 1/1001, потому что Oracle должен обнаруживать запросы по часто встречаю- щимся значениям и действовать соответственно. В принципе, в этом случае нам нужна формула для скорректированной плотности, чтобы уменьшить плот- ность, которая появляется при отсутствии гистограммы. На практике это рабо- тает хорошо для экстремальных случаев, один из которых я только что описал. Однако если в ваших данных есть часто встречающиеся значения и Oracle Не обнаруживает их во время создания гистограммы, формула будет выдавать увеличенную плотность. К тому же вам нужно знать ваши данные и знать, чего ВЫ ожидаете, чтобы быть уверенными, что оптимизатор сможет все правильно Сделать. Третья стратегия для расчета селективности и кардинальности: после ко- роткого отступления мы можем изучить вопросы, связанные со сканирования- ми по диапазону. Конечно, эта тема очень обширна, поэтому мы разберем толь- ко два показательных случая. Так как набор данных, который я использовал, Достаточно необычный, для этих тестов я создал другой набор данных, чтобы гарантировать, что результаты очень четко покажут, что делает Oracle. Для это- го понадобится сценарий hist_sel.sql в онлайн-хранилище кода. Сценарий генерирует 10 000 записей с помощью масштабируемого нормально- го распределения в диапазоне от -5000 до И 000, в середине которого находится значение 3000. В соответствии с этим распределением у меня есть по 500 записей
202 Глава 7. Гистограммы на каждое из значений 500, 1000, 1500, и т. д. до 10 000 — то есть список из 20 часто встречающихся значений. После анализа этой таблицы с 250 группами (по 80 записей на группу) я по- лучил гистограмму, показывающую пики по моим 20 часто встречающимся зна- чениям. Количество уникальных значений равно 5626, плотность столбца равна 0,000119361 (значение меньше, чем 1/5626 = 0,000177746). Мой первый тестовый пример — это сканирование по диапазону групп, не содержащих часто встречающихся значений: nl between 1GG and 200. Если мы посмотрим на гистограмму, то обнаружим, что необходимые значения попада- ют на группы со значениями конечных точек, равными 17, 117 и 251. ENDPOINT_NUMBER ENDPOINT_VALUE 8 -120 9 17 10 117 11 251 12 357 Итак, мы применяем стандартную формулу — принимая во внимание, что мы должны разделить наш диапазон на две отдельные группы, чтобы исследо- вать диапазоны от 100 до 117 и от 117 до 200: Селективность = (диапазон) / (наибольшее значение - наименьшее значение) + 2 * плотность (200-117)7(251-117) + (117-100)7(117-17) + 2 * 0.000177746 = 0.619403 + 0.17 + .000355486 = 0.789047508 Кардинальность = селективность * количество строк В ГРУППЕ = 0.789047508 * 80 = 63.1238 Конечно, когда мы выполним запрос с автотрассировкой, мы получим сле- дующее: План выполнения (автотрассировка, версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=60 Card=63 Bytes=945) 1 0 TABLE ACCESS (FULL) OF 'Tl' (Cost=60 Card=63 Bytes=945) Вторым тестовым примером, конечно, является сканирование по диапазону по группам с часто встречающимися значениями: nl between 4GG and 600 (один из моих пиков находится в значении nl = 5GG). Нам нужно проверить неболь- шую часть гистограммы: ENDPOINT_NUMBER ENDPOINT_VALUE 12 13 19 20 21 22 357 450 500 *** 520 598 670 Обратите внимание, что значение 500 является часто встречающимся (очень часто встречающимся) — оно расположено в шести группах гистограммы. Та- ким образом, мы собираемся произвести расчет по как минимум шести группам плюс остальной диапазон. После тщательного анализа мы видим, что диапазон от 400 до 600 будет расширен от группы 12 до группы 22, как показано ниже.
Еще раз о проблемах с данными 203 О Выборка от 357 до 450. О Все группы от 450 до 598. О Выборка от 598 до 670. Итак, у нас есть восемь полных групп (endpoint_numbers от 13 до 21) плюс (450 - 400) / (450 - 357) + (600 - 598) / (670 - 598) + 2 * 0.000177746 = 50/93 + 2/72 + 0.000355486 = 0.537634 + 0.0277778 + 0.000355486 = 0.565768 Не забыв добавить 8 полных групп, мы получим кардинальность, равную Кардинальность = селективность * количество записей В ГРУППЕ = 8.565867 * 80 = 685.3 Выполнив еще раз запрос с автотрассировкой, мы получим План выполнения (автотрассировка, версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=60 Card=685 Bytes=10275) 1 0 TABLE ACCESS (FULL) OF 'Tl' (Cost=60 Card=685 Bytes=10275) Обратите внимание, что поскольку версия 8i использует старый механизм analyze генерации гистограмм, гистограмма и плотность не будут совпадать со значениями из версий 9i и 10g. Однако после генерации гистограммы и плотно- сти расчеты производятся таким же образом. Еще раз о проблемах с данными В главе 6 я показал пару примеров, в которых Oracle столкнулся с проблемами при получении правильной оценки селективности из-за неверных дизайнер- ских решений, не относящихся к Oracle, а затем высказал утверждение, что в таких случаях помогает создание гистограммы. Давайте более подробно рас- смотрим, что происходит в таких случаях. Проблемные типы данных Мы создали таблицу, содержащую даты за пять лет, но эта информация хранит- ся в трех различных формах — в форме даты Oracle, в форме восьмизначного числа вида ГГГГММДД и в форме символьной строки, похожей на число — 'ггггммдд'. Код для создания таблицы приводится здесь и в сценарии date_ oddity.sql в онлайн-хранилище кода: create table tl ( dl date, nl number(8), vl varchar2(8) Insert into tl select dl. to_number(to_char(dl,'yyyymmdd')),
204 Глава 7. Гистограммы to_char(dl,'yyyymmdd') from ( select to_date('31-Dec-1999') + rownum dl from all_objects where rownum <= 1827 ) I Запросы, использующие предикат равенства, могут работать с типами дан- ных, не хранящими даты, и если мы отсортируем данные по столбцам с псевдо- датами, упорядочивание будет произведено правильно. Но предикаты на осно- ве диапазонов дают неверные значения кардинальности, потому что числовые и символьные представления дат имеют большие пробелы. Oracle знает, что от 1 апреля 2003 года до 31 марта 2003 года один день, но он не может применить ту же самую логику, сравнивая 20030401 с 20030331 или '20030401' с '20030331'. Как мы видели в главе 6, запрос с предикатом nl between 20021230 and 20030105 выдавал кардинальность, равную 396 в Oracle 91, хотя мы знаем, что запрос вернет 7 дней, а тот же запрос, но со столбцом типа «дата», выдал кардинальность, равную 8. Когда мы создаем гистограмму из 120 групп, кардинальность запроса со столбцом числового типа приближается к верному значению и равна 15 строкам. Сейчас мы рассмотрим, почему это так. В разделе «Введение» этой главы я привел пример команды SQL, которую вы можете выполнить на таблице user_tab_histograms для вывода значений ширины и высоты прямоугольников гистограммы. Версия сценария date_oddity. sql, которая относится к этой главе, выполняет те же самые действия для гисто- граммы со 120 группами для числовых данных, и секция с важными результата- ми выглядит следующим образом: BUCKET L0W_VAL HIGH_VAL WIDTH HEIGHT 68 20021028 20021112 84 .1813 69 20021112 20021127 15 1.015 70 20021127 20021212 85 .1791 71 20021212 20021227 15 1.015 72 20021227 20030111 8884 .0017 73 20030111 20030126 15 1.015 74 20030126 20030210 84 .1813 75 20030210 20030225 15 1.015 76 20030225 20030312 87 .175 77 20030312 20030327 15 1.015 Как видите, гистограмма позволяет Oracle получить лучшее представление о данных. Отчетливо виден очень большой пробел — с очень небольшим коли- чеством данных — между числами 20021227 и 20030111. Пробел связан с окон- чанием года и понятен для человека, но эта информация ориентирована на че- ловека и неизвестна компьютеру — по крайней мере, без помощи гистограммы. Гистограмма большего масштаба, в которой нет подробной информации о меся-
Еще раз о проблемах с данными 205 цах, дает Oracle графическое представление данных (часть 3 на рис. 7.2), позво- ляющее приблизиться к правильному ответу. Таким же образом гистограмма показывает, что от конца каждого месяца до начала следующего месяца (на самом деле учитывается размер нашей группы, до середины) данных относительно мало, но для остальной части месяца суще- ствует примерно одна запись на день. Конечно, из первой строки предыдущего списка мы знаем, что разница между 20 021 112 и 20 021 028 составляет всего 14 дней, но Oracle думает, что она составляет 84 дня (в числовом представле- нии), среди которых нужно распределить 14 записей из нашей исходной таб- лицы. Расчеты для нашего запроса полностью попадают на группу 72, так что оп- тимизатор запросов использует стандартную формулу для селективности, но Значения конечных точек использует из группы. У нас есть выражение be- tween, то есть нам нужно учесть поправочный коэффициент на обеих границах диапазона. Реальное распределение символьных/числовых данных Нужный нам __ _. _I участок I п I ..........> . Представление Oracle о распределении данных без гистограмм Рис. 7.2. Гистограммы и неверные типы данных ПРИМЕЧАНИЕ Когда вы будете создавать гистограммы, то плотность не обязательно будет равна 1 / num.dlstlnct (хотя в данном примере это так, потому что у нас нет часто встречающихся значений и нет частот- ной гистограммы). Ранее мы использовали 1 / num_di sti net в качестве поправочного коэффи- циента, и нас не волновало, что, возможно, следует использовать плотность.
206 Глава 7. Гистограммы В большинстве случаев именно плотность нам и нужно использовать, так что наша формула будет выглядеть следующим образом: Селективность = ('необходимый диапазон' / 'общий диапазон группы' ) + 2 * плотность = (20030105 - 20021230) / (20030111 - 20021227) + 2 * 0.000547345 = 8875/8884 + 0.00109469 = 1.00008 Но эта селективность должна применяться к количеству записей в исполь- зуемых группах, а не к целой таблице, и это количество равно 1827/120 = 15 за- писей на группу — round (15 * 1.00008) = 15, что и требовалось. Обратите внимание, что каждое значение в таблице повторяется одинаковое количество раз. Не существует значений, встречающихся очень часто. Согласно традиционному мнению (небольшое количество часто встречающихся значе- ний), этот набор данных не нуждается в гистограмме. Но, как видите, гисто- грамма оказывает огромное влияние на работу оптимизатора. Любой столбец с распределением данных, которое не имеет вид горизон- тальной линии, может потребовать использования гистограммы, если вы будете использовать этот столбец в выражении where. Графическое представление этого распределения плоское, но в нем есть несколько больших провалов, о ко- торых оптимизатор должен знать. Проблемы со значениями по умолчанию Другая проблема, которую мы рассмотрели в главе 6, относится к использова- нию специального (экстремального) значения вместо NULL. У нас был столбец типа «дата», но для представления отсутствующих данных в нем использова- лось значение 31 декабря 4000 года. В этом случае также возникали проблемы в запросах с диапазонами, потому что, хотя оптимизатор производил расчеты с датами правильно, в стандартной формуле использовалось абсолютно нереа- листичное большое значение. У нас были данные с 1 января 2000 года по 31 декабря 2004 года и следую- щий предикат: where date_closed between to_date('01-Jan-2003','dd-mon-yyyy') and to_date('31-Dec-2003','dd-mon-yyyy') Конечно, мы ожидаем, что Oracle вычислит, что нам нужна пятая часть дан- ных. Однако Oracle видит большое значение, 31 декабря 4000 года, и рассчиты- вает селективность, равную примерно 3/2000, потому что он использует сле- дующую формулу: (31 декабря 2003 года - 1 января 2003 года) / (31 декабря 4000 года - 1 января 2000 года) + 2/1828 = 0.00159 Но когда мы создаем гистограмму всего лишь из 11 групп — указывая по две группы на год и одну запасную, просто для демонстрации того, что иногда дос- таточно и очень маленького количества групп, — стоимостный оптимизатор не- ожиданно получает очень хороший результат селективности, а значит, и карди- нальности. С использованием того же самого метода для вывода данных в гисто-
Еще раз о проблемах с данными 207 грамме в графическом представлении полная гистограмма выглядит следующим образом: Группа LOW_VAL HIGH_VAL Ширина Высота 1 01-Jan-2000 15-Jun-2000 166 100.0548 2 15-Jun-2000 28-Nov-2000 166 100.0548 3 28-Nov-2000 13-May-2001 166 100.0548 4 13-Мау-2001 27-Oct-2001 167 99.4556 5 27-Oct-2001 ll-Apr-2002 166 100.0548 6 ll-Apr-2002 24-Sep-2002 166 100.0548 7 24-Sep-2002 09-Mar-2003 166 100.0548 8 09-Mar-2003 23-Aug-2003 167 99.4556 9 23-Aug-2003 05-Feb-2004 166 100.0548 10 05-Feb-2004 20-JU1-2004 166 100.0548 11 20-Jul-2004 31-Oec-4000 729188 .0228 Если вы посмотрите на SQL-код, который я использовал для этой таблицы (сценарий defauIts.sql в онлайн-хранилище кода), вы обнаружите, что реальные значения, хранящиеся в гистограмме, являются юлианскими эквивалентами дат и что я использовал функцию to_date () для их вывода. Примерное графи- ческое представление этих значений приводится в третьей части рис. 7.3. Реальное распределение данных с псевдозначением null, находящимся далеко за пределами диапазона Нужный нам участок данных Представление Oracle о распределении данных без гистограмм Рис. 7.3. Гистограммы и псевдозначения NULL Набор данных был создан со 100 записями на день в разрешенном диапазоне, с 1 % данных от набора данных до 31 декабря 4000 года. Обратите внимание, что странные значения показаны только в последней группе, с огромным диапазоном
208 Глава 7. Гистограммы дат и с очень маленьким количеством записей (0,0228) на один день, в то время как остальная часть гистограммы показывает приблизительно 100 записей на один день для первых 4,5 лет. Любые запросы, которые мы выполняем для первых четырех лет, будут иметь достаточно точные оценки кардинальности. Только в последние шесть месяцев (с 20 июля) эти значения станут неверными — и мы можем уменьшить диапазон, в котором значения становятся неверными, создав гораздо больше групп. Также мы можем забыть про анализ таких предсказуемых данных и про- сто написать небольшую программу, которая создаст искусственную гистограм- му, чтобы предоставить Oracle еще более точные данные. И снова получается, что гистограмма предназначена не только для данных с небольшим количеством часто встречающихся значений — у нас есть график, не являющийся простой горизонтальной и непрерывной линией, и стоимост- ной оптимизатор должен об этом знать. Кстати, если оставить включенным автоматический сбор статистики, то Ora- cle версии 10g не создаст гистограммы на этом наборе данных, даже если она ему будет необходима. Заключение Если в вашей базе данных есть столбец, имеющий особенное распределение данных, и он используется в выражении where (даже в условии соединения), то вам может понадобиться создать гистограмму на этом столбце. Столбцы, храня- щие небольшое количество значений (меньше 255), являются хорошими пре- тендентами на создание для них частотных гистограмм, но не забывайте об из- менении данных и необходимости своевременного обновления гистограмм. У Oracle могут возникнуть проблемы с созданием частотной гистограммы, если вы используете пакет dbms_stats (который вы должны использовать), даже если у вас есть всего лишь от 180 до 200 уникальных значений в столбце. В этом случае вы всегда можете вручную сгенерировать гистограмму и запол- нить словарь данных. Если у вас есть большое количество уникальных значе- ний в столбце, то вам нужно будет приблизительно оценить количество групп. В большинстве случаев безопасным способом будет просто выбор максималь- ного количества. Но вам может не повезти. Знайте о ваших часто встречающих- ся или странных значениях и убедитесь, что они видны в гистограмме. Также помните, что гистограмма существует для помощи оптимизатору — вы всегда можете скорректировать внутренний метод Oracle для генерации гистограмм, чтобы предоставить оптимизатору информацию о важных особенностях ваших данных. Помните, что для использования гистограмм есть гораздо больше причин, чем просто «небольшое количество очень часто встречающихся значений».
Тестовые сценарии 209 тестовые сценарии файлы к этой главе, доступные для загрузки, перечислены в табл. 7.2. Таблица 7.2. Тестовые сценарии к главе 7 Сценарий Комментарии Sstjntro.sql slmilansql dist_hist.sql c_skew_freq.sql CjSkew_freq_01.sql Cjskew_freq_02.sql fatei.hlst.sql (»ckjstats.sql ?jSkew_ht_01.sql Создает большой набор данных с нормальным распределением Демонстрирует влияние установки cursor_sharing = similar Демонстрирует игнорирование гистограмм распределенными запросами Создает пример частотной гистограммы Примеры значений кардинальности, полученных из частотной гистограммы Пример создания частотной гистограммы Примеры влияния изменения статистики на гистограммы Сценарий для модификации статистики прямо в словаре данных Примеры значений кардинальности, полученные с помощью «сбалансированных по высоте» гистограмм hl5t_sel.sql Еще один пример кардинальности, полученной с помощью «сбалансированных по высоте» гистограмм date_oddlty.sql Пример того, как гистограммы могут помочь в решении проблем с неверными типами данных defauits.sqi Пример того, как гистограммы могут Помочь в решении проблем с экстремальными значениями по умолчанию ietenv.sql Установка стандартизированной тестовой среды для SQL*Plus
О Битовые индексы Когда я начинал писать эту главу, я думал, что она будет гораздо короче, чем такая же глава про индексы со структурой бинарного дерева (глава 4), по двум причинам. Во-первых, битовые индексы на самом деле «скрыты» в структурах В-дерева, так что большая часть расчетов описана в другой главе (поэтому вам лучше сначала прочитать ее). Во-вторых, битовые индексы намного проще (как я думал), чем индексы со структурой бинарного дерева, так что придется рас- сматривать меньшее количество сложных случаев. Однако когда я начал писать эту главу, то понял, что мое безосновательное предположение оказалось неверным. Со стратегической точки зрения вы ис- пользуете битовые индексы абсолютно по-другому, чем индексы со структурой бинарного дерева. Так что, хотя при рассмотрении всего одного битового ин- декса нужно узнать немного новой информации, вы не получите всю картину, если вы остановитесь только на одном индексе. А если вы начнете рассматри- вать расчеты, которые использует оптимизатор запросов во время комбиниро- вания битовых индексов или при их генерации во время выполнения, то може- те столкнуться с очень странным поведением. Однако, перед тем как пойти дальше, я хочу сделать одно замечание. Вы мо- жете столкнуться с некоторыми странными эффектами во время проведения контролируемых экспериментов, и в некоторых случаях полученные мной ре- зультаты настолько странны, что, вероятно, они вызваны ошибками. Почему, например, стоимость использования битового индекса изменяется случайным образом, когда вы изменяете значение db_f1le_multiblock_read_count? Из-за очевидных аномалий в расчетах битовых индексов я был немного обеспокоен точностью расчетов в этой главе. Если у меня есть теория, предска- зывающая, что значение будет равным 27,23, а полученный ответ равен 27,51, - это ошибка в моей теории или это побочный эффект ошибки оптимизатора? В результате, когда я получал ответ, близкий к предполагаемому, я не тратил много времени на выяснение причины разницы, потому что не хотелось его те- рять на поиски несуществующего решения. Введение Начинать главу всегда лучше с конкретного примера, и эту главу я бы хотел на- чать со всего лишь одной таблицы, которая демонстрирует большинство основ- ных причин, влияющих на стомость использования битового индекса.
Введение 211 ' Как обычно, табличное пространство должно состоять из блоков по 8 Кбайт, $ лучше, чтобы оно локально управлялось однородными экстентами размером в 1 Мбайт, а не ASSM, если вы хотите получить те же результаты, что и я. Так- же, если вы не использовали объявления из init.ora из онлайн-хранилища кода, to вам нужно будет убедиться, что значение db_file_multiblock_read_count у вас равно 8. Как и при рассмотрении стоимости индекса со структурой бинар- ного дерева, мы начнем работу с выключенной оценкой стоимости ресурсов Процессора. Следующий код является фрагментом сценария bitmap_.cost_01.sql. цз онлайн-хранилища кода: create table tl pctfree 70 pctused 30 select mod((rownum-1),20) nl, trunc((rownum-l)/500) n2, mod((rownum-1),25) n3, trunc((rownum-l)/400) n4, mod((rownum-1),25) n5, trunc((rownum-l)/400) n6, lpad(rownum,10,'0') small_vc, rpad('x',220) padding from all_objects where rownum <= 10000 - - 20 распределенных значений - - 20 кластеризованных значений - - 25 распределенных значений - -25 кластеризованных значений - -25 распределенных значений для В-дерева - - 25 кластеризованных значений для В-дерева - - значения для выборки - - добавляем в таблицу дополнительное пространство create bitmap index tl_il on tl(nl) pctfree 90 Create bitmap index tl_i2 on tl(n2) pctfree 90 create bitmap index tl_13 on tl(n3) pctfree 90 create bitmap index tl_14 on tl(n4) pctfree 90 Create /* B-tree */ index tl_15 on tl(n5) pctfree 90 Create /* B-tree */ index tl_i6 on tl(n6) pctfree 90
212 Глава 8. Битовые индексы begin ); end; dbms_stats.gather_table_stats( ownname => user, tabname => 'Tl', cascade ss> true, est1mate_percent =S> null. method_opt »> 'for all columns size 1' В этой таблице содержится шесть значимых столбцов, на каждом из кото- рых создан индекс. Столбцы nl и п2 содержат по 20 уникальных значений, но значения в одном столбце сгенерированы с помощью функции mod () по значе- ниям rownum таким образом, что значения распределены по таблице равномер- но, а значения другого столбца сгенерированы с помощью функции trunc() так, что все записи определенного значения собраны в группы по 500 записей. Я создал битовые индексы на этих двух столбцах для демонстрации того, как вариации в распределении данных влияют на статистику. Столбцы пЗ и п4 идентичны, и в каждом содержится по 25 значений. Из этих столбцов мы можем увидеть, как оптимизатор запросов рассчитывает стоимость во время комбинирования битовых индексов. Столбцы п5 и пб идентичны столбцам пЗ и п4, но на них построены индексы со структурой бинарного дерева, которые позволяют нам видеть, как различа- ются размеры битовых индексов и индексов со структурой бинарного дерева. Обратите внимание, что в созданном мною тестовом примере я создал и таб- лицу, и индексы с довольно большим значением параметра pctfree, чтобы не- много увеличить их размеры. Этот пример случайно выявил небольшое разли- чие в размещении битовых значений на листовых блоках между версиями 8i и 91, которое я не заметил раньше, — удивительно, как часто мы можем узнать что-либо новое об Oracle случайно, во время создания тестовых примеров. Статистика по индексам показана в табл. 8.1, если вы используете версию 9i для этого тестового примера. Таблица 8.1. Статистические различия между битовыми индексами и индексами со структурой бинарного дерева Статистика tljl (битовые индекс) tlJ2 i (битовы! индекс) tlJ3 tlJ4 tlJ5 i (битовый (битовый (индекс co tlJ6 (индекс со структурой бинарного дерева) индекс) индекс) структурой бинарного дерева) blevel 1 1 1 1 1 1 leaf_blocks 60 10 63 9 217 217 dlstinct_keys 20 20 25 25 25 25 num_rows 120 20 125 25 10 000 10 000 clusteri ngjactor 120 20 125 25 10 000 1112 avgjeaf_blocks_per_key 3 1 2 1 8 8 avg_data_blocks_per_key 6 1 5 1 400 44
Введение 213 Важные замечания. О На количество листовых блоков в битовых индексах заметно влияет класте- ризация данных (значения в столбце nl расположены случайным образом, индекс имеет 60 листовых блоков; значения в столбце п2 кластеризованы, индекс имеет 10 листовых блоков; аналогично столбцы пЗ и п4 имеют по 63 и 9 блоков, соответственно). В общем случае битовые индексы на располо- женных случайным образом данных больше, чем битовые индексы на кла- стеризованных данных, которые во всех прочих отношениях идентичны. На размер индекса со структурой бинарного дерева кластеризация данных так не влияет (но столбец п5 имеет расположенные случайным образом данные, а столбец пб — те же данные, но кластеризованные, и оба индекса имеют по 217 блоков). О Этот пример показывает, какими непредсказуемыми могут быть размеры би- товых индексов. Индексы tl_il и tl_i2 имеют по 20 уникальных ключей, индексы tl_i3 и tl_14 имеют по 25 уникальных ключей. При сравнении tl_i 1 с tl_i 3 (два индекса по распределенным данным) видно, что увеличе- ние количества уникальных значений привело к увеличению количества лис- товых блоков. При сравнении tl_i2 с tl_14 (два индекса с кластеризован- ными данными) видно, что увеличение количества уникальных значений привело к противоположному эффекту. О В случаях, когда таблицы не очень большие, вы можете обнаружить, что зна- чения distinct_keys и num_rows для битового индекса равны — это совпа- дение, а не правило (если вы создадите тестовый пример для версии 8i, вы обнаружите, что значения distinct_keys и num_rows равны во всех случа- ях). О В этом конкретном случае значение num_rows больше значения distinct_ keys для распределенных данных (tl_il и tl_i3), потому что, во-первых, последовательность бит для каждого ключевого значения делится на не- сколько частей, чтобы поместиться в листовые блоки, и во-вторых, результа- ты получены в версии 9i. ПРИМЕЧАНИЕ Вовремя написания этой главы я обнаружил следующую деталь: версия 9I выделяет листовые бло- ки битового индекса по-другому, чем версия 8I, для больших значений pctfree. Для битовых индек- Ш вы можете начать с большого значения pctfree, с 50 или даже с 67, чтобы уменьшить негативное Яйияние случайного DML кода. Вольфганг Брейтлинг — один из моих рецензентов — во время вы- данения одного из тестовых сценариев выяснил, что это изменение также имеет довольно неста- бильные границы, которые зависят от размера блока, использующегося для индекса. На момент на- писания этой главы никто из нас еще не исследовал полностью это изменение. w» ..... ........—..-...-.....-.........-..-........... ..... Ф Значение clustering_factor битового индекса всего лишь дублирует зна- чения num_rows индекса. Значение clustering_factor напрямую не связа- но с распределенностью данных в таблице. Распределенность данных влияет на размер записей битового индекса, и может показаться, что между значе- нием clustering_factor и распределенностью данных есть арифметиче- ская связь; но это не прямой результат, а побочный эффект.
214 Глава 8. Битовые индексы О Значение avg_leaf_blocks_per_key по-прежнему слабо применимо в бито- вых индексах (это значение по-прежнему вычисляется как round(leaf_ blocks / distinct_keys)). О Значение avg_data_blocks_per_key абсолютно неприменимо в битовых индексах (это значение по-прежнему вычисляется как round(clustering_ factor / diSt1nct_keys), но, как вы видели, значение clustering_factor в битовом индексе не описывает таблицу). НЕ СЛЕДУЕТ иВоЛЬЗОВАТЬ ВМЕСТЕ DML И БИТОВЫЕ ИНДЕКСЫ В общем случае выполнять обновление столбцов с включенными битовыми индексами довольно опасно, как и добавлять или удалять записи в таблицу с включенными битовыми индексами. Одно- временно выполняемые операции DML могут легко вызвать взаимные блокировки (deadlocks), и даже немного сериализованные DML-операции могут вызвать резкий рост битовых индексов. Ситуация с небольшими случайными изменениями улучшена в версии 10д, но для более ранних вер- сий вы можете обнаружить, что если вы не можете отключить битовый индекс, когда вам нужно внести изменения в данные, то создание его с большим начальным значением свободного места в процентах (например, значение pctfree, равное 67) может стабилизировать индекс на разумном уровне свободного места. Учитывая, что некоторые значения статистики <и в особенности cluste- ri ng_f actor) отличаются для битовых индексов,,какое влияние имеет это раз- личие на расчет стоимости использования индекса? Попробуйте выполнить простой запрос столбец = константа на столбцах пб, п5, п4 и пЗ по очереди. Все столбцы имеют одинаковое количество уйикальных значений, так что результа- ты, которые мы получим из автотрассировки четырех запросов, могут быть очень информативными. Ниже показаны четыре плана выполнения, получен- ные в версии 9.2.0.6: План выполнения - (пб: Индекс со структурой бинарного дерева на кластеризованном столбце) ------------------------------------------ - -и--------... 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=54 Card=400 Bytes=5600) 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=54 Card=400 Bytes=5600) 2 1 INDEX (RANGE SCAN) OF 'T1_I6' (NON-UNIQUE) (Cost=9 Card=400) План выполнения - (n5: Индекс со структурой бинарного дерева на столбце с распределенными данными) 0 SELECT STATEMENT Optimizer=ALL_R0W5 (Cost=170 Card=400 Bytes=5600) 1 0 TABLE ACCESS (FULL) OF 'Tl' (Cost=170 Card»400 Bytes=5600) План выполнения - (n4: Битовый индекс на кластеризованном столбце) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=114 Card=400 Bytes=5600) 1 0 TABLE ACCESS (BY INDEX ROWID) OF ’T1‘ (Cost=114 Card=400 Bytes=5600) 2 1 BITMAP CONVERSION (TO ROWIDS) 3 2 BITMAP INDEX (SINGLE VALUE) OF 'T1_I4' План выполнения - (пЗ: Битовый индекс на столбце с распределенными данными) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=117 Card=400 Bytes=5600) 1 0 TABLE ACCESS (BY INDEX ROWID) OF ’Tl' (Cost=117 Card=400 Bytes=S600)
Введение 215 I 1 BITMAP CONVERSION (TO ROWIDS) 3 2 BITMAP INDEX (SINGLE VALUE) OF 'T1_I3' Возможно, вы не удивились, увидев, что запросы к столбцам с индексами со структурой бинарного дерева имели разные планы выполнения для кластеризо- ванных данных (где индекс является достаточно эффективным) и распределен- ных данных (где индекс неэффективен). „ Но стоимости двух битовых индексов почти равны, несмотря на широкое распределение или плотную упаковку данных (плохо, что оптимизатор не вы- водит стоимость битового индекса — это важная часть полезной информации, которая также отсутствует в плане выполнения и может быть получена только из файлов трассировки 10053). Конечно, при виде статистики двух битовых индексов неудивительно, что их стоимости схожи — на самом деле, в статистике нет никакой информации о рас- пределении данных в таблице (вы можете подумать, что семикратная разница Значений leaf_block должна что-то значить, и в этом есть доля правды, но на самом деле это не лучший показатель. В данном примере этот фактор велик из-за объявленного мною значения свободного места, равного 90 %). Итак, как же рассчитывается стоимость битовых индексов? Одну часть отве- та я могу сказать точно, другую приблизительно — но имейте в виду мое преду- преждение о точности расчетов и ошибках. компонент индекса Выполните еще раз запросы с включенным событием 10053, чтобы вывести рас- четы стоимостного оптимизатора, и вы обнаружите следующую информацию 3 файлах трассировки (примеры из версии 9i): Для запроса пЗ = 2 с битовым индексом на распределенных данных Access path: index (equal) '' Index: T1_I3 TABLE: Tl RSC_CPU: 0 RSC_IO: 3 , IX_SEL: 4.0000e-002 TB_SEL: 4.0000e-002 ******** Bitmap access path accepted ******** £ost: 117 Costjo: 117 Cost_cpu: 0.000000 Selectivity: 0.040000 llot believed to be index-only. BEST_CST: 116.54 PATH: 20 Degree: 1 Для запроса n4 = 2 с битовым индексом на кластеризованных данных Access path: index (equal) Index: T1_I4 TABLE: Tl RSC_CPU: 0 RSC_IO: 1 IX_SEL: 4.0000e-002 TB_SEL: 4.0000e-002 ******** Bitmap access path accepted ******** CdSt: 114 Cost_io: 114 Cost_cpu: 0.000000 Selectivity: 0.040000 f&t believed to be index-only. BE5T_CST: 114.34 PATH: 20 Degree: 1
216 Глава 8. Битовые индексы Здесь есть два важных замечания. Во-первых, значение best_cst в каждом случае не является целым числом — оно выводится с двумя знаками после за- пятой и округляется до ближайшего целого числа (116,54 округляется до 117, а 114,34 — до 114). Второе замечание относится к значениям, которые мы видим (которые, ко- нечно, являются специфическими для данного примера). Выведенная стоимость индекса в обоих случаях (RSC_IO: 3 и RSC_IO: 1) получена тем же способом, что и для индексов со структурой бинарного дерева: ceiling (leaf_blocks х х эффективная селективность индекса) плюс 0 для blevel (как уже упомина- лось в главе 4, когда значение blevel равно единице, оно не учитывается в рас- четах). Последним замечанием, которое не сразу можно увидеть из этих значений, является то, что окончательная стоимость запроса получается умножением компонента индекса на 1,1. Возможно, это сделано для того, чтобы дать индек- сам со структурой бинарного дерева небольшое преимущество над битовыми индексами, когда таблица содержит оба типа индексов; возможно, чтобы умень- шить риск того, что оптимизатор будет выполнять ненужные превращения ин- дексов со структурой бинарного дерева в битовые индексы. Если вы еще раз посмотрите на выведенные ранее значения, вы получите следующие результаты. О Использование индекса tl_i3. Стоимость индекса равна 3, но она увеличи- вается до 3,3. Значение best_cst равно 116,54, так что стоимость обращения к табличным блокам была рассчитана как 116,54 - 3,3 • 113,24. О Использование индекса tl_i4. Стоимость индекса равна 1, но она увеличи- вается до 1,1. Значение best_cst равно 114,34, так что стоимость прохожде- ния по табличным блокам была рассчитана как 114,34 - 1,1 - 113,24. Забудьте о кластеризации и распределении данных. Для битовых индексов рассчитанная стоимость выборки из текущей таблицы для определенного объе- ма данных одинакова, независимо от того, распределены данные или кластери- зованы. Для битовых индексов стоимостный оптимизатор не имеет информа- ции о реальном распределении данных. Эта удивительная разница в стратегии оценки стоимости приводит к пута- нице относительно преимуществ использования битовых индексов. Помните, что мы видим оценку оптимизатора, то есть то, что должно произойти по его оценке, в особенности оценку оптимизатором количества предполагаемых опе- раций ввода-вывода. Иногда можно столкнуться с тем, что оптимизатор игнори- рует индекс со структурой бинарного дерева, но если превратить индекс в бито- вый, то оптимизатор будет его использовать (именно это и случится, если вы измените мой индекс tl_i 5 — индекс со структурой бинарного дерева на рас- пределенных данных — на битовый индекс). В результате неудачных экспериментов стало общепринятым, что битовый индекс эффективнее во время получения большого количества данных из табли- цы. Но это неверно. Если вы выполнили правильно подготовленные тесты с би- товым индексом и индексом со структурой бинарного дерева, то объем работы будет одинаковым для обоих, какую бы стоимость ни показывал оптимизатор.
Введение 217 Механизм времени выполнения будет запрашивать небольшое количество бло- ков из индекса, а затем загрузку блоков из таблицы. Количество табличных блоков будет одинаковым, независимо от того, является ли индекс битовым цди индексом со структурой бинарного дерева. Так что же тогда на самом деле происходит с битовыми индексами? Оптимизатор теряет важную информацию р распределении данных в таблице и вынужден использовать в качестве значе- ния распределения данных некие нереальные цифры. Каковы же будут послед- ствия, если вы решите превратить индекс со структурой бинарного дерева в би- товый индекс? Если у вас есть индекс со структурой бинарного дерева, стоимость которого Невелика, то стоимость такого же битового индекса, скорее всего, будет выше — Подумайте о результате превращения индекса со структурой бинарного дерева tl__i 6 (значение стоимости равно 54) в битовой индекс tl_14 (значение стои- ^рсти равно 114). 4 Если, у вас есть индекс сс структурой, бинарного дерева, стоимость которого больше, то стоимость такого битового индекса скорее всего будет ниже — поду- майте о результате превращения индекса со структурой бинарного дерева tl_i 5 (значение стоимости настолько высоко, что оИ не был использован) в битовый Нвдекс tl_i3 (значение стоимости равно 117, достаточно низкое, чтобы этот ЙНДекс был использован). ...... — I.— II—— - « .. ....- ... ».—.-I- . . ......— , , , ВНИМАНИЕ Помимо явно недостаточного объяснения результатов свйих экспериментов, все, кто защищает пре- вращение индексов со структурой бинарного дерева в битовые индексы для «настройки» запросов, вероятно, не замечают проблем, связанных с поддержкой, блокировками и взаимоблокировками, которые возникают при использовании битовых индексов на столбцах, где данные хотя бы изредка Вменяются. Жбличный компонент Вернувшись к расчетам для двух запросов со следующими предикатами: фщге пЗ » {constant} n4 « {constant} мы определили, что оптимизатор вычислил стоимость прохождения по таблице равную 113,24. Откуда взялось это значение? Здесь мы можем воспользо- ||ТВСЯ двумя другими столбцами с битовыми индексами в таблице, чтобы про- верить трассировки 10053 для предикатов: Stere nl и {constant} s#W*e n2 в {constant} г Если мы так сделаем, то увидим, что относящееся к таблице значение стои- мости этих двух предикатов равно 137,99. С учетом того, что каждое значение столбца nl или п2 возвращает по 500 за- Жсей, а каждое значение столбца пЗ или п4 возвращает по 400 записей, не уди- вительно, что значение 137,99, полученное из запросов со столбцами nl и г»2, |Ыольно близко к значению 113,24 х 500/400 (точнее, недостаточно близко
218 Глава 8. Битовые индексы к результату этих вычислений, равному 141,55). Похоже, что оптимизатор рабо- тает в соответствии со своими предположениями о распределении данных. По мнению авторов книги «Oracle Wait Interface: A Practical Guide to Oracle Performance Diagnostics and Tuning» (издательство «Osborne McGraw-Hill», 2004), оптимизатор предполагает, что 80 % нужных данных расположены неда- леко друг от друга, а 20 % широко распределены. Вы можете применить данный подход двумя немного различающимися способами, но давайте испытаем его на столбцах nl / пЗ и п2 / п4, предполагая, что 80 % данных кластеризованы, а ос- тавшиеся 20 % распределены по оставшимся табличным блокам. Результаты показаны в табл. 8.2. Таблица 8.2. Расчет стоимости битовых индексов — проверка разделения 80/20 Значение Индексы на столбцах nl и п2 (20 значений) Индексы на столбцах пЗ и п4 (25 значений) Записей в таблице 10 000 10 000 Блоков в таблице 1112 1112 Количество записей в блоке В,993 В,993 Количество выбранных записей по условию равенства 500 400 Количество кластеризованных записей 0,В х 500 = 400 0,6 х 400 = 320 Количество блоков для кластеризованных записей 400/6,993 = 44,46 320/6,993 = 35,5В Количество оставшихся блоков 1112-44,46= 1067 1112 - 35,55 = 1076 Распределенные записи (20 %) 0,2 х 500 = 100 0,2 х 400 = ВО Количество блоков для кластеризованных записей 100 ВО Общее количество необходимых табличных блоков 100 + 44,4В = 144,4В ВО + 35,56 = 115,56 Итоговая стоимость выборки из таблицы (из трассировки 10053) 137,99 113,24 Итак, предположение об использовании оптимизатором 80 % в качестве примерного значения для степени кластеризации данных в некоторой степени верно, но недостаточно верно для полученных нами значений. Но есть еще одна трудность. Если вы меняете значение db_file_multi- block—read_count, значение стоимости этих запросов также меняется, хотя и не в той же степени. Вообще говоря, с увеличением этого параметра стои- мость использования битового индекса также растет — но в расчетах присутст- вуют неточности. В табл. 8.3 показаны результаты (сгенерированные сценарием bitmapjnbrc. sql из онлайн-хранилища кода) расчета стоимости следующего запроса: select /*+ index(tl) */ small_vc from tl where nl = 2
Комбинации битовых индексов 219 Как видите, стоимость в общем случае увеличивается с ростом значения db_f ile_multiblock_read_count, но это не строгое правило. Таблица 8.3. Значение db_file_multiblock_jead_count изменяет стоимость использования индекса Значение db_file_multiblock_read_count Рассчитанная стоимость 4 В 16 32 64 $0 81 82 83 84 131 141 155 170 191 201 199 200 202 200 Однако для получения разумных приближенных значений я предпочитаю Придерживаться предположения, что оптимизатор использует разделение 80/20 между кластеризованными и распределенными записями как встроенное Правило для работы с битовыми индексами. Я подозреваю, что эта формула идентична формуле для скорректированного значения dbf_mbrc (см. главу 2) для изменения стоимости использования бито- вого индекса в расчетах; но обычно необходимо только разумное приближение, чтобы определить, почему план выполнения игнорируется или используется, и на данный момент мне вполне подходит основное приближение по принципу 80/20. Комбинации битовых индексов Вели мы продолжим рассматривать модель 80/20 и будем считать, что все наши Предположения будут по большей части правильными, какую точность мы по- дучим в более сложных запросах? Давайте начнем с простой операции bitmap and в сценарии bitmap_cost_02.sql из онлайн-хранилища кода: select small_vc from tl here nl = 2 -- одна запись на 20 and пЗ = 2 -- одна запись на 25 Мы начнем со стандартных правил бинарных деревьев, которые мы узнали. Ивдекс на столбце nl имеет 20 уникальных значений и 60 листовых блоков, индекс столбце пЗ имеет 25 уникальных значений и 63 листовых блока; оба индекса ШМеют значение blevel, равное. 1, которое в результате будет проигнорировано.
220 Глава 8. Битовые индексы Стоимость сканирования индексов для нахождения нужных битовых значений будет равна cei li ng(60/20) + сеi Ii ng(63/25): результат равен 6, мы его уве- личим до 6,6 из-за множителя битового индекса, равного 1,1. Если бы данные были действительно распределены случайным образом, ка- ждый из запросов вернул бы одну запись из 500 (20 х 25), что значит 20 запи- сей из доступных 10 000 — несложный расчет показывает, что мы увидим план выполнения с кардинальностью, равной 20. Используем приближение 80/20. Тогда 4 из этих записей будут широко рас- пределены (что в данном случае эквивалентно четырем отдельным блокам), а 16 записей будут кластеризованы; но в среднем у нас есть 9 записей на блок, то есть нам понадобится два блока для этих 16 записей, что в сумме дает шесть блоков. Итак, мы предсказываем, что общая стоимость будет равна round(6.6 + 6) = = 13. Запустив запрос с включенной автотрассировкой, мы получим следую- щие результаты: План выполнения (версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL ROWS (Cost=13 Card=20 Bytes=340) 1 0 TABLE ACCESS (BY INDEX ROWID? OF 'Tl' (Cost=13 Card=20 Bytes=340) 2 1 BITMAP CONVERSION (TO ROWIDS) 3 2 BITMAP AND 4 3 BITMAP INDEX (SINGLE VALUE) OF 'T1_I3' 5 3 BITMAP INDEX (SINGLE VALUE) OF 'T1_I1' Для подтверждения результатов мы можем проверить трассировку 10053, чтобы получить следующее: Access path: index (equal) Index: Tl II TABLE: Tl RSC_CPU: 0 RSC_IO: 3 IX_SEL: 5.0000e-002 TB_SEL: 5.0000e-002 Access path: index (equal) Index: T1_I3 TABLE: Tl RSC_CPU; 0 RSC_IO: 3 IX_SEL: 4.0000e-002 TB_SEL: 4.0000e-002 ******** Bitmap access path accepted ******** Cost: 13 Cost_io: 13 Cost_cpu: 0.000000 Selectivity: 0.002000 Not believed to be index-only. BEST_CST: 12.B7 PATH: 20 Degree: 1 Получив лучшее значение стоимости, равное 12,87, мы видим, что наша оценка, равная 12,66, не была идеальной, но наши значения для индекса были верными и оценка близка к реальной (видимо, оптимизатор использовал для обращения к таблице значение стоимости, равное 6,27, вместо 6 — возможно, здесь проявляется необычное использование значения db_file_multiblock_ read_count). Здесь необходимо указать на небольшую деталь при просмотре планов вы- полнения, в которых выполняется операция bitmap and: похоже, индексы рас- ставляются в порядке селективности, то есть самый селективный (тот, который больше всего уменьшает выборку) идет первым. Вероятно, это отражается на механизме времени выполнения, так как это условие может позволить механиз-
Комбинации битовых индексов 221 jMty выполнения уменьшить количество фрагментов битового индекса, которые ему нужно получить и сравнить. Таким же образом, если мы выполним следующее: select <. small_vc from tl where n2 = 2 -- одна запись из 20, кластеризованные данные and п4 = 2 -- одна запись из 25, кластеризованные данные получим следующий план выполнения из автотрассировки: План выполнения (версия 9.2.0.6) .......................................................... J» SELECT STATEMENT Optimizer=ALL_ROWS (Cost=B Card=20 Bytes=340) 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=B Card=20 Bytes=340) 1 BITMAP CONVERSION (TO ROWIDS) > 2 BITMAP AND 4 3 BITMAP INDEX (SINGLE VALUE) OF 'T1_I4' 5 3 BITMAP INDEX (SINGLE VALUE) OF 'T1_I2' > В этом случае итоговое значение стоимости равно 8, на самом деле немного Меньше из-за размера индексов. Мы ожидаем получения 20 записей из табли- даг, так что наша оценка стоимости выборки из таблицы по-прежнему равна 6, НО индексы настолько малы, что стоимость обращения к каждому индексу со- ставляет всего лишь один блок на индекс, что в сумме дает 2, увеличиваясь за- тем до 2,2; и в самом деле, мы видим следующую информацию в последних Строках трассировки 10053: ******** Bitmap access path accepted ******** gst: В Cost_io: В Cost_cpu: 0.000000 Selectivity: 0.002000 t believed to be index-only. BEST_CST: B.47 PATH: 20 Degree: 1 Кстати, обратите внимание, что в этом фрагменте файла трассировки 8,47 = * 2,2 + 6,27, а в предыдущем фрагменте у нас было 12,87 = 6,6 + 6,27. Оптимиза- тор действительно умножает индексный компонент на 1,1 и выдает нецелочис- ЛбНное значение стоимости обращения к табличным блокам. г Использующееся здесь приближение 80/20 начинает выдавать все более не- верные результаты по мере увеличения размера данных и количества исполь- >емых индексов. Швысокое значение кардинальности |>рли у вас есть несколько битовых индексов в таблице, то Oracle может исполь- зовать такое их количество, которое ему необходимо, чтобы повысить эффек- тивность запроса, но он необязательно будет использовать все подходящие ин- дексы. Поэтому возникает важный вопрос: когда Oracle решает, что ему доста- точно и что не нужно использовать еще один битовый индекс? Примерно, без уче- та ошибок, ответ будет таким: когда стоимость сканирования еще одного набора Листовых блоков станет больше, чем уменьшение количества табличных бло- |рв, по которым нужно будет пройти.
222 Глава 8. Битовые индексы Например, предположим, что у вас есть таблица, в которой перечислены шесть свойств людей: пол (2 значения), цвет глаз (3 значения), цвет волос (7 значений), родной город (31 значение), возрастная группа (47 значений) и ра- бочая профессия (79 значений). Теперь рассмотрим пример в сценарии bitmap_ cost_03.sql из онлайн-хранилища кода, который создает таблицу размером 800 Мбайт (107 543 блока), хранящую информацию о 36 млн человек, а также выполняет запрос со следующим условием sex_code = 1 and eye_code = 1 and hair_code = 1 and town_code = 15 and age_code = 25 and work_code = 40 Если вы создали отдельные битовые индексы на всех шести столбцах, како- ва вероятность, что Oracle использует все шесть для этого запроса? Ответ — практически нулевая. На самом деле в моем тестовом примере Oracle использо- вал три индекса с наиболее высокой точностью и проигнорировал три индекса с меньшей точностью. РАСПРОСТРАНЕННАЯ ОШИБКА Классическим примером для объяснения битовых индексов является столбец, содержащий флаг мужского/женского пола. К сожалению, это пример битового индекса, которого, скорее всего, не будет. Что самое интересное, если вы хотите обратиться к этой таблице, чтобы подсчитать количество мужчин и женщин, то существуют версии Oracle, в которых можно быстрее создать индекс, выпол- нить запрос и удалить индекс, чем напрямую выполнять запрос с агрегатом к таблице. Кстати, это всего лишь наблюдение, а не стратегическое указание — создание индекса «на ходу» обычно не является хорошим решением. В версиях Oracle, которые не поддерживают переписыва- ние запроса, индекс может быть полезен для запросов, просто подсчитывающих вхождения, но в этом случае таблица итогов (summary table), объявленная как материализованное представление, будет более разумным решением. Почему столбец с очень маленьким количеством уникальных значений час- то является неудачным решением для битового индекса? Давайте еще раз рас- смотрим предыдущий пример (сценарий для создания полного теста может вы- полняться 30 минут и требует около 1,2 Гбайт дискового пространства). Шесть индексов должны иметь статистику, показанную в табл. 8.4 для версии 9.2.O.6. Таблица 8.4. Статистика индексов из примера с 36 000 000 записей Столбец blevel leaf_blocks distinct_keys leaf_blocks / dlstinct_keys (плюс blevel) sex_code 2 1340 2 670 (672) eye_code 2 2010 3 670 (672) halr_code 2 4690 7 670 (672) town_code 2 5022 31 162 (164) age_code 2 5241 47 112(114) work_code 2 56BB 79 72 (74)
Комбинации битовых индексов 223 Если вы вычислите среднее количество записей, идентифицирующихся только столбцами town_code, age_code и work_code, заданными в предыдущем примере, вы увидите, что оптимизатору хватит этих трех столбцов для ограни- чения количества обращений к таблице 36 000 000 / (31 х 47 х 79) = 313 запи- сями. Итак, с самым худшим возможным распределением данных (вместо пред- полагаемого разделения 80/20), чтобы получить эти записи, необходимо 313 обращений к табличным блокам. ' Даже если бы следующий индекс из лучших (ha 1 r_code с семью уникаль- цыми значениями) уменьшил бы количество обращений к таблице с 313 (в худ- шем случае) до 0, все равно было бы неэффективно его использовать, потому цто он бы потребовал обращения еще к 672 индексным блокам — а это намного больше, чем количество обращений к табличным блокам, которые были-бы сэкономлены. В качестве теста я использовал пакет dbms_stats для изменения значения leaf_block в индексе hair_code, и мне пришлось снизить это значение до 1127 блоков, уменьшив тем самым leaf_blocks / distinct_keys до 161 — для того, чтобы оптимизатор посчитал, что индекс имеет смысл использовать. (Если таблица была бы отсортирована сначала по h а 1 r_code, то индекс содержал бы 675 листовых блоков вместо 4690, так что тест имел бы смысл.) — Учитывая время, требующееся для генерации данных, изменение статисти- ки с помощью пакета dbms_stats (см. сценарий hack_stats.sql из онлайн-храни- даща кода) пару десятков раз для нахождения нужной точки перехода является разумным решением. Хотя это решение ясно показывает искусность оптимиза- тора в выборе битовых индексов, этот пример также показывает большое коли- чество недостатков в моем приближенном алгоритме расчета стоимости. Оптимизатор выбрал всего три упомянутых индекса и вывел общую стои- мость запроса, равную 314; но если вы проверите сумму (blevel + leaf_blocks х х эффективная селективность индекса) для этих индексов, то она будет равна 352 (164 + 114 + 74). Итоговое значение стоимости меньше, чем сумма его час- тей — и это до умножения стоимости на 1,1 и добавления стоимости обращения к таблице! , Мне понадобилось некоторое время, чтобы понять, что происходит, но, ко- гда я использовал пакет dbms_stats для изменения значения blevel у одного Ж используемых индексов, изменение стоимости не соответствовало изменени- ем, сделанным мною в значении blevel. Из-за этого я решил, что в итоговом выведенном значении стоимости не учитывалась статистика двух из трех ин- дексов, а трижды использовалось значение из индекса, требующего наимень- шей стоимости. .Примерная стоимость (ожидаемое значение равно 314) = '74 * 1,1 * 3 + (стоимость индекса с наименьшей стоимостью использования, умноженная на 3 и на 1.1) 6,В * 313/335 + (В0% записей, хранящихся по 335 записей на блбк) 0,2 * 313 = (20% записей ,• распределенных по отдельным блокам) /44,2 + 62,6 + 0,75 = 307,55 (ошибка в 2,1%) , Решение, при котором всегда берутся значения из индекса с наименьшей Стоимостью использования, кажется мне ошибкой, но это решение может быть
224 Глава 8. Битовые индексы намеренным. Как бы то ни было, оно имеет некоторые странные побочные эф- фекты. Когда я использовал процедуру dbms_stats. set_1ndex_stats для из- менения количества листовых блоков для индекса ha 1 r_code с 4690 до 1127, оптимизатор решил использовать этот индекс без необходимости указания под- сказки и выполнил операцию bitmap and по четырем индексам. Однако когда я выполнил этот измененный тест, стоимость использования четвертого индекса при выполнении запроса составила 336, тогда как стои- мость использования трех индексов перед этим составила лишь 314. Другими словами, оптимизатор сам выбрал дополнительный индекс, несмотря на то, что это увеличивало стоимость выполнения запроса — а ведь считается, что оптими- затор всегда выбирает наименее затратный план выполнения, если только не указана какая-либо подсказка. Когда я увидел эту аномалию, я решил: было бы рискованно предполагать, что это было реальное поведение оптимизатора, а не странный результат не- много измененного словаря данных — и поэтому пересоздал тестовый пример, отсортировав данные по столбцу hа 1 r_code, и попробовал еще раз (см. сцена- рий bitmap_cost_03a.sql из онлайн-хранилища кода). Это привело к тому же ре- зультату, что и при изменении статистики. Оптимизатор выбрал четыре индек- са для плана выполнения, даже несмотря на то, что он выводил информацию о том, что использование трех индексов было менее затратным, когда я удалял четвертый индекс или отключал его с помощью подсказки no_index(). Оче- видно, что в коде для расчета стоимости (или, возможно, в выводе стоимости) битовых индексов что-то не так. Чтобы быстро проверить теорию о том, что индексы с очень низкой карди- нальностью обычно не являются полезными, я выполнил исходный запрос еще несколько раз с разными подсказками для отключения некоторых индексов. Результаты, показанные в табл. 8.5, не совсем соответствовали моим ожида- ниям. Таблица 8.5. Изменение стоимости при отключении некоторых индексов Подсказка Использованные индексы Стоимость nojndex(tl 14) 16,15, В, 12 (work, age, hair, eyes) 428 no_lndex(tl 15) 16,14,13,12,11 (work, town, hair, eyes, sex) 485 no_index(tl 15 il) 16,14,13,12 (work, town, hair, eyes) 481 Как видите, как только я отключил индекс на столбце age_code (индекс 15 с 47 уникальными значениями), оптимизатор решил использовать индекс на столбце sex_code (индекс 11 с всего лишь двумя уникальными значениями). Несмотря на имевшие место ранее мои комментарии о том, что индекс на столбце со значениями пола, скорее всего, не будет использован, в данном слу- чае он был использован — возможно, потому, что оценка стоимости делалась на основе стоимости индекса 16 на столбце work_code, а не его собственной. Однако обратите внимание на то, что, когда мы отключаем индекс на столб- це sex_code, рассчитанная стоимость запроса становится существенно ниже. На самом деле реальная стоимость выполнения запроса также значительно
Комбинации битовых индексов 225 ниже, составляя 2607 логических операций ввода-вывода с использованием не- эффективного индекса и только 2285 без него. Конечно, у этого выбора может быть логическое обоснование — это может быть нужно для расчетов, которые я еще не тестировал. Битовые индексы обычно имеют очень маленький размер по сравнению с таблицей, на которой они созданы; например, мой самый большой индекс имел меньше 6000 блоков на таблице, состоящей из более чем 100 000 блоков. У вас может быть достаточ- но памяти для кэширования всех важных битовых индексов. При этом оптими- затору имеет смысл выполнить большое количество логических операций вво- да-вывода для битовых индексов вместо небольшого количества физических операций ввода-вывода для таблицы. В моем примере увеличение логических операций ввода-вывода с 2285 до 2607 позволило бы Oracle уменьшить количе- ство обращений к табличным блокам с 16 до 8. Эти дополнительные 320 логи- ческих операций ввода-вывода кажутся неплохой заменой 8 физическим опера- циям ввода-вывода. Тестовый пример выявил еще одно изменение в поведении — полученная стоимость запроса не изменилась настолько, насколько я изменял значение db_file_multiblock_read_count. Возможно, изменение было таким неболь- шим, потому что наборы данных стали больше. Возможно, эффект от ошибки (возможной ошибки) в обработке нескольких индексов является настолько су- щественным, что ошибка (возможная ошибка), связанная с db_file_ multi- block_read_count, при этом не видна. Возможно, причиной является некий специальный код для небольших наборов данных. В любом случае приятно уз- нать, что для больших наборов данных (для которых вы, скорее всего, и исполь- зуете битовые индексы) расчеты выглядят более стабильными, чем для неболь- ших. Столбцы со значениями NULL Всегда важно, чтобы Oracle знал как можно больше о ваших данных — особен- но важны столбцы, не позволяющие хранить значения NULL, так как они могут привести к разному поведению оптимизатора запросов. Битовые индексы явля- ются хорошим решением в этом случае. Рассмотрим следующий запрос, осно- ранный на исходном тестовом наборе данных. Обратите внимание на первое условие с неравенством (это фрагмент сценария bitmap_cost_04.sql из онлайн- хранилища кода). select small_vc from tl Where nl != 2 and n3 = 2 » В зависимости от наличия точных данных об объявлении таблицы вы обна- ружите, что существуют два возможных плана выполнения для этого запроса:
226 Глава 8. Битовые индексы План выполнения (версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL ROWS 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' 2 1 BITMAP CONVERSION (TO ROWIDS) 3 2 BITMAP MINUS 4 3 BITMAP INDEX (SINGLE VALUE) OF 'Tl 13' 5 3 BITMAP INDEX (SINGLE VALUE) OF 'T1_I1' План выполнения (версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL ROWS 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' 2 1 BITMAP CONVERSION (TO ROWIDS) 3 2 BITMAP MINUS 4 3 BITMAP MINUS 5 4 BITMAP INDEX (SINGLE VALUE) OF 'Tl 13' 6 4 BITMAP INDEX (SINGLE yALUE) OF 'Tl II' 7 3 BITMAP INDEX (SINGLE VALUE) OF 'T1_I1 Обратите особое внимание на две операции bitmap minus во втором плане выполнения. Этих операций две, потому что битовые индексы включают запи- си для ключей, полностью состоящих из значений null. ОПЕРАЦИЯ BITMAP MINUS Чтобы выполнить операцию bitmap minus, Oracle берет вторую битовую карту и инвертирует ее, ме- няя нули на единицы, а единицы на нули. Затем операция bitmap minus может быть выполнена как операция bitmap and с использованием инвертированной битовой карты. Если мы объявили столбец nl как not null, Oracle может выполнить запрос, найдя битовую карту для предиката nl = 2, инвертировав ее, а затем использо- вав результат в операции bitmap and с битовой картой для предиката пЗ = 2, что и дает нам первый план выполнения. Если мы не объявили столбец nl как not null, полученная на первом шаге битовая карта будет включать биты для записей, в которых значение столбца n 1 равно null, так что Oracle будет вынужден получить битовую карту для nl i s null, инвертировать ее и выполнить еще одну операцию bitmap andc промежу- точным результатом. Этот дополнительный шаг потребует выполнения дополнительной работы во время выполнения. Итак, если вы знаете, что в столбце не предполагается хранить значения NULL, то это еще одна причина для включения этого ограни- чения в определение таблицы. Обратите внимание, что я не указал значения стоимости в планах выполне- ния, — это было сделано специально, чтобы избежать путаницы. Версия 8i ра- ботает нормально и стоимость первого плана выполнения (с одним шагом bitmap minus) ниже, чем стоимость второго (с двумя шагами bitmap minus). К сожалению, похоже, что и версия 9i, и 10g считают, что запрос с двумя ша- гами bitmap minus имеет значительно меньшую стоимость, чем запрос с одним шагом bitmap minus. Это предположение может быть разумным, но, исходя из статистики, показывающей отсутствие значений NULL для соответствующего
Комбинации битовых индексов 227 столбца, шанс уменьшить количество обращений к таблице совсем небольшой. Опять же, есть некоторые условия, при которых алгоритмы расчета стоимости битовых индексов не совсем верны. Со значениями NULL и битовыми индексами есть и другие проблемы. Похо- же, есть ошибка в механизме bitmap or, который теряет количество значений NULL в столбце, когда два битовых индекса связаны выражением OR (см. сцена- рий bitmap_or.sql из онлайн-хранилища кода), create table tl as with generator as ( select --+ materialize rownum id from all_objects where rownum <= 3000 ) select /*+ ordered use_nl(v2) */ decode( mod(rownum-l,1000). 0, rownum - 1, null ) nl, decode( mod(rownum-l,1000), 0, rownum - 1, null ) n2, lpad(rownum-1.10,'0') small_vc from generator vl, generator v2 where rownum <= 1000000 create bitmap index tl_il on tl(nl); create bitmap index tl_12 on tl(n2); -- Сбор статистики с помощью dbms_stats select small_vc from tl where nl = 50000 select small_vc from tl Where nl = 50000 Or n2 = 50000 /* (nl = 50000 and nl is not null) Or (n2 » 50000 and n2 is not null) */
228 Глава 8. Битовые индексы В этом примере присутствует ровно 1000 записей, в которых столбец nl не содержит значений NULL, и в столбце ровно 1000 уникальных значений. Oracle может увидеть эту информацию в статистике, полученной при вызове gather_ table_stats(). Столбец п2 идентичен столбцу nl. В первом запросе оптимизатор определит, что столбец n 1 имеет плотность, равную 1/1000, с общим количеством записей, не содержащих значение NULL, равным 1000, и решит, что кардинальность будет равна 1. Если вы поменяете столбец nl на п2 в этом запросе, рассчитанная кардинальность будет такой же. Но когда вы выполните запрос с предикатом where nl = 50000 or n2 = 50000 рассчитанная кардинальность будет равна 1999. Похоже, Oracle рассчитал плот- ность, исходя из общего количества записей в таблице, кроме записей со значе- ниями NULL. Итак (с помощью стандартной формулы для операции and двух предикатов), у нас будет 1/1000 от миллиона записей, полученных для преди- ката nl, плюс 1/1000 от миллиона записей, полученных для предиката п2; ми- нус одна запись из-за частичного совпадения. Добавьте один предикат isnotnull,H рассчитанная кардинальность умень- шится до 1000; добавьте второй предикат is not null,и кардинальность умень- шится до 1 (правильное значение). Оценка стоимости использования ресурсов процессора Любое обсуждение стоимости битовых индексов было бы неполным без рас- смотрения того, что произойдет при включении оценки стоимости ресурсов процессора (CPU costing). Учитывая, что стоимость ресурсов процессора является стратегическим по- казателем, хорошим решением является подготовка к тому моменту, когда вы должны будете включить оценку этой стоимости. Вы можете предположить, что оценка стоимости ресурсов процессора имеет большее влияние на планы выполнения с использованием битовых индексов (и с превращением индексов со структурой бинарного дерева в битовые индексы) из-за конвертирования битов в идентификаторы записей и обратно. Мы используем ту же системную статистику, какую мы использовали в гла- ве 4 (см. сценарий bitmap_cost_05.sql из онлайн-хранилища кода): alter session set "_optimizer_cost_model" = cpu; begin dbms_stats.set_system_stats('MBRC,8); dbms_stats.set_system_stats('MREADTIM',20); dbms_stats.set_system_stats('SREADTIM',10); dbms_stats.set_system_stats('CPUSPEED',350); end; I alter system flush shared_pool;
Оценка стоимости использования ресурсов процессора 229 С этими значениями мы можем выполнить пару запросов, которые мы ис- пользовали ранее в этой главе с нашим исходным набором данных, и посмот- реть, что изменится. Например, ниже показаны планы выполнения запроса к столбцу п4 (кластеризованные данные, 25 уникальных значений, битовый ин- декс) и столбцу пб (кластеризованные данные, 25 уникальных значений, ин- декс со структурой бинарного дерева) с их исходной стоимостью и с их стоимо- стью при включенной оценке стоимости ресурсов процессора — все планы получены в версии 9.2.0.6: План выполнения - битовый индекс на кластеризованном столбце 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=114 Card=400 Bytes=5600) 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=114 Card=400 Bytes=5600) 2 1 BITMAP CONVERSION (TO ROWIDS) 3 2 BITMAP INDEX (SINGLE VALUE) OF 'T1_I4' План выполнения - битовый индекс на столбце с распределенными данными с включенной оценкой стоимости ресурсов процессора 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=128 Card=400 Bytes=5600) 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=128 Card=400 Bytes=5600) 2 1 BITMAP CONVERSION (TO ROWIDS) 3 2 BITMAP INDEX (SINGLE VALUE) OF 'T1_I4' План выполнения - индекс co структурой бинарного дерева на кластеризованном столбце 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=54 Card=400 Bytes=5600) 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=54 Card=400 Bytes=5600) 2 1 INDEX (RANGE SCAN) OF 'T1_I6' (NON-UNIQUE) (Cost=9 Card=400) План выполнения - индекс co структурой бинарного дерева на кластеризованном столбце с включенной оценкой стоимости ресурсов процессора 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=55 Card=400 Bytes=5600) 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=55 Card=400 Bytes=5600) 2 1 INDEX (RANGE SCAN) OF ’T1_I6’ (NON-UNIQUE) (Cost=10 Card=400) Похоже, что наше предположение верное. Стоимость запроса с битовым ин- дексом возросла на 14 единиц, стоимость запроса с индексом со структурой би- нарного дерева возросла всего лишь на 1. Конечно, мы знаем, что стоимость за- проса с индексом со структурой бинарного дерева была рассчитана на основе Обращения примерно к 45 табличным блокам, по сравнению со стоимостью за- проса с битовым индексом на основе обращения примерно к 100 табличным блокам, но при обращении к таблице не важно, использует ли Oracle индекс Со структурой бинарного дерева или битовый индекс. Итак, основное разли- чие в стоимостях так или иначе вызвано битовым индексом... что мы и пред- полагали! Именно здесь, во-первых, автотрассировка действительно показывает свои недостатки и, во-вторых, нам преподносится неприятный сюрприз. Если мы по- строим нужный план выполнения (explain plan), то получим отдельные столбцы
230 Глава 8. Битовые индексы 1 o_cost и cpu_cost (еще более подробную информацию можно узнать из трас- сировки 10053). О Стоимость индекса со структурой бинарного дерева, равная 55, получается из io_cost = 54 и компонента cpu_cost = 1 (cpu_cost = 577 401). О Стоимость битового индекса, равная 128, полученная из io_cost = 127 и ком- понента cpu_cost = 1 (cpu_cost = 1 063 986). Из этих значений можно выделить следующую информацию: во-первых, ре- альная стоимость ресурсов процессора практически удвоилась, что примерно в 2 раза превышает рассчитанное количество операций ввода-вывода (114 вме- сто 54) — получение доступа к буферизованному блоку требует интенсивного использования ресурсов процессора. Но процессору нужно обрабатывать рас- ширение битового индекса, что не сильно отличается от двоичного поиска по индексным блокам. Во-вторых, что более существенно, код для расчета стоимости операций вво- да-вывода для битовых индексов дает другие значения стоимости операций ввода-вывода, чем значения, полученные при включенной оценке стоимости ре- сурсов процессора — начальная стоимость для нашего примера была равна 114, но возросла до 127. В общем случае при включенной оценке стоимости ресур- сов процессора стоимость растет, и это может стать причиной двух значитель- ных последствий. Во-первых, когда вы включаете оценку стоимости ресурсов процессора, у некоторых из ваших запросов могут абсолютно измениться пла- ны выполнения, потому что другие планы могут оказаться менее затратными. Также некоторые из ваших планов выполнения на первый взгляд могут остать- ся неизменными (трудно заметить внезапное появление дополнительного бито- вого индекса), но на самом деле будут работать дольше, потому что Oracle будет использовать дополнительный битовый индекс там, где раньше это считалось не нужным. (Конечно, с учетом ошибок, описанных ранее, использование до- полнительного индекса может на самом деле уменьшить время выполнения не- которых запросов.) Но по-настоящему странным последствием включения оценки стоимости ресурсов процессора при использовании битовых индексов является то, что на результаты все еще влияют изменения параметра db_f 1 le_multiblock_read_ count. Это действительно похоже на ошибку, если сравнивать с оценкой стои- мости индексов со структурой бинарного дерева, где включение оценки стоимо- сти использования ресурсов процессора приводит к использованию значений mbrc и mreadtim для любых расчетов, основанных на многоблочном чтении. Если это ошибка, то она, конечно, будет исправлена, и после ее исправления не- которые планы выполнения изменятся. Интересные случаи В этом разделе я собрал некоторые наиболее неясные аспекты битовых индек- сов, чтобы вы просто о них знали. Однако во всех этих случаях расчеты не ме- няются, так что я не буду тратить много времени на описание подробностей.
Интересные случаи 231 Индексы, построенные на нескольких столбцах Все примеры в данной главе до этого момента содержали индексы, построен- ные на одном столбце, что, вероятно, отражает работу большинства рабочих систем. Так как самое большое преимущество использования битового индекса заключается в том, что вы можете комбинировать столбцы в произвольном по- рядке, редко возникает потребность в указании нескольких столбцов в индексе. Однако у вас может быть пара столбцов, которые всегда запрашиваются од- новременно. Например, эти столбцы могут быть зависимы друг от друга или, возможно, содержать важные значения статуса. В зависимости от того, как разные значения столбцов распределяются по таблице, вы можете обнаружить, что один битовый индекс на паре столбцов имеет меньший размер, чем сумма размеров двух отдельных битовых индексов. В таких случаях хорошим решением может быть создание индекса на несколь- ких столбцах. Расчеты, использующиеся для индексов на нескольких столбцах, не отлича- ются от уже увиденных. Вы просто применяете формулу В. Брейтлинга (она Приведена в главе 4) к предикатам, относящимся к этому индексу, для расчета количества листовых блоков индекса, к которым будет произведено обращение, а затем для расчета части таблицы, к которой будет произведено обращение (таким образом, все это вы уже знаете из описания работы с обычными индек- сами со структурой бинарного дерева). Далее вы объединяете селективности таблицы, полученные из каждого индекса, для итогового расчета той части таб- лицы, к которой вы собираетесь обратиться, рассчитываете количество записей и применяете стандартную формулу разделения 80/20 для битовых индексов. Битовые индексы соединения Одним из интересных улучшений битовых индексов в версии 9i стало добавле- ние битового индекса соединения (bitmap join index, BJI) — индекса, который мо- жет хранить точки входа для записей данных из одной таблицы, а ключевые значения из другой таблицы (или таблиц). Например (см. сценарий bitmap_ cost_06.sql в онлайн-хранилище кода): create bitmap index fct_dim_name on fact_table(dim.dim_name) from dim_table dim, fact_table fct where dim.id = fct.dim_id Create bitmap index fct_dim_par on fact_table(dim.par_name) from dim_table dim, fact_table fct where dim.id = fct.dim_id
232 Глава 8. Битовые индексы Эти объявления индексов демонстрируют два потенциальных преимуще- ства. О Первый пример демонстрирует индекс на очень большой таблице фактов, использующей длинное имя измерения, которое, конечно, не должно хранить- ся в таблице фактов несколько миллионов раз. О Второй пример демонстрирует индекс, который может обратиться к таблице фактов с помощью запроса к атрибуту таблицы измерений, которая (как предполагает имя столбца) может иметь гораздо меньше уникальных значе- ний, чем идентификаторов измерения, и, таким образом, может иметь на- много меньший и более эффективный индекс (мы предполагаем, что также существует атрибут, часто применяемый пользователями для идентифика- ции и суммирования данных). Лично я не уверен, что битовый индекс соединения работает намного лучше, чем простые битовые индексы, но я могу представить, что в некоторых случаях такое решение может быть более эффективным. Однако, рассмотрев детально то, как и зачем создается битовый индекс со- единения, можно понять, что использующиеся расчеты остаются неизменными (хотя оптимизатор вместо использования индекса все еще может рассматривать вариант выполнения соединения). Рассмотрим следующий запрос: select count(fct.id) from dim_table dim, fact_table fct where dim.par_name = 'Parent_001' and fct.dim_id = dim.id План выполнения (автотрассировка, версия 9.2.0.6) SELECT STATEMENT Optimizer=ALL_ROWS (Cost=2149 Card=l Bytes=9) SORT (AGGREGATE) TABLE ACCESS (BY INDEX ROWID) OF 'FACT.TABLE' (Cost=2149 Card=10000 Bytes=90000) BITMAP CONVERSION (TO ROWIDS) BITMAP INDEX (SINGLE VALUE) OF 'FCT_DIM_PAR' Как видите, Oracle решил, что этот запрос возвратит 10 000 записей (Card ~ = 10 000). Применение обычного разделения 80/20 дает нам 2000 широко рас- пределенных по таблице записей и 8000 плотно упакованных записей. Рассмот- ренная нами таблица содержит 1 000 000 записей и 30 303 блока, то есть по 33 за- писи на блок. Таким образом, для обращения к данным нужно 2000 + 8000/33 = 2242 об- ращения к блокам, что ненамного превышает полученную стоимость, равную 2149, особенно когда вы добавляете значение, примерно равное 7, на обращения к индексным блокам. Как бы то ни было, это значение попадает в диапазон, ко- торый может быть получен при настройке значения db_file_multiblock_ read_count, так что меня не очень волнует эта разница.
Интересные случаи 233 Если вам интересно, даже в версии 9i оптимизатор рассчитывает стоимость соединения двух таблиц перед оценкой стоимости этого доступа к одной таб- лице. Трансформации битовых индексов Последнее замечание о расчетах стоимости битовых индексов касается двух Трансформаций, которые могут иметь место при использовании битовых ин- дексов. В основе обеих трансформаций лежит то, что мы все время рассматри- вали в этой главе — часто возникающая в планах выполнения строка bitmap conve rsion (to rowids), то есть конвертация битовых индексов в идентифика- торы записей. Битовый индекс по сути является искусно сжатым двухмерным массивом единиц и нулей. Каждый столбец в массиве соответствует одному из уникаль- ных значений ключа индекса, каждая строка — определенной записи в таблице. В Oracle есть простые расчеты, которые можно представить как «Строка X в массиве соответствует записи N в блоке М таблицы; и наоборот, запись Р в блоке Q таблицы соответствует строке Z в массиве». Операция превращения строки массива в запись таблицы является операци- ей конвертации битовых значений и может использоваться для конвертации в обе стороны в любом месте плана выполнения. Обычно мы видим использова- ние этой операции для конвертации массива в таблицу, bi tmap conve rs ion (to rowi ds), прямо перед получением данных из таблицы, но есть и другие вариан- ты использования. Один из этих вариантов хорошо известен, и на самом деле он вызывает неко- торые проблемы при переходе с версии с 8i на версию 9i: конвертация индекса со структурой бинарного дерева в битовый индекс (В-tree to bitmap conversion), ко- торая в плане выполнения показывается как bitmap conversion (from ro- wi ds) — конвертация идентификаторов записей в битовые значения. Напри- мер: select small_vc from tl where nl = 33 and n2 = 21 План выполнения (версия 9.2.0.6) SELECT STATEMENT Optimizer=ALL_ROWS (Cost=206 Card=400 Bytes=6B00) TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=206 Card=400 Bytes=6B00) BITMAP CONVERSION (TO ROWIDS) BITMAP AND BITMAP CONVERSION (FROM ROWIDS) INDEX (RANGE SCAN) OF ’Т1_1Г (NON-UNIQUE) (Cost=41) BITMAP CONVERSION (FROM ROWIDS) INDEX (RANGE SCAN) OF 'T1_I2' (NON-UNIQUE) (Cost=41)
234 Глава 8. Битовые индексы В этом примере (см. сценарий bitmap_cost_07.sql из онлайн-хранилища кода) у нас есть два индекса со структурой бинарного дерева на таблице tl. Оптими- затор решил, что ни один из них (tl_i 1 на столбце nl и tl_12 на столбце п2) не является настолько эффективным, как обращение к таблице, но он рассчитал, что количество строк, получающихся в результате комбинирования двух пре- дикатов, будет небольшим. Поэтому оптимизатор выбрал вариант, который использует индекс t l_i 1 для получения набора идентификаторов записей с nl = 33 и индекс tl_i2 для получения набора идентификаторов записей с п2 = 21. Но идентификаторы за- писей всегда могут быть превращены в массив битовых значений. Итак, опти- мизатор превращает каждый список идентификаторов записей в битовый мас- сив с помощью операции bitmap conversion (from rowids), после чего он получает два массива, которые можно использовать в операции bitmap and та- ким же образом, как если бы они были получены из пары обычных битовых ин- дексов. С этого момента план выполнения является всего лишь обычным пла- ном с битовыми индексами: мы конвертируем биты обратно в идентификаторы записей с помощью операции bitmap conversion (to rowids) и обращаемся к таблице. Арифметика, используемая оптимизатором, просто объединяет несколько уже знакомых нам вещей — стоимость сканирования индекса по диапазону (пе- ред обращением к таблице), комбинированную селективность двух предикатов и правило распределения 80/20, относящееся к битовым индексам. О Оба наших индекса имеют 1947 листовых блока, значение blevel, равное 2, и 50 уникальных значений, так что компонент индекса формулы В. Брейт- линга равен 2 + cei 1(1947/50) = 41. О Селективность двух предикатов равна 1/50, так что комбинированная селек- тивность равна 1/2500. Так как в таблице примера хранится 1 000 000 запи- сей, кардинальность результата равна 1 000 000/2500 = 400. О Согласно правилу 80/20, 320 записей будут плотно упакованными, а 80 бу- дут широко распределены. 1 000 000 записей в тестовой таблице находится в 17 855 блоках — по 56 записей на блок — что дает в сумме 5,7 блоков упа- кованных данных и 80 блоков распределенных данных. О Общая стоимость должна быть примерно равна (41 х 1,1 х 2) + 80 + 5,7 = = 175,9. К сожалению, 175,9 не очень похоже на полученное значение, равное 206. Поэтому пришлось опять вернуться к экспериментам. После нескольких про- стых экспериментов я начал менять значение blevel индекса, используя dbms_ stats. set_i ndex_stats, и обнаружил, что когда я увеличиваю значение blevel одного из индексов на 10, стоимость запроса возрастает на 14. Я начал двигаться в этом направлении, изменяя количество leaf_blocks, чтобы стои- мость сканирования индекса по диапазону увеличилась на 10 и, конечно, стои- мость запроса увеличилась на 14. Похоже, есть еще один фактор, который связан с конвертациями индекса со структурой бинарного дерева в битовый индекс. Итак, на основе моих цифр стоимость этого плана выполнения должна выглядеть следующим образом:
Интересные случаи 235 5иш(сканирование индекса по диапазону * 1.4) + блоки для 20% распределенных записей + блоки для В0% плотно упакованных записей = 41 * 1.4 * 2 + В0 + 5,7 =200,5 Это гораздо больше похоже на нужное нам значение, равное 206, и находит- ся в пределах той везде встречаемой вариации, которую мы получаем при изме- нении значения db_f ile_multiblock_read_count, так что я остановлюсь на этом приближенном значении и перейду к другой трансформации битового ин- декса. Вторую трансформацию битового индекса я нашел совсем недавно (в вопро- се на сайте AskTom по адресу http://asktom.oracle.com). Как и в случае с многими планами выполнения в Oracle, когда я его увидел, стало очевидно, что я должен был знать о возможности существования такого плана (случай «ретроспекции 20-20»), но я никогда не ожидал, что он реально будет создан. Если Oracle мо- жет производить конвертацию записей битового индекса в идентификаторы за- писей и обратно, то нет никаких технических запретов на то, чтобы делать это в любом месте плана выполнения, так что следующий план выполнения абсо- лютно логичен (см. сценарий bitmap_cost_08.sql из онлайн-хранилища кода): select dl, count(*) from tl where nl = 2 and dl between to_date('&m_today', 'DD-MON-YYYY') and to_date(’&m_future','DD-MON-YYYY') group by dl План выполнения (версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=4B Card=4 Bytes=44) 1 0 SORT (GROUP BY) (Cost=48 Card=4 Bytes=44) 2 1 VIEW OF 'index$_join$_001' (Cost=41 Card=B01 Bytes=BBll) 3 2 HASH JOIN 4 3 BITMAP CONVERSION (TO ROWIDS) 5 4 BITMAP INDEX (RANGE SCAN) OF 'Tl Dl' 6 3 BITMAP CONVERSION (TO ROWIDS) 7 6 BITMAP INDEX (SINGLE VALUE) OF 'T1_N1' Обратите особое внимание на то, что мы начинаем с двух битовых индексов, Получаем данные из нескольких листовых блоков по очереди, а затем превра- щаем результаты в индекс со структурой бинарного дерева, расположенный В памяти. Так как у нас есть два раздела индекса со структурой.бинарного дере- ва, мы можем выполнить между ними индексное соединение хэширования. Ко- нечно, этот пример является противоположностью предыдущего примера, где мы начинали с индексов со структурой бинарного дерева, получали данные из нескольких листовых блоков и превращали результаты в битовые индексы, рас- положенные в памяти.
236 Глава 8. Битовые индексы Как обычно, мы можем объединить все рассмотренное нами до этого, чтобы узнать, как этот план рассчитает итоговую стоимость. В данном случае нас больше всего интересует тот участок, в котором показаны результаты соедине- ния хэширования (представление index$_joi п$_001 в строке 2). Всякий раз, когда вам нужно решить подобные проблемы, обычно существу- ет несколько разных подходов, и самой простой демонстрацией того, что здесь действительно происходит, является выполнение запроса с включенной трасси- ровкой события 10053. Посмотрев файл трассировки, вы обнаружите, что Ora- cle просто рассчитывает стоимость обычного обращения к индексу для доступа к листовым блокам двух индексов, без какого-либо специального коэффициента и без учета дополнительной стоимости выполнения операции bitmap conversion (to rowids). To есть стоимость этого соединения хэширо- вания является всего лишь обычной стоимостью соединения хэширования (о ко- торых пойдет речь в главе 12). Альтернативным способом определить, что Oracle просто использует обыч- ную оценку стоимости индекса, является использование dbms_stats.set_ index_stats для настройки статистики индекса. Добавьте 10 к значению blevel одного индекса, и общая стоимость запроса возрастет на 10; измените значение в leaf_blocks индекса так, чтобы стоимость индекса возросла на 10, и это приведет к увеличению общей стоимости запроса на 10. Заключение Битовые индексы не имеют информации о распределении данных, так что оп- тимизатору приходится использовать некие абстрактные значения. Так как оп- тимизатор использует жестко заданные константы вместо реальной информа- ции, некоторые из ваших запросов неизбежно будут работать неправильно. Я еще не знаю точной формулы, которую использует Oracle для расчета стоимости доступа по битовому индексу, — в алгоритме расчетов даже могут быть ошибки, способные сделать оценку стоимости нестабильной. Я думаю, что замечаний и приближенных вычислений в этой главе должно вам хватить для разумного предположения, как ведет себя оптимизатор, но, похоже, есть стран- ный коэффициент настройки, зависящий от значения db_f ile_multiblock_ read_count. Когда вы переходите от традиционной оценки стоимости к оценке стоимо- сти, включающей оценку стоимости использования ресурсов процессора, вы можете увидеть, что некоторые планы выполнения сильно меняются, а другие в основном остаются неизменными, но работают медленнее из-за использова- ния дополнительного битового индекса (возможно, ненужного) для фильтра- ции данных. Когда вы объединяете битовые индексы, оптимизатор рассчитывает стои- мость на основе стоимости наименее затратного индекса вместо стоимости дей- ствительно использующихся индексов. Это может привести к странным побоч- ным эффектам, из-за которых некоторые запросы могут выполняться медленнее, так как станут использовать неподходящий набор индексов.
Тестовые сценарии 237 Возможно, что наблюдаемые ошибки в расчетах на самом деле являются не ошибками, а намеренно заложенной в дизайн функциональностью, чтобы вы- полнялось большое количество логических операций ввода-вывода на битовых индексах вместо выполнения небольшого количества физических операций ввода-вывода на таблице. В результате модель оценки стоимости может счи- тать, что ваши битовые индексы находятся в большом пуле KEEP, а соответ- ствующие таблицы — в маленьком пуле RECYCLE. Предупреждение: этот ком- ментарий очень спорный, так что не сильно на него полагайтесь. Внимательно отслеживайте в списке патчей любые исправления ошибок, от- носящиеся к оценке стоимости битовых индексов. Некоторые исправления ошибок могут сильно повлиять на производительность работы с вашими база- ми данных. Остовые сценарии Файлы к этой главе, доступные для загрузки, перечислены в табл. 8.6. Таблица 8.6. Тестовые сценарии к главе 8 Сценарий Комментарии bitmap_cost_01.sql Базовый сценарий, использующийся в этой статье для создания таблицы с шестью индексированными столбцами bitmap_mbrc.sql Пример влияния значения db_file_multiblock_read_count на стоимость битового индекса bitmap_cost_02.sql Запросы, объединяющие два индекса на базовой таблице из сценария bltmap_cost_01.sql bitmap_cost _03.sql Сценарий для генерации большой таблицы (800 Мбайт с 36 млн записей) для демонстрации бесполезности в общем случае битовых индексов на столбцах с очень низкой уникальной кардинальностью hack_stats.sql Тестовый сценарий, показывающий, как производить небольшие изменения в существующей статистике на уровне объектов bitmap_cost_03a.sql Повторяет сценарий bitmap_cost_03.sqi, но с сортировкой по столбцу hair_code bltmap_cost_04.sql Результаты использования столбцов со значениями NULL в операции bitmap minus bitmap_or.sql bitmap_cost_05.sql bltmap_cost_06.sql Wtmap_cost_07.sql Демонстрация ошибки в операции bitmap от Влияние включенной оценки стоимости ресурсов процессора Пример битового индекса соединения Пример плана выполнения для конвертации индекса со структурой бинарного дерева в битовый индекс, расположенный в памяти bitmap_cost_08.sql Пример конвертации битового индекса в индекс со структурой бинарного дерева, расположенный в памяти setenv.sql Установка стандартизированной тестовой среды для SQL*Plus
9 Трансформации запросов Пройдено уже восемь глав книги, а я еще даже не рассматривал соединение двух таблиц. Я не планирую обсуждать соединения и в следующей главе, пока не рассмотрю такую функциональность, как подзапросы, слияние представлений {view merging), уменьшение уровней вложенности запроса {unnesting) и транс- формация типа «звезда» {star transformation). Причина рассмотрения части наиболее сложной функциональности перед рассмотрением соединений заключается в том, что оптимизатор пытается ре- структурировать написанный вами SQL-код, в основном упрощая его и превра- щая в прямое соединение таблиц перед выполнением оптимизации. Это значит, что первым шагом в понимании того, как оптимизатор оценивает план выпол- нения, является определение структуры SQL-кода, который на самом деле оп- тимизируется. Так как в этой главе рассматриваются механизмы трансформа- ции запросов, в ней не будет много информации о стоимости. Ключевым моментом, который следует иметь в виду при рассмотрении этих трансформаций и странных последствий, с которыми вы иногда можете столк- нуться, является то, что код, который принимает решение о трансформации ва- шего SQL-кода, по-прежнему частично основывается на правилах (или, как го- ворится в руководствах, является эвристически управляемым, heuristically dri- ven). В одной версии базы данных вы можете увидеть определенный план вы- полнения, который был сгенерирован, потому что так было указано правилами. В дальнейшем вы обновляете версию и оптимизатор рассчитывает стоимость двух планов выполнения, одного с трансформацией, а другого — без, и выбира- ет план с наименьшей стоимостью — этот план может не совпасть с тем планом, который вы видели до обновления версии, и может работать медленнее. Похоже, что обычный жизненный цикл внутреннего кода для типичной трансформации запроса включает следующие этапы. О Состояние бета-версии. Внутренний код существует, и вы можете выпол- нить его с помощью скрытого (возможно) параметра или недокументиро- ванной подсказки.
Введение 239 О Первая официальная публикация. Внутренний код по умолчанию включен, но он не включается в оценку стоимости, так что трансформация произво- дится всегда. О Окончательное состояние. Оптимизатор рассчитывает стоимость исходно- го и трансформированного SQL-кода и выбирает код с наименьшей стоимо- стью. Подсказка становится устаревшей (как, например, hash_aj в версии 10g). В таких случаях вы можете посмотреть трассировку 10053, чтобы опреде- лить, в каком состоянии сейчас находится эта функциональность и ее код. Введение Мы начнем с примера, демонстрирующего, в каких разделах оптимизатора мо- жет изменяться код. Как обычно, мы используем 8 Кбайт в качестве размера блока в локально управляемых табличных пространствах с однородным разме- ром экстента, равным 1 Мбайт, не используем ASSM и отключаем системную статистику, чтобы тестовый пример был воспроизводимым (см. сценарий filter_ cost_01.sql в онлайн-хранилище кода), create table emp( dept_.no not null, sal, emp_.no not null, paddi ng, constraint e_pk primary key(emp_no) ) as with generator as ( select --+ materialize rownum id from all_objects where rownum <= 1000 ) select mod(rownum,6), rownum, rownum, rpad('x',60) from generator vl, generator v2 where rownum <= 20000 В этом сценарии я создал таблицу с 20 000 сотрудников, распределенными по 6 отделам. Каждый сотрудник имеет свой идентификатор и свой уровень за- работной платы. Пример кода демонстрирует функциональность версии 9i — механизм выноса подзапроса (с подзапросом), subquery factoring — которую я часто использую для генерации большого количества записей из источника,
240 Глава 9. Трансформации запросов где количество записей невелико (в сценарии версии 8i создается таблица для промежуточных данных). Теперь мы выполним запрос для вывода всех сотрудников, чья зарплата больше среднего уровня заработной платы в своем отделе, и проверим планы выполнения, полученные из автотрассировки в разных версиях Oracle. Чтобы показать важную информацию, в SQL-коде используется подсказка no_unnest, благодаря которой оптимизатор использует определенный план выполнения, потому что в этом случае изменение внутреннего кода приводит к сильным из- менениям рассчитываемой стоимости и кардинальности плана выполнения, select outer. * from emp outer where outer.sal > ( select /*+ no_unnest */ avg(inner.sal) from emp inner where inner.dept_no = outer.dept no ) План выполнения (версия 8.1.7.4) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=33 Card=1000 Bytes=72000) 1 0 FILTER 2 1 TABLE ACCESS (FULL) OF 'EMP' (Cost=33 Card=1000 Bytes=72000) 3 1 SORT (AGGREGATE) 4 3TABLE ACCESS (FULL) OF 'EMP' (Cost=33 Card=3334 Bytes=26672) План выполнения (версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=35035 Card=1000 Bytes=72000) 1 0 FILTER 2 1 TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=1000 Bytes=72000) 3 1 SORT (AGGREGATE) 4 3TABLE ACCESS (FULL) OF 'EMP1 (Cost=35 Card=3333 Bytes=26664) План выполнения (версия 10.1.0.4) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=245 Card=167 Bytes=12024) 1 0 FILTER 2 1 TABLE ACCESS (FULL) OF 'EMP' (TABLE) (Cost=35 Card=20000 Bytes=1440000) 3 1 SORT (AGGREGATE) 4 3TABLE ACCESS (FULL) OF 'EMP' (TABLE) (Cost=35 Card=3333 Bytes=26664) Обратите внимание, что стоимость простого табличного сканирования таб- лицы emp (строка 2) изменилась с 33 в версии 81 на 35 в версиях 91 и 10g. Одну единицу из разницы дает скрытый параметр _tablescan_cost_plus_one, зна- чение которого равно true в новых версиях Oracle и false в старых версиях.
Введение 241 Вторая единица возникает по причине немного различающихся размеров таб- лиц из-за изменения поведения параметра i ni trans. В версии 9.2 параметр хранения 1 ni trans для таблицы по умолчанию равен 2 (даже если вы установите его равным 1 или даже если в словаре данных будет указано, что он равен 1). Более того, когда вы выполняете команду create table as select, действительное количество слотов в ITL каждого из изна- чально созданных табличных блоков равно трем вместо двух, указанных в ini trans. В этом примере такого небольшого различия хватает, чтобы таблица етр стала на один блок больше, что приводит к увеличению стоимости ее ска- нирования на единицу. Обратите внимание, что во всех трех случаях планы выполнения одинаковы, но значения стоимости и кардинальности очень сильно различаются. Так как стоимость и кардинальность одной из строк плана выполнения могут повлиять на выбор оптимизатором порядков соединений и методов соединений для всего плана, согласитесь, что изменения в расчетах этой операции могут иметь значи- тельное влияние на выполнение более сложных запросов при обновлении вер- сии Oracle. Посмотрите на стоимость плана выполнения в версии 8i: похоже, что опти- мизатор просто забыл учесть стоимость подзапроса, так что стоимость запроса всего лишь состоит из полного сканирования управляющей таблицы. Стоимость, полученная в версии 9i, по крайней мере, более реальна. Компо- нент, равный 35 035, является стоимостью сканирования управляющей табли- цы (35) в строке 2. Согласно плану выполнения, количество записей (card =) при сканировании управляющей таблицы будет равно 1000 — так что подза- прос (который также является табличным сканированием) явно должен быть Выполнен 1000 раз: 1000 х 35 = 35 000. Сложив эти два значения, вы получите Необходимое значение, равное 35 035. Но откуда было получено значение 1000, которое оптимизатор рассчитал дак кардинальность сканирования управляющей таблицы? К сожалению, на другом шаге расчетов оптимизатор определил, что кардинальность окончатель- ного результирующего набора данных равна 1000. Причина в том, что единст- венным предикатом управляющей таблицы является salary > еще не извест- ный результат подзапроса, и селективность этого предиката была определена как 5 %, то есть как в случае столбец > : переменная_связывания. Однако обра- тите внимание на аргумент с циклом: окончательный результат будет содер- жать 1000 записей, так что оптимизатор предположил, что подзапрос, генери- рующий окончательный результат, будет выполнен 1000 раз. Наконец мы добрались до версии 10g — здесь стоимость выполнения запро- са равна 245. Также обратите внимание, что в строке 2 плана выполнения опти- мизатор выводит кардинальность сканирования управляющей таблицы, рав- ную 20 000 (количество записей в таблице), а не 1000. Так откуда же берется эта стоимость? Вы можете получить ответ, выполнив обратный расчет: 245/35 = 7. Стои- мость равна семи табличным сканированиям таблицы етр. Вычтите единицу на сканирование управляющей таблицы, и вы сможете сделать вывод: оптимизатор решил, что общее влияние подзапроса будет эквивалентно шести табличным
242 Глава 9. Трансформации запросов сканированиям. Почему? Потому что именно это предсказывает оптимизатор в качестве реальной работы, которую должен будет выполнить механизм вы- полнения. Вспомните, что у нас шесть отделов, для каждого из которых нам нужно было найти среднюю зарплату, — версия 10g достаточно «интеллектуальна», чтобы определить, что подзапрос нужно выполнить только шесть раз, создавая в памяти ссылочную таблицу результатов (мы рассмотрим эту функциональ- ность позже в разделе «Оптимизация фильтрации»). Вы даже можете увидеть такое поведение в строке 4 плана выполнения, в которой оптимизатор выпол- няет полное табличное сканирование для подзапроса и выводит кардиналь- ность, равную 3333, то есть равную одной шестой таблицы. Итоговое значение кардинальности, равное 167, является несколько неожи- данным. Но когда я впервые увидел это значение, то подумал, что вряд ли это является совпадением, — если вы возьмете 5 % от 20 000 (вспомните, что селек- тивность предиката столбец > : переменная_связывания равна 5 %) и раздели- те результат на 6 (количество отделов), то получится 167. Поэтому я повторил мой тестовый пример с восемью отделами — и получил кардинальность, рав- ную 125, что равно 1000/8. Оптимизатор выполнит деление на связанный, но ненужный коэффициент, когда выполняет операцию фильтрации (это лучше, чем могло бы быть — в версии 10g есть скрытый параметр _optimizer_ correct_sq_select1v1ty, значение которого равно true; измените его на false, и оптимизатор будет использовать 5 % для переменной связывания вто- рой раз для расчета кардинальности, уменьшая ее до 50, вместо использования коэффициента группировки из подзапроса). Обратите внимание, что вне зависимости от используемой вами версии Oracle, оптимизатор рассчитал неверную результирующую кардинальность. Во многих типичных наборах данных количество записей со значением, большим, чем среднее, будет примерно равно половине записей (хотя один из английских профсоюзных активистов вошел в историю, потому что заявлял, что он не успо- коится, пока каждый рабочий не будет зарабатывать выше среднего уровня). Так как в нашей таблице етр содержится 20 000 записей, то оценка оптимизато- ра в 1000 записей в версиях 8i и 9i ниже в 10 раз, а в версии 10g — в 60 раз. Ко- нечно, проблема находится в коде; оптимизатор только видит подзапрос, он не «понимает» специфического влияния функции получения среднего значения, которая вставлена в этот подзапрос. Эволюция Итак, из наших экспериментов с фильтрацией мы обнаружили, что версия 8i не имеет информации о стоимости, 9i использует аргумент с циклом для расчетов стоимости, а версия 10g имеет информацию о стоимости, но выдает такую кар- динальность, которая является самой неверной для данного примера. Мы вернемся к подзапросам позже в этой главе. Важный момент, который я хотел показать с помощью именно этого примера, заключается в том, что в об- ласти трансформации запроса видны большие эволюционные изменения, когда вы работаете с разными версиями оптимизатора.
Фильтрация 243 фильтрация По мере эволюции оптимизатора Oracle все чаще может избегать использова- ния подзапросов с помощью различных трансформаций, возможно, делая опе- рацию фильтрации, которую мы видели ранее, почти ненужной. Однако суще- ствуют случаи, в которых подзапросы не могут или, возможно, не должны быть трансформированы, так что эти случаи нужно рассмотреть. ЗНАЧЕНИЕ ПОНЯТИЯ «ФИЛЬТРАЦИИ» Понятие «фильтр» применимо ко многим различным типам операций: например, при выполнении операции исключения таблицы (table elimination) в секционированных представлениях, оценки вы- ражения having в запросах с агрегатами и оценки коррелированных подзапросов, рассмотренных в этой главе. Пока в таблице plan-table в версии 9i не появился столбец filter_predlcates, иногда было сложно точ- но определить, что означает строка filter в плане выполнения. Убедитесь в том, чтб вы знаете, что на данный момент находится в таблице plan-table. Иногда новая версия дает вам дополнительную информацию в новых столбцах. Рассмотрим следующий SQL-код, взятый из сценария push_subq.sql в он- лайн-хранилище кода: select /*+ push_subq */ par.small_vcl, chi.small_vcl from parent par, child chi where par.idl between 100 and 200 and chi.idl = par.idl and exists ( select /*+ no_unnest */ null from subtest sub where > sub.small_vcl = par.small_vcl and sub.idl = par.idl and sub.small_vc2 >= '2' В этом коде выполняется соединение двух таблиц по принципу «родитель- ская таблица/дочерняя таблица» — данные были созданы таким образом, чтобы каждая родительская запись имела восемь дочерних записей. Запрос использу- ет подзапрос на основе значений из родительской таблицы для исключения не- которых данных (подсказка no_unnest заставляет версии 9i и 10g повторять поведение по умолчанию версии 8i). Когда оптимизатор не может обернуть подзапрос в основное тело запроса, Выполняемая подзапросом проверка производится на самой последней стадии выполнения. В этом примере подзапрос обычно выполняется после соединения
244 Глава 9. Трансформации запросов с дочерней таблицей, при этом (потому что я так создал этот пример) будет вы- полнено большое количество ненужных соединений родительских записей с дочерними, которые следовало бы исключить перед выполнением соедине- ния, и объем выполненной работы будет больше необходимого (также возмож- но, что в более общих случаях подзапрос может выполняться гораздо чаще, но этого может и не произойти из-за специальной оптимизации фильтрации, опи- санной далее в этой главе). В подобных случаях вы можете захотеть изменить поведение по умолчанию. Для этого вы можете использовать подсказку push_subq, как я сделал в этом примере, чтобы оптимизатор сгенерировал план выполнения, который выпол- няет подзапрос на самой ранней стадии. С включенной автотрассировкой для вывода плана выполнения и статисти- ки выполнения мы можем увидеть, как меняется план и статистика при исполь- зовании подсказки (я вывожу результаты только по той статистике, в которой произошли изменения): План выполнения (версия 8.1.7.4, БЕЗ подсказки push_subq - подзапрос выполняется в конце) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=22 Card=7 Bytes=126) 1 0 FILTER 2 1 NESTED LOOPS (Cost=22 Card=7 Bytes=126) 3 2TABLE ACCESS (BY INDEX ROWID) OF 'PARENT' (Cost=4 Card=6 Bytes=54) 4 3 INDEX (RANGE SCAN) OF ’PAR_PK' (UNIQUE) (Cost=2 Card=6) 5 2TABLE ACCESS (BY INDEX ROWID) OF 'CHILD' (Cost=3 Card=817 Bytes=7353) 6 5 INDEX (RANGE SCAN) OF 'CHI_PK' (UNIQUE) (Cost=2 Card=817) 7 1 TABLE ACCESS (BY INDEX ROWID) OF 'SUBTEST' (Cost=2 Card=l Bytes=14) 8 7INDEX (UNIQUE SCAN) OF 'SUB_PK' (UNIQUE) (Cost=l Card=l) Statistics 1224 consistent gets План выполнения (версия 8.1.7.4, С подсказкой push_subq - подзапрос выполняется в начале) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=22 Card=7 Bytes=126) 1 0 NESTED LOOPS (Cost=22 Card=7 Bytes=126) 2 1 TABLE ACCESS (BY INDEX ROWID) OF 'PARENT' (Cost=4 Card=6 Bytes=54) 3 2INDEX (RANGE SCAN) OF 'PAR_PK' (UNIQUE) (Cost=2 Card=6) 4 2TABLE ACCESS (BY INDEX ROWID) OF 'SUBTEST' (Cost=2 Card=l Bytes=14) 5 4 INDEX (UNIQUE SCAN) OF 'SUB_PK' (UNIQUE) (Cost=l Card=l) 6 1 TABLE ACCESS (BY INDEX ROWID) OF 'CHILD' (Cost=3 Card=817 Bytes=7353) 7 6INDEX (RANGE SCAN) OF 'CHI_PK' (UNIQUE) (Cost=2 Card=817) Statist!cs 320 consistent gets Как видите, количество логических операций ввода-вывода существенно снизилось. Если вы тщательнее проанализируете сделанную работу, вы обнару-
фильтрация 245 Жите, что другие показатели проделанной работы (использование ресурсов Процессора, значения latch gets, 'buffer is pinned count') также уменьши- лись, так что преимущество, которое вы видите в значении consistent gets, является действительно преимуществом (хотя стоимость двух планов выполне- ния для версий 9i и 10g практически отражает это уменьшение, как видите из предыдущего примера, оба плана выполнения имеют ту же стоимость в вер- сии 8i). Обратите внимание на операцию фильтрации в строке 1 первого плана вы- полнения с дочерними операциями в строках 2 и 7. Это значит, что в строке 2 соединяется слишком большое количество записей, а затем каждая запись, воз- вращаемая соединением, проверяется в подзапросе (хотя оптимизация фильт- рации, описанная далее в этой статье, делает стоимость выполнения этого шага Незначительной). Во втором плане выполнения нет явной операции фильтрации, но обратите Внимание, что вложенный цикл в строке 1 все еще имеет таблицы parent И chi Id в качестве дочерних операций (строки 2 и 6). Однако в строке 4 появ- ляется таблица subtest в качестве дочерней операции по отношению к строке 2, которая является табличным доступом к таблице parent. На самом деле план выполнения не показывает нам всю правду. Вам придет- ся переключиться в версию 9i и посмотреть представление v$sql_plan_ Statistics, чтобы понять, что в действительности происходит — и даже тогда Вы получите искаженную информацию о плане выполнения. Следующий план Выполнения получен из v$sql_plan после выполнения запроса с подсказкой push_subq, но во втором и третьем столбцах показаны значения last_starts И last_output_rows из динамического представления с данными о производи- тельности v$sql_plan_stati sties: ID Starts Rows Plan (версия 9.2.0.6 - v$sql_plan_statisties - с подсказкой push_subq) 0 SELECT STATEMENT (all_rows) 1 1 8 TABLE ACCESS (analyzed) CHILD (by index rowid) 2 1 10 NESTED LOOPS 3 1 1 TABLE ACCESS (analyzed) PARENT (by index rowid) 4 1 101 INDEX (analyzed) PAR_PK (range scan) 5 101 1 TABLE ACCESS (analyzed) SUBTEST (by index rowid) Л 101 101 INDEX (analyzed) SUB_PK (unique scan) 1 1 8 INDEX (analyzed) CHI_PK (range scan) Обратите внимание, что строка 6 выполняется 101 раз, возвращая 101 за- вись, а затем строка 5 выполняется 101 раз, возвращая только одну запись. Но Почему строка 6 выполняется 101 раз? Потому что строка 4 возвращает 101 за- пись, а затем каждая запись проверяется в подзапросе. Подзапрос появляется ВЛ самом раннем этапе — во время чтения листового блока индекса — и только одна запись индекса успешно проходит фильтрацию, приводя только к одному обращению к таблице. В принципе, я думаю, что план выполнения должен выглядеть следующим Образом, чтобы вы могли лучше понять, что происходит:
246 Глава 9. Трансформации запросов ID Starts Rows Plan (гипотетический) 0 SELECT STATEMENT (all_rows) 1 1 8 TABLE ACCESS (analyzed) CHILD (by index rowid) 2 1 10 NESTED LOOPS 3 1 1 TABLE ACCESS (analyzed) PARENT (by index rowid) 4 1 1 FILTER 5 1 101 INDEX (analyzed) PAR_PK (range scan) 6 101 1 TABLE ACCESS (analyzed) SUBTEST (by index rowid) 7 101 101 INDEX (analyzed) SUB_PK (unique scan) 8 1 8 INDEX (analyzed) CHI_PK (range scan) Однако я не уверен, что вас устроит план выполнения, показывающий стро- ку между таблицей и индексом, который был использован для доступа к табли- це. Возможно, именно поэтому эта строка была исключена. На самом деле меха- низм, использующийся по умолчанию для этого запроса в версиях 9i и 10g, абсолютно отличается от механизма версии 8i, так что проблема, как и где пока- зывать фильтры, может оказаться неактуальной в будущих версиях. ПОДСКАЗКА ORDERED_PREDICATES В таких случаях иногда используется подсказка ordered_predicates. Однако она не дает возможно- сти оптимизатору выполнять подзапрос перед соединением, ее целью является контроль порядка применения предикатов, когда оптимизатор сообщает: «Сейчас я нахожусь на таблице X и у меня есть N предикатов одной таблицы, которые на данный момент можно использовать». Сценарий ord_pred.sql в онлайн-хранилище кода демонстрирует эту функциональность. Кстати, часто утверждается (и так даже написано в большинстве версий «Grade Performance and Tuning Guide»), что подсказка ordered_predicates срабатывает после where. Это неверно — она сра- батывает после select, как и любая другая подсказка. К сожалению, подсказка ordered_predicates в версии 10g является устаревшей. Оптимизация фильтрации В первом примере этой главы — в котором выбираются все сотрудники, зараба- тывающие больше среднего по отделу — я показал, что версия 10g определила, что коррелированный подзапрос нужно выполнять только 6 раз, и, таким обра- зом, рассчитала оптимальную стоимость выполнения запроса. На самом деле модель расчетов, использующаяся в версии 10g, отражает ме- тод времени выполнения, который в Oracle использовался годами, — как мини- мум с появления версии 8i. План выполнения проверяет, выполняется ли под- запрос для каждой записи, возвращаемой из управляющей таблицы (как это было в версии 7.3.4 и как до сих пор описывался этот механизм в «Performance Guide and Reference», Part A96533-01, версия 9.2, с. 2-13, где говорится: «В этом примере для каждой записи, удовлетворяющей условию внешнего запроса, вы- полняется коррелированный подзапрос с EXISTS»), но код времени выполне- ния на самом деле гораздо более экономичен, чем этот метод. Как я отмечал ранее, Oracle попытается выполнить подзапрос только шесть раз, по одному разу на отдел, запоминая каждый результат для дальнейшего ис- пользования. В версии 8i вы можете узнать это, только выполнив запрос и по- смотрев количество табличных сканирований логических операций ввода-вы- вода и просканированных записей в статистике сессии. Начиная с версии 9i
фильтрация 247 (выпуск 2), вы можете выполнить запрос с параметром statistics_level, ус- тановленным в all, для получения всей статистики, а затем обратиться напря- мую к v$sql_plan_stati sties для получения количества раз, которое выпол- нилась каждая строка плана выполнения. Создавая различные тестовые примеры (см., например, сценарий filter_cost_ O2.sql в онлайн-хранилище кода), можно определить, что примерно происходит в фильтре подзапроса, и я верю, что механизм времени выполнения делает это каждый раз, когда получает значение из управляющей таблицы: if это первая запись, выбранная из управляющей таблицы выполнить подзапрос со значением из этой управляющей таблицы сохранить значение из управляющей таблицы (входное) и возвращаемое значение (выходное) как 'текущие значения' установить для 'текущих значений' статус 'еще не сохранены', else if значение из управляющей таблицы соответствует входному значению из 'текущих значений' вернуть выходное значение из 'текущих значений' else if статус 'текущих значений' равен 'еще не сохранены’ попытаться сохранить ’текущие значения' в хэш-таблицу в памяти if происходит хэш-коллизия исключить 'текущие значения' из рассмотрения end if end if зондировать хэш-таблицу, используя новое значение из управляющей таблицы if новое значение из управляющей таблицы находится в хэш-таблице получить сохраненное возвращаемое значение из хэш-таблицы в памяти сохранить эти значения как 'текущие значения' установить для 'текущих значений' статус 'ранее сохраненные' else выполнить подзапрос с новым значением из управляющей таблицы сохранить значение из управляющей таблицы и возвращаемое (выходное) значение как 'текущие значения' установить для 'текущих значений’ статус 'еще не сохранены', end if вернуть выходное значение из 'текущих значений’ end if end if Самый главный вывод, который можно сделать из этого, заключается в том, что полученный план выполнения не обязательно является точным представле- нием происходящего. На самом деле он, вероятно, будет выводить правдивые результаты в большем количестве случаев, чем фильтр подзапроса, — содержи- мое плана выполнения понятно и просто; не всегда можно точно выразить про- исходящее и при этом сохранить такой уровень простоты и ясности в плане вы- полнения. В этом примере присутствует пара интересных побочных эффектов. Oracle ограничивает размер хэш-таблицы, находящейся в памяти (предположительно, Чтобы остановить чрезмерное потребление памяти в неудачных случаях). В версиях 8i и 9i ограничение размера хэш-таблицы равно 256 хэш-группам, В версии 10g это ограничение, похоже, равно 1024 хэш-группам.
248 Глава 9. Трансформации запросов Это значит, что на производительность фильтра подзапроса могут повлиять количество различных существующих управляющих значений, порядок, в кото- ром они появляются во время прохождения по управляющей таблице, и сами значения. Если хэш-таблица просто слишком мала или у вас есть управляющие значения, которые вызывают дополнительные коллизии в хэш-таблице, то ваш подзапрос может выполняться гораздо чаще, чем нужно. В качестве простой демонстрации попробуйте выполнить следующий код с данными, созданными сценарием для первого тестового примера этой главы (см. сценарий filter_cost_01a.sql в онлайн-хранилище кода). update emp set dept_no = 67 -- set dept_no = 432 -- Первое значение коллизии в версии 91 -- Первое значение коллизии в версии 10g, в котором rownum = 1 select /*+ no_merge(iv) */ count(*) from ( select outer.* from emp outer where outer.sal > ( select /*+ no_unnest */ avg(inner.sal) from emp inner where 1nner.dept_no = outer.dept_no ) ) iv Так как я хочу выполнить этот запрос, а не просто посмотреть его автотрас- сировку, я обернул исходный запрос в представление, чтобы подсчитать коли- чество записей в результирующем наборе. Подсказка no_merge используется для того, чтобы оптимизатор не был слишком «интеллектуальным» и не транс- формировал запрос перед подсчетом количества. Я хочу убедиться, что этот за- прос выполняет тот же объем работы, что и исходный запрос; count(*) во внешнем запросе применяется только для уменьшения объема вывода. В такой форме подсказка no_merge () относится к псевдониму представления, который находится в выражении from. БЛОКИРОВКА СОЕДИНЕНИЙ СЛИЯНИЯ Подсказка по_тегде иногда описывалась в литературе (возможно, даже в руководствах Oracle в оп- ределенное время) как нечто указывающее оптимизатору не выполнять соединение слияния. Это неверно. Подсказка по_тегде указывает оптимизатору не выполнять глубокую оптимизацию представлений (хранимых и подставляемых) и некоррелированных подзапросов. С технической точки зрения эта подсказка предотвращает слияние комплексных представлений (complex view merging). Если вы хотите заблокировать соединение слияния, прямого способа не существует. Только в вер- сии 10g появляется подсказка no use merge.
фильтрация 249 Проверьте, сколько времени выполняется count (*) перед обновлением, за- тем выполните его после обновления. На моей системе с версией 9i и процессо- ром с частотой 2,8 ГГц время выполнения запроса изменилось с 0,01 с до 14,22 с (имеется в виду только время работы процессора). Значение 67 приводит к хэш-коллизии со значением 0 в хэш-таблице. По- бочный эффект установки dept_.no равным 67 только лишь в первой записи И привел к тому, что значение средней зарплаты для dept_.no == 67 сохранилось в хэш-таблице, заблокировав возможность для Oracle сохранить среднюю зар- плату для dept_.no = 0, так что подзапрос будет выполняться 3332 раза, по од- ному разу на каждую запись для dept_.no == 0 (из-за того, что в версии 10g хэш-таблица меняет размер, значение 432 становится первым значением, у ко- торого происходит коллизия со значением 0). Я не думаю, что статистически лучшей стратегией было бы замещение старых значений во время возникнове- ния коллизий — в этом случае мне пришлось бы только немного изменить мой код, чтобы получить пример, в котором эта стратегия также приведет к интен- сивному использованию ресурсов процессора. Также обратите внимание, что алгоритм выполняет сравнение следующей записи с предыдущей, чтобы избежать поиска по хэш-таблице. Это значит, что интенсивность использования ресурсов процессора обычно несколько ниже, если записи из управляющей таблицы отсортированы. Более того, если вы от- сортируете записи из управляющей таблицы, то количество уникальных значе- ний станет несущественным, потому что подзапрос будет выполняться только во время изменения управляющего значения — что может дать вам возмож- ность использования ручной оптимизации в определенных случаях. Тот факт, что во время выполнения действительное потребление ресурсов Этим планом выполнения может очень сильно варьироваться из-за различных количества и порядка элементов данных, не учитывается в расчетах стоимости. Конечно, так говорить не очень честно: я не думаю, что есть возможность со- брать нужную информацию для эффективного расчета стоимости, так что в код должен быть встроен некий типичный сценарий. С другой стороны, важно по- нимать, что это один из тех случаев, когда стоимость и потребление ресурсов очень слабо связаны. Начиная с версии 91, вы, вероятно, будете реже видеть простую фильтрацию в подзапросе, так как уменьшение уровней вложенности запроса (контролируе- мое по большей части изменением значения по умолчанию параметра _unnest_ subquery) происходит гораздо чаще. Однако, как вы видели, операция фильт- рации может быть очень эффективной, и некоторые обнаружили, что после об- новления версии до 9i часть их SQL-кода стала работать гораздо медленнее, по- тому что эффективный фильтр был превращен в гораздо менее эффективное соединение после уменьшения количества уровней вложенности запроса (если Вы столкнетесь с этой проблемой, подумайте о подсказке no_unnest, которую я использовал в моих тестовых примерах для версий 9i и 10g). Вы также долж- ны помнить, что существуют случаи, в которых присутствие множества подза- просов делает уменьшение уровней вложенности запроса невозможным — если у вас есть такие случаи, то для каждого оставшегося подзапроса будет создана своя хэш-таблица.
250 Глава 9. Трансформации запросов Скалярные подзапросы Я выбрал скалярные подзапросы в качестве следующей темы для обсуждения, потому что их реализация тесно связана с реализацией описанной ранее опера- ции фильтрации. Было бы интересно узнать, появилась ли оптимизация фильт- рации из-за того, что разработчики создали скалярные подзапросы, или скаляр- ный подзапрос появился из-за метода оптимизации, созданного для фильтрации подзапросов. Рассмотрим следующую модификацию нашего исходного запроса (см. сце- нарий scalar_sub_01.sql в онлайн-хранилище кода): select count(av_sal) from ( select /*+ no_merge */ outer.dept_no, outer.sal, outer.emp_no, outer.padding, ( select avg(inner.sal) ' from emp inner where inner.dept_no = outer.dept_no ) av_sal from emp outer ) where sal > av_sal В этом запросе я написал подзапрос (коррелированный) в списке select другого запроса. Таким образом, мы ссылаемся на этот подзапрос как на ска- лярный подзапрос, то есть подзапрос, возвращающий одно значение (один столбец и не более одной записи — и, если записей для возврата нет, возвраща- ется NULL). В этом примере я также продемонстрировал альтернативную форму подсказки no_merge — подсказка встроена в представление, которое не должно участвовать в соединении слияния и, таким образом, не требует псевдонима. Так как подзапрос теперь является столбцом в основном списке select, то можно просто предположить, что он должен выполняться для каждой записи, возвращаемой основной командой select. Однако код ведет себя как код фильт- рации, и если мы проверим план выполнения, то поймем, почему это так: План выполнения (автотрассировка, версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=35035 Card=l Bytes=26) 1 0 SORT (AGGREGATE) 2 1 VIEW (Cost=35035 Card=1000 Bytes=26000) 3 2FILTER 4 3 TABLE ACCESS (FULL) OF ’EMP’ (Cost=35 Card=1000 Bytes=8000) 5 3 SORT (AGGREGATE) 6 5 TABLE ACCESS (FULL) OF ’EMP1 (Cost=35 Card=3333 Bytes=26664) Фрагмент в середине, очевидно, был превращен в тот же подзапрос с фильт- рацией, который использовался в нашем первом эксперименте. Обратите вни-
фильтрация 251 мание на одно забавное обстоятельство — этот план выполнения получен в вер- сии 9i, и в исходном эксперименте мне пришлось использовать подсказку по_ unnest, чтобы оптимизатор использовал предыдущий план выполнения. В этом же случае оптимизатор выполнил одну трансформацию, превратив скалярный подзапрос в подзапрос с фильтрацией, но не смог выполнить трансформацию, которая бы уменьшила количество уровней вложенности запроса, исключив Подзапрос с фильтрацией. В принципе, это одно из последствий подсказки no_merge, и некоторые другие запросы, похожие на выполненный мною при- мер, действительно трансформировались в соединения хэширования, когда я убрал подсказку. Однако ни один из них не трансформировался в версии 10g (даже с подсказкой unnest); более того, тестовый запрос прервал мою сессию в версии 9.2.0.6, когда я убрал подсказку no_merge. Возможно, в коде, обраба- тывающем скалярные подзапросы, по-прежнему необходимы некоторые усо- вершенствования. Если вы уберете выражение where в тестовом запросе, так чтобы результат подзапроса был выведен, но не был использован в предикате, вы обнаружите, цто, во-первых, строки 3, 5 и 6 исчезли — непонятно даже, был ли выполнен Подзапрос, код выполнил одно табличное сканирование, и, во-вторых, проде- ланная запросом работа по-прежнему равна выполнению скалярного подзапро- са только шесть раз. План выполнения (версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=35 Card=l Bytes=13) 1 0 SORT (AGGREGATE) 2 1 VIEW (Cost=35 Card=20000 Bytes=260000) 3 2TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=20000 Bytes=60000) Однако, чтобы убедиться, что такая оптимизация имела место, мы можем сделать что-то более впечатляющее (см. сценарий scalar_sub_02.sql в онлайн- хранилище кода). create or replace function get_dept_avg(1_dept in number) return number deterministic as m_av_sal number; begin select avg(sal) into m_av_sal from emp where dept_no = i_dept return m_av_sal; «nd; select count(av_sal) from ( select /*+ nojnerge */ dept_no, sal,
252 Глава 9. Трансформации запросов emp_no, paddf ng, (select get_dept_avg(dept_no) from dual) av_sal -- get_dept_avg(dept_no) av_sal from emp ) Я включил две возможные строки в финальный запрос: одна явно вызывает функцию get_dept_avg(), а другая вызывает ее, выполняя скалярный подза- прос, который обращается к таблице dual). ДЕТЕРМИНИСТИЧЕСКИЕ ФУНКЦИИ Детерминистическая функция, согласно требованиям Oracle, гарантирует возврат одного и того же результата при передаче одного и того же набора входных параметров. Если вы используете индек- сы на основе функций (fUnction-based indexes) — или, как они скорее должны называться, индексы с виртуальным столбцами,—то виртуальные столбцы должны быть описаны с помощью детермини- стических функций. Ключевым моментом детерминистических фукнций является то, что, если Oracle может определить, что текущий вызов функции использует тот же набор входных параметров, что и предыдущий, он может использовать предыдущие результаты и избежать вызова функции—так говорится в руково- дствах. Насколько я могу утверждать, эта особенность детерминистических функций никогда не была реализована. Но эта возможность может быть эмулирована скалярными подзапросами. Если вы выполните версию запроса, использующую скалярный подзапрос, она выполнится очень быстро, произведя подзапрос только шесть раз. Если вы выполните версию запроса, использующую прямой вызов функции, она будет работать гораздо дольше, вызывая функцию для каждой записи основного тела запроса (у быстрой версии есть та же проблема хэш-коллизий при выполнении нормального подзапроса, если вы измените значение столбца dept_no одной из записей таблицы етр на 67 или на 432 для версии 10g). Это фантастический выигрыш в производительности. Единственным недо- статком является то, что вы вообще не видите подзапрос в плане выполнения, идентичном показанному ранее, в котором скалярный подзапрос выводился как столбец результата, но не использовался в выражении where. План выполнения (автотрассировка, версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=35 Card=l Bytes=13) 1 0 SORT (AGGREGATE) 2 1 VIEW (Cost=35 Card=20000 Bytes=260000) 3 2TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=20000 Bytes=60000) Обратите внимание: абсолютно нет признаков, указывающих на что-то боль- шее в этом запросе, чем просто полное сканирование таблицы етр (и, конечно, последующее агрегирование). Однако действительно замечательной особенностью скалярных подзапросов является то, что с их помощью намного проще узнать что-то еще об ограниче- ниях в хэш-таблице. Вспомните мой комментарий в разделе «Оптимизация фильтрации» о том, что ограничение, похоже, равно 256 в версиях 8i и 9i и 1024 в версии 10g.
Фильтрация 253 Мне потребовалось проделать немало сложной работы, чтобы определить эти значения (а для версии 10g работа еще не закончена). Но посмотрите, что вы можете сделать со скалярными подзапросами: select (select packl.f_n(16) from dual) x from dual У нас есть скалярный подзапрос, вызывающий функцию пакета с одним чи- словым входным параметром (см. сценарий scalar_sub_03.sql в онлайн-хранили- ще кода). Определение пакета показано ниже — обратите особое внимание на глобальную переменную и способ, с помощью которого две функции увеличива- ют значение глобальной переменной перед возвратом их результата: create or replace package packl as g_ct number(10) := 0; function f_n(i in number) return number; function f_v(i in varcharZ) return varcharZ; end; / Create or replace package body packl as function f_n(1 in number) return number is begin packl.g_ct ;= packl.g_ct + 1; return i; end function f_v(i in varcharZ) return varcharZ is begin packl.g_ct := packl.g_ct + 1; return i; end end; / Если вместо использования скалярного подзапроса к таблице dual мы вы- полним подзапрос к таблице, содержащей числа от 1 до 16 384 по два раза (та- кой, как показано ниже), мы можем получить интересные результаты: .create table tl as with generator as ( select --+ materialize rownum id from all_objects where rownum <= 3000 ) select rownum nl, lpad(rownum,16,'0') vl6, lpad(rownum,3Z,'0') v3Z from generator vl, generator vZ where
254 Глава 9. Трансформации запросов rownum <= 16,384 t insert /*+ append */ into tl select * from tl select count(distinct x) from ( select /*+ no_merge */ (select packl.f_n(nl) from dual) x from tl ) Так как каждое значение с 1 по 16 384 находится в таблице (в числовой фор- ме и в двух символьных формах с лидирующими нулями), то когда мы выпол- ним скалярный подзапрос, у нас будет много хэш-коллизий в хэш-таблице. Но мы также собираемся быстро заполнить хэш-таблицу за один проход по 16 384 значениям, а затем повторно использовать сохраненных на диск значений по одному разу во время второго прохода по 16 384 значениям в таблице. Следова- тельно, после выполнения запроса значение глобальной переменной packl. g_ct будет равно не 32 768 записей в таблице, а меньше этого значения ровно на размер хэш-таблицы. Итак, нам нужно сделать следующее: execute packl.g_ct := 0; -- выполнение запроса execute dbms_output.put_liпе('Hash table size: ' II ( 32768 - packl.g_ct)) и мы напрямую получим размер хэш-таблицы; результат будет выглядеть сле- дующим образом 81 256 91 256 10g Зависит от входных параметров функции, выходных параметров функции и от значения параметра _query_execution_cache_max_size Обратите внимание, что мое определение пакета включает функцию, кото- рая принимает символьный параметр и возвращает символьное значение. Я сде- лал так, чтобы проверить, меняет ли хэш-таблица свое поведение при использо- вании больших символьных строк. В версиях 8i и 9i изменения количества хэш-групп хэш-таблицы при различных сочетаниях символьных и числовых входных и выходных значений не было. Но когда я выполнил версию запроса, которая передавала 32-символьную строку с лидирующими нулями и возвра- щала ту же строку, изменения в версии 10g были очень заметными: execute packl.g_ct := 0; select count(distinct x) from ( select /*+ nojnerge */ (select packl.f_v(v32) from dual) x
Фильтрация 255 from tl ) > execute dbms_output.put_line('Hash table size: ' II ( 32768 - packl.g_ct)) COUNT(DISTINCTX) 16384 Hash table size: 16 Как видите, размер хэш-таблицы уменьшился до 16. Используя substг () в качестве возвращаемого значения функции, я смог увеличить размер хэш-таб- лицы, уменьшив размер подстроки. Передавая символьные входные параметры В функцию, возвращающую числовое значение, я смог уменьшить размер хэш- таблицы, увеличив размер входной строки. Вероятно, размер хэш-таблицы кон- тролируется фиксированным значением предела памяти, а не общим количест- вом хэш-групп. Так как размер хэш-таблицы уменьшился до 16 во время использования функции, возвращающей строку неограниченной длины (что на самом деле озна- чает длину varchar2(4000)), я стал искать любой параметр со значением по умолчанию, равным 64 или 65 536, и нашел параметр _query_execution_ cache_max_size. Без сомнения, вы можете увеличить размер хэш-таблицы до Предела, равного 16 384 хэш-группам (размер всегда равен степени двойки), увеличивая значение этого параметра. Результаты выполнения этой серии тестов разочаровывают: после обновле- ния версии до 10g вы можете обнаружить, что некоторые запросы, включающие скалярные подзапросы или подзапросы с фильтрацией, выполняются намного Медленнее, даже если их планы выполнения не изменились, потому что значе- ния их входных и выходных параметров довольно длинные. В этом случае у вас есть два способа вручную улучшить производительность. Попытайтесь умень- шить общий размер входных и выходных значений (объединением строк, яв- ным извлечением подстрок) или изменить значение параметра _query_ execution_cache_max_size в сессии (однако, как обычно, вы должны знать, Что скрытые параметры лучше не использовать без одобрения технической под- держки Oracle, и не следует ожидать, что это решение будет стабильно работать Мосле следующего обновления версии). Механизм выноса подзапроса Я не буду много говорить о механизме выноса подзапроса {subquery factoring): вы уже видели его в действии несколько раз в этой книге и каждый раз я ис- пользовал подсказку, чтобы оптимизатор делал то, что я хочу, вместо принятия решения на основе оценки стоимости. Если мы возьмем запрос, который я использовал для создания первого примера в этой главе, и уберем подсказку, то он будет выглядеть следующим Образом:
256 Глава 9. Трансформации запросов with generator as ( select rownum id from all_objects where rownum <= 1000 ) select mod(rownum,6), rownum, rownum, rpad('x',60) from generator vl, generator v2 where rownum <= 20000 Основное тело запроса ссылается (дважды) на объект, который я назвал generator, и в начальной секции запроса производится определение объекта generator. Во время выполнения оптимизатор может выбрать: он может под- ставить текст определения объекта всякий раз, когда видит имя generator, или он может один раз создать временную таблицу для хранения результатов вы- полнения запроса, определяющего объект generator, и использовать эту вре- менную таблицу каждый раз, когда видит имя generator. Для механизма выноса подзапроса доступны две подсказки: при использова- нии подсказки material i ze оптимизатор создает временную таблицу, а при ис- пользовании подсказки inline оптимизатор заменяет имя определяющим объ- ект текстом и оптимизирует результирующий запрос. Ниже показаны два возможных плана выполнения, сгенерированные с по- мощью утилиты dbms_xplan в версии 10g для предыдущего запроса (см. сцена- рий with_subq_01.sql в онлайн-хранилище кода). Чтобы'сократить план выпол- нения, я заменил представление all_objects в определении объекта generator на таблицу tl, созданную следующим образом: select * from all_objects. План выполнения с подсказкой /*+ materialize */: Idl Operation | Name | Rows | Bytes | Cost 01 select STATEMENT 1 | 20000 1 1 97 И TEMP TABLE TRANSFORMATION | 1 1 1 1 2| LOAD AS SELECT 1 1 1 1 1*3| COUNT STOPKEY 1 1 1 1 1 4| TABLE ACCESS FULL 1 Tl | 44666 1 1 95 1*5| COUNT STOPKEY 1 1 1 1 1 6| NESTED LOOPS 1 | 20000 1 1 2 1 7| VIEW 1 1 1 1 1 1 8| TABLE ACCESS FULL | SYS TEMP | 44666 | 567K | 2 | 0FD9D662D 1 1 1 | 543F90 1 1 1 1 9| VIEW 1 | 20000 1 1 1 1101 TABLE ACCESS FULL | SYS_TEMP | 44666 | 567K | 2
фильтрация 257 | _0FD9D662D Illi I _543F90 Illi predicate Information (identified by operation id): 3 - filter(ROWNUM<=1000) 5 - fiIter(ROWNUM<=20000) План выполнения с подсказкой /*+ inline */: 1 Id | Operation Name | Rows Bytes | Cost | 1 0 | SELECT STATEMENT 1 11 1 3 | |* 1 | COUNT STOPKEY 1 1 1 I 2 | NESTED LOOPS 1 11 1 3 | 1 з | VIEW 1 1 1 1 1 |* 4 | COUNT STOPKEY 1 1 1 1 5 |TABLE ACCESS FULL Tl I 44666 1 95 | 1 6 | VIEW 1 11 1 2 | |* 7 | COUNT STOPKEY 1 1 1 1 8 |TABLE ACCESS FULL Tl I 44666 1 95 | Predicate Information (identified by operation id): 1 - fiIter(ROWNUM<=20000) 4 - filter(ROWNUM<=1000) 7 - fiIter(ROWNUM<=1000) Если вы посмотрите на столбец Rows в обоих планах выполнения, вы увиди- те, что числа в нем бессмысленны. Например, влияние использования выраже- ний rownum <= 1000 не было отражено на столбце, а второй план выполнения показывает итоговое количество записей равным 11 вместо 20 000, как должно быть. Более того, обратите внимание, что итоговая стоимость плана выполнения с подсказкой inline абсолютно неверна: каждое из двух сканирований табли- цы 11 имеет стоимость, равную 95, а общая стоимость выполнения запроса все равно выводится равной 3, хотя понятно, что она должна быть равна как мини- мум 190 (2 х 95). На самом деле, хотя план выполнения с подсказкой inline показан как План с самой низкой стоимостью, именно план выполнения, в котором для под- запроса создается временная таблица, используется в том случае, когда запрос выполняется без указания подсказок. Причиной этих аномалий является использование предиката rownum. Если ВЫ посмотрите трассировку 10053 по этим запросам, то обнаружите, что опти- мизатор (только в версии 10g) переключился в режим оптимизации first_ rows(n). В результате из-за моего последнего предиката rownum <= 20 000 оп- тимизатор произвел расчеты, которые были бы использованы, если бы я вклю- чил в запрос подсказку f i rst_rows(20 000). Файл трассировки показывает, что оптимизатор выполняет расчеты в два прохода: в первый раз он рассчитывает итоговую кардинальность, полученную с помощью соединения всего представления generator с самим собой (в моем
258 Глава 9. Трансформации запросов случае общее количество записей равно 1 995 239 224), а во второй раз рас- считывает коэффициент, равный 0,000010023905893 (который получен из 20 000/1 995 239 224). Я подозреваю, что это значит, что планы выполнения выводят значения, показывающие полную стоимость операции, и значения, показывающие мас- штабированную стоимость операции. К сожалению, моя уловка для генерации данных обнаружила сложную ошибку в выводе планов выполнения при исполь- зовании механизма выноса подзапроса. Немного развлечемся Одной из полезных особенностей механизма выноса подзапроса является то, что вы можете использовать подход «разделяй и властвуй» (также известный как «чистка лука», peeling the onion) для решения проблем. Раньше вы могли использовать подставляемые представления с подсказкой no_merge — но под- сказка no_merge несколько ограничивает возможности, потому что из-за нее оптимизатор превращает промежуточные данные во временные наборы данных и может слишком ограничивать возможности сложных трансформаций. При использовании механизма выноса подзапроса вы можете писать слож- ные запросы простыми частями и давать возможность оптимизатору выбирать, генерировать ли промежуточные наборы данных или создавать более сложную, расширенную форму запроса и оптимизировать ее. Этот метод я использовал для решения следующей головоломки, которую кто-то предложил в новостной группе comp.databases.oracle.server пару лет назад; в задаче нужно было написать одну команду SQL, выводящую пра- вильный результат без предварительного программирования ответа в команде SQL. Два математика встречаются на встрече выпускников колледжа и заводят разговор о своих семьях. Так как они математики, они не могут не говорить за- гадками. Математик X: У тебя есть дети? Математик У: Да, у меня есть три дочери. Математик X: И сколько им лет? Математик Y: Если ты перемножишь их возраст, то получишь 36. Математик X: Мало информации. Математик Y: Если ты сложишь их возраст, результатом будет количество людей в этой комнате. Математик X {оглядев комнату): Все еще мало информации. Математик Y: У моей старшей дочери есть хомячок с деревянной ногой. Математик X: Твои двухлетние дочери — однояйцевые близняшки или нет? Итак, как вы можете написать команду SQL, которая выводит возраст трех девочек и количество людей в комнате? Вы можете попытаться определить воз- раст и выяснить, сколько людей видел математик X в комнате, перед тем как читать дальше. Во-первых, вам нужно знать, что базовый синтаксис механизма выноса под- запросов учитывает следующие выражения:
Фильтрация 259 with aliasl as (subqueryl), alias? as (subquery?), aliasN as (subqueryN) select Итак, давайте решим математическую загадку (см. сценарий with_subq_02.sql в онлайн-хранилище кода). Сначала учтем, что произведение возраста дочерей равно 36. Мы можем предположить, что годы полные, и, таким образом, у нас есть минимум возрас- та, равный 1, и максимум, равный 36 — так что давайте сгенерируем таблицу с 36 записями, по одной на каждый возможный возраст: with age_list as ( select rownum age from all_objects where rownum <= 36 ). Но их три, так что нам понадобится три копии таблицы, и, когда вы пере- множите три возраста, результат должен быть равен 36 — после этого надо будет сложить три возраста, так что давайте сделаем это сразу. Обратите внимание, что предыдущая секция SQL-кода окончилась запятой, и следующая секция просто начинается с нового псевдонима (я не повторяю with): product_check as ( select agel.age as youngest, age?.age as middle, age3.age as oldest, agel.age + age?.age + age3.age as summed, agel.age * age?.age * age3.age as product from age_list agel, age_li st age?, age_list age3 where age?.age >= agel.age and age3.age >= age?.age and agel.age * age?.age * age3.age 36 ). Обратите внимание, что я избавился от перестановок каждого возможного результата, использовав предикат age2.age >= agel.age and age3.age >= Sge2.age. Это значит, что если, например, я перечислил (2, 3, 6), я не буду опять перечислять (6,3,2), (3, 2, 6) и оставшиеся три перестановки. На данный момент у нас есть все возможные варианты возрастов, произведение которых дает 36. Если мы сделаем выборку из таблицы product_check, мы получим сле- дующий результат: YOUNGEST MIDDLE OLDEST SUMMED PRODUCT 1 1 36 38 36
260 Глава 9. Трансформации запросов 1 2 18 21 36 1 3 12 16 36 1 4 9 14 36 1 6 6 13 36 ** 2 2 9 13 36 ** 2 3 6 11 36 3 3 4 10 36 Также мы знаем, что сумма возрастов равна количеству людей в комнате — но математик Y огляделся, подсчитал количество людей в комнате, и не смог получить результат! Поэтому неизвестное количество людей в комнате должно соответствовать по крайней мере двум записям в наборе данных product_ check — другими словами, должны быть хотя бы две записи, в которых сумма возрастов одинакова (посмотрите на записи, отмеченные символами **). По- этому давайте уменьшим результат из product_check, выбрав только эти запи- си, но не указывая их явно: summed_check as ( select youngest, middle, oldest, summed, product from ( select youngest, middle, oldest, summed, product, count(*) over(partition by summed) ct from product_check ) where ct > 1 ) В этом подзапросе я использовал аналитическую версию функции count (), выполняющую группировку по суммам возрастов (столбец summed). Количест- во будет больше единицы только для сумм, которые встречаются больше одно- го раза, так что на этот момент результат будет select * from summed_check YOUNGEST MIDDLE OLDEST SUMMED PRODUCT 1 6 6 13 36 2 29 13 36 Теперь мы знаем (после того, как математик Y огляделся), что в комнате на- ходилось 13 человек. Но математик Y все еще не мог выбрать одну из двух за- писей: пока он не услышал о хомячке (с деревянной ногой, что абсолютно не важно в данном случае), — и проблема была решена обычным (по крайней мере, в Британии) предположением о возрасте близняшек. У старшей дочери есть хо- мячок, но в первой строке нет старшей дочери — есть младшая дочь и две стар- шие близняшки. А во второй записи — старшая дочь и две младшие близняшки, поэтому итоговый запрос должен выглядеть следующим образом: with age_list as ( select rownum age
фильтрация 261 from all_objects where rownum <= 36 ) product_check as ( select agel.age as youngest. age2.age age3.age agel.age + as middle, as oldest, age2.age + age3.age as summed, agel.age * age2.age * age3.age as product from age_list age_li st age_li st where age2.age >; and age3.age >: and agel.age * agel, age2, age3 = agel.age = age2.age age2.age * age3.age = 36 summed_check as ( select youngest, middle, oldest, summed, product from ( select youngest, middle, oldest, summed, product, count(*) over(partition by summed) ct from product_check ) where ct > 1 ) select * from summed_check where oldest > middle Конечно, это не очень серьезный SQL-код (посетите веб-сайт Тома Кайта http://asktom.oracle.com, и посмотрите демонстрацию использования этой тех- нологии для организации турнира по гольфу, если хотите более практичный пример), но в нем показаны базовые принципы. Обратите внимание, что в этом примере я использовал каждый подзапрос в последующем подзапросе — это одна из многих возможностей. Есть требова- ние, что каждый именованный подзапрос должен использоваться по крайней мере один раз; иначе вы получите следующую ошибку Oracle: ORA-32035: unreferenced query name defined in WITH clause Но, соблюдая это требование, вы имеете много возможностей для использо- вания именованных подзапросов. Мой простой пример генерации большого на- бора данных использовал один и тот же подзапрос дважды — в общем случае подзапрос на любом уровне может использовать множество копий любого или ч даже всех подзапросов на более высоких уровнях (в DB2 подзапрос может даже ссылаться сам на себя, но в Oracle эта возможность еще недоступна).
262 Глава 9. Трансформации запросов Слияние комплексных представлений В главе 1 вы видели пример слияния комплексных представлений (см. сцена- рий view_merge_01.sql в онлайн-хранилище кода). Мы создаем агрегатное пред- ставление на таблице, а затем выполняем соединение этого представления с другой таблицей: create or replace view avg_val_view as select id_par, avg(val) avg_val_tl from t2 group by id_par select /* no_merge(avg_val_view) */ -- Добавьте символ «+» для подсказки блокировки слияния tl.vcl, avg_val_tl from tl, avg_val_view where tl.vc2 = lpad(18,32) and avg_val_view.id_par = tl.id_par У оптимизатора есть два варианта, для оптимизации этого запроса: создание результирующего набора данных для агрегатного представления и последую- щее его соединение с базовой таблицей или расширение представления для вы- полнения соединения двух базовых таблиц с последующим агрегированием со- единения: План выполнения (версия 9.2.0.6, с подсказкой для создания экземпляра представления) SELECT STATEMENT Optimizer=CHOOSE (Cost=15 Card=l Bytes=95) HASH JOIN (Cost=15 Card=l Bytes=95) • TABLE ACCESS (FULL) OF 'Tl' (Cost=2 Card=l Bytes=69) VIEW OF 'AVG_VAL_VIEW' (Cost=12 Card=32 Bytes=832) SORT (GROUP BY) (Cost=12 Card=32 Bytes=224) TABLE ACCESS (FULL) OF 'T2' (Cost=5 Card=1024 Bytes=7168) План выполнения (версия 9.2.0.6, co слиянием представления) SELECT STATEMENT Optimizer=CHOOSE (Cost=14 Card=23 Bytes=1909) SORT (GROUP BY) (Cost=14 Card=23 Bytes=1909) HASH JOIN (Cost=8 Card=32 Bytes=2656) TABLE ACCESS (FULL) OF 'Tl' (Cost=2 Card=l Bytes=76) TABLE ACCESS (FULL) OF 'T2' (Cost=5 Card=1024 Bytes=7168) Посмотрев файл трассировки 10053 (см. главу 14 для большей информации о файле трассировки стоимостного оптимизатора) для запроса без подсказки в версии 81, вы обнаружите две следующие секции general plans (из которых я вывожу только заголовки — обычно каждая из секций general plans начина-
Фильтрация 263 ется со строки Joinorder[l]). Видно, что в версии 8i была рассмотрена только возможность создания агрегатного представления и его соединения с первой таблицей. Также обратите внимание, что при оценке стратегий создания агре- гатного представления первая из секций general plans включает шаг пере- оценки стоимости для оценки использования подходящего индекса на таблице £2, чтобы рассчитать агрегат без выполнения сортировки, что приводит к до- полнительной операции Join order[1]. Join orderll]: T2 [T2] ****** Recost for ORDER BY (using join row order) ******* join order[l]: T2 [T2] Join order[l]: Tl [Tl] AVG_VAL_VIEW [AVG_VAL_VIEW] Join order[2]: AVG_VAL_VIEW [AVG_VAL_VIEW] Tl [Tl] Посмотрев файл трассировки для запроса без подсказки в версии 9i, вы об- наружите одну секцию general plans, показанную ниже. После обновления версии (после которого значение по умолчанию параметра _complex_view_ merging изменяется cfalsenatrue) оптимизатор рассматривает только вари- ант слияния представления и выполнения соединения перед агрегацией. Обра- тите внимание, что нет никакого упоминания о переоценке стоимости — для этой стратегии она не подходит. При указании подсказки no_merge() в файле трассировки просто происходит переключение на то, что делалось в версии 8i, хотя при этом не производится попытка переоценки стоимости для индекса на таблице t2 — тоже интересная деталь. Join order[l]: Т1[Т1]#0 Т2[Т2]#1 Join order[2] : Т2[Т2]#1 Т1[Т1]#0 Посмотрев файл трассировки для запроса без подсказки в версии 10g, вы об- наружите четыре секции general plans, показанные ниже. В этой трассировке видно, что оптимизатор рассчитал стоимость обоих вариантов, а затем выбрал вариант с наименьшей стоимостью. На самом деле здесь были выполнены два набора расчетов для соединения двух базовых исходных таблиц. Единственная разница между двумя последними наборами расчетов состоит в отсутствии стоимости для столбца соединения — и я не смог выяснить, насколько это явля- ется важным. Join order[l]: Т2[Т2]#0 Join order[l]: Т1[Т1]#0 AVG_VAL_VIEW[AVG_VAL_VIEW]#1 Join order[2]: AVG_VAL_VIEW[AVG_VAL_VIEW]#1 T1[T1]#0 Join order[l]: T1[T1]#0 T2[T2]#1 Join order[2]: T2[T2]#1 T1[T1]#0 Join order[l] : T1[T1]#0 T2[T2]#1 Join order[2]: T2[T2]#1 T1[T1]#0 Включение предикатов в представления При выполнении слияния представлений существуют некоторые ограничения, и они меняются в Oracle от версии к версии. Однако даже когда оптимизатор не позволяет выполнить слияние представления в основной части запроса, есть слу- чаи, когда при выполнении некоторых соединений с использованием вложенных циклов оптимизатор может включить предикат соединения в представление
264 Глава 9. Трансформации запросов (pushing predicates). Это приводит к множеству вариантов создания небольших экземпляров представления, а не к созданию одного большого экземпляра представления. Хотя я иногда вижу включенные в представления предикаты в планах выполнения, создать реальный пример оказалось не так просто, поэто- му я вернулся к ограничению оптимизатора для демонстрации этой возможно- сти (см. push_pred.sql в онлайн-хранилище кода). Включение предикатов чаще всего происходит при выполнении внешних соединений на многотабличных представлениях: create or replace view vl as select t2.idl, t2.id2, t3.small_vc, t3.padding from t2, t3 where t3.idl = tZ.Idl and t3.id2 = t2.id2 select tl.*, vl. * from tl, vl where tl.nl = 5 and tl.idl between 10 and 50 and vl.idl(+) = tl.idl План выполнения для этого запроса показывает включение предиката со- единения в представление. В этом примере, чтобы показать план, я использовал пакет dbms_xplan версии 9i, так как важны значения столбцов filter_ predicates и access_predicates из plan_table. lid | Operation | Name | Rows 1 | Bytes | Cost | 1 0 | SELECT STATEMENT 1 1 | 240 | 5 I 1 1 | NESTED LOOPS OUTER 1 1 | 240 | 5 I |*2 | TABLE ACCESS BY INDEX ROWID 1 Tl | 1 1 | 119 | 3 I |*3 | INDEX RANGE SCAN 1 Tl PK | 42 | 2 I 1 4 | VIEW PUSHED PREDICATE 1 Vl | 1 ! 1 121 I 2 I |*5 1 FILTER 1 6 | NESTED LOOPS 1 1 1 129 | 3 1 |*7 |INDEX RANGE SCAN 1 T2 PK | 1 \ 1 9 1 2 1 1 8 ITABLE ACCESS BY INDEX ROWID 1 T3 | 1 1 1 120 | 1 1 |*9 | INDEX UNIQUE SCAN 1 T3_PK | 1 1 1 Predicate Information (identified by operation id):
Общие подзапросы 265 2 - filter("Tl"."Nl“=5) 3 - access("Tl"."ID1">=10 AND "Tl"."ID1"<=50) 5 - filter("Tl"."ID1"<=50 AND "Tl"."ID1">=10) 7 - access("T2"."ID1"="T1"."ID1") filter("T2"."ID1">=10 AND "T2"."ID1"<=50) 9 - access("T3"."ID1"="T2"."ID1" AND "T3"."ID2"="T2"."ID2") filter("T3"."ID1">=10 AND "T3"."ID1"<=50) Обратите особое внимание на строку 4 с операцией включения предиката соединения в представление. Это важная строка, показывающая, что оптимизатор действительно выполняет включение предикатов соединения в представление. Также обратите внимание, что в плане показано много предикатов фильтра- ции и что четыре раза встречаются предикаты фильтрации вида соТХ >= 10 and colX <= 50. Предикаты фильтрации в строках 5, 7 и 9 действительно лишние и исчезают в версии 10g — на самом деле операция фильтрациии в строке 5 плана выполнения и сама полностью исчезает в версии 10g. Однако очень удобно, что эти предикаты появились в данном примере: это позволяет мне показать, что они являются не следствием включения предиката соединения в представление, а результатом переходного замкнутого выражения (transitive closure), и могут появиться даже в тех случаях, когда строка pushed predi cate отсутствует. Технически предикаты включения в представление являются предикатами соединения, которым в этом примере является предикат access в строке 7. Общие подзапросы Очень просто запросить информацию таким образом, что потребуются подза- просы. О Покажите мне падение продаж в магазине, который принес самую большую прибыль в июне. О Покажите мне номера телефонов всех клиентов, чей почтовый индекс нахо- дится в рекламном регионе Carlton. О Покажите мне список всех сотрудников, которые получают больше среднего в их отделе. В зависимости от того, как ставится вопрос, и от вашего знания структуры данных вы можете для этих запросов на естественном языке выбрать один из нескольких вариантов их выражения на SQL. В некоторых случаях вы можете решить выполнить прямой перевод с есте- ственного языка, потому что стратегия перевода ясна: понятно, что нужно сна- чала сделать X, а потом сделать Y, используя результат в структуре подзапроса. Например, для третьего вопроса нужно создать простой запрос для определе- ния средней зарплаты отдела, а затем использовать его для фильтрации запроса По таблице employees: Шаг 1: select avg(salary) from employees
266 Глава 9. Трансформации запросов where department = {X} Шаг 2: select * from employees empl where salary > ( select avg(salary) from employees emp2 where emp2.department = empl.department ) И наоборот, вы можете решить, что какой-то вопрос можно изменить так, чтобы создать более простую команду SQL, даже если эквивалент в естествен- ном языке не такой прямой. Рассмотрим пример с почтовым индексом — вы мо- жете начать с запроса по таблице, с помощью которого вы сможете определить все почтовые индексы рекламного региона Carlton, а затем использовать эти данные в качестве входных данных в более сложном запросе: Шаг 1: select distinct zip_code from tableX where agent = 'CARLTON1; Шаг 2: select tel from customers where zip_code in ( select /* distinct */ zip_code from tableX where agent = 'CARLTON' ) С другой стороны, возможно, в этом случае гораздо проще выполнить про- стое соединение таблиц, что является абсолютно подходящим и понятным ме- тодом получения информации, если считать, что мы знаем, что у каждого клиен- та не более одного почтового индекса и что этот факт включен в виде ограниче- ния в базу данных. (Обратите внимание на использование distinct в первом из двух предыдущих запросов. Механизм использования подзапроса в опера- ции in делает использование distinct неявным, вот почему я закомментиро- вал эту операцию во втором запросе.) select tel from tableX, customers where tableX.agent = 'CARLTON' and customers.zip_code = tableX.zip_code При переводе требований пользователей в SQL нам приходится определять, как мы выразим эти требования. Конечно, в идеале мы хотим выразить пробле-
Общие подзапросы 267 му наиболее близко к нашему естественному языку, но чтобы оптимизатор при этом мог выполнить результирующий запрос наиболее эффективным способом. ВНИМАНИЕ Время от времени я сталкиваюсь со случаями, когда программисты превращают соединения в под- запросы и подзапросы в соединения — и в результате получают команду SQL, которая логически не идентична исходной. Будьте очень осторожны при использовании подзапросов и переписывании кода- Посмотрим на ситуацию с другой точки зрения: если мы выполним следую- щий запрос в некой реляционной СУБД, что она должна сделать для его вы- полнения? select * from tableX where z_code in ('A','В','C'); Одним из вариантов является сканирование каждой записи в таблице tableX и проверка столбца z_code на соответствие 'А', 'В' или 'С, то есть операция фильтрации. Другим вариантом может быть использование подходя- щего индекса, с помощью которого можно будет «найти все ' А', затем найти все ' В', затем найти все ' С'» с очень низкой стоимостью — механизм, который я часто называю управляющей операцией (driving operation). Та же возможность существует, когда запрос становится несколько более сложным: select * from tableX where z_code in (другой запрос); Должна ли реляционная СУБД получить все записи из таблицы tableX и выполнять другой запрос для каждой записи — или она должна попробовать каким-либо образом выполнить другой запрос один раз и использовать его ре- зультирующий набор данных для управления выборкой данных из таблицы tableX? Если все, что я сказал в этом разделе, вам абсолютно ясно, то вы поняли все, что нужно знать (с концептуальной точки зрения) о трансформации подзапро- сов. Есть варианты написания SQL-кода, который красив, легок для понимания и интуитивно очевиден для говорящего на естественном языке, и есть варианты написания SQL-кода, дающие возможность механизму базы данных работать эффективно. Чем лучше оптимизатор может превращать одни команды SQL в другие эквивалентные команды SQL, тем меньше вам нужно будет работать над вашими запросами, чтобы они соответствовали требованиям механизма базы данных. Трансформация подзапросов является одной из развивающихся областей кода оптимизации в Oracle, которая здесь и рассматривается. Параметры подзапроса Чтобы показать, как сложно отслеживать, что происходит в подзапросах, и ка- кие специальные приемы будут или не будут работать, я создал небольшую таб- лицу параметров (табл. 9.1), относящихся к трансформации подзапросов (воз- можно, я некоторые пропустил — иногда по имени и описанию нелегко опреде- лить назначение параметра).
268 Глава 9. Трансформации запросов Таблица 9.1. Список параметров для обработки подзапросов продолжает изменяться Название Версия 8i Версия Si Версия 10g Описание _unnest_ notexists.sq Отсутствует single Отсутствует Уменьшает уровни вложенности запроса — исключает подзапросы not exists с одной или более таблицами, если это возможно _unnest_ subquery false true true Разрешает уменьшение уровней вложенности запроса (исключение коррелированных подзапросов) idctlpar_ordered_ semi-join true true true Разрешает отсортированные подзапросы с полусоединениями (exists) _cost_equality_ semiJoin Отсутствует true true Включает оценку стоимости полусоединений (exists) при проверке на равенство _always_antl_ join nestedjoops choose choose Всегда используйте этот метод для антисоединений (not exists), если возможно _always_semi_ join standard choose choose Всегда используйте этот метод для полусоединений (exists), если возможно „optimizer. correct_sq_ selectivity Отсутствует Отсутствует true Выполняет правильный расчет селективности подзапроса _optlmizer_squ_ bottomup Отсутствует Отсутствует true Разрешает уменьшение уровней вложенности запроса «снизу вверх» _distinct_view_ unnesting Отсутствует Отсутствует false Разрешает преобразование подзапроса типа in в представление select distinct _right_outer_ hash.enable Отсутствует Отсутствует true Разрешает использование правого внешнего соединения хэширования (включая полусоединения и антисоединения) _remove_aggr_ subquery Отсутствует Отсутствует true Позволяет убирать подзапросы с агрегатами Количество параметров, которых не существовало до версии 10g, заставляет задуматься: сколько старых практических методов вам придется забыть из-за того, чего не следует делать в SQL? Давайте рассмотрим параметр _unnest_notexi sts_sq. Почему его убрали в версии 10g? Значит ли это, что оптимизатор совсем недавно был улучшен на- столько, чтобы обрабатывать все возможные подзапросы not exists, и теперь ему не нужен параметр для определенной обработки этих подзапросов? Или это значит, что оптимизатор теперь выполняет уменьшение уровней вложенно- сти запроса в случае подзапросов not exists? (В разделе «Аномалия с антисо- единениями» есть пример, показывающий, что ответом на этот вопрос будет «нет»). А как насчет параметра _cost_equality_semi_join, который появился в версии 9i? Почему на MetaLink есть два сообщения (258 945.1 и 144 967.1), в которых, похоже, утверждается, что в версии 9i уменьшение уровней вложен-
Общие подзапросы 269 ности запроса производится без оценки стоимости, когда этот параметр предпо- лагает, что есть случаи, в которых стоимость учитывается? Возможно, это про- исходит из-за того, что полусоединения (semi-joins) и антисоединения (anti- joins) на самом деле не рассматриваются как примеры уменьшения уровней вложенности запроса. Возможно, это просто из-за того, что даже аналитикам на MetaLink трудно отслеживать все изменения. Что бы этот список ни говорил вам, мне он говорит, что нелегко отследить новые возможности и функциональность, которые продолжают появляться в оптимизаторе, особенно в области трансформации подзапросов. Во всей об- ласти работы с подзапросами я регулярно вынужден напоминать себе, что «я никогда этого не видел» — это не то же самое, что «такое не случается». Это также область, в которой читать трансформированные планы выполнения тя- желее всего (подумайте о моем гипотетическом плане при рассмотрении фильт- рации ранее с отсутствующей строкой filter). Категоризация Перед тем как продолжить, мы должны провести категоризацию подзапросов, чтобы иметь общее представление о типах запросов, которые мы будем рас- сматривать, и чтобы получить общую картину типов подзапросов, в которых возможности трансформации (на текущий момент) ограничены. При решении проблем с подзапросами, которые работают неправильно, я стараюсь разделить их на несколько категорий, как показано в табл. 9.2. Эта категоризация не строго научна и даже не формальна; это лишь мой персональный способ напомнить себе, чего можно ожидать от различных типов подзапросов. Таблица 9.2. Примерная классификация типов подзапросов Категория Характеристики Коррелированные/ Коррелированный подзапрос ссылается на столбцы из внешнего блока некоррелированные запроса. Коррелированные подзапросы часто могут трансформироваться в соединения; некоррелированные подзапросы иногда становятся управляющими (driving) подзапросами Простые/сложные Простые подзапросы содержат только одну таблицу. Сложные подзапросы содержат множество таблиц в соединениях или подзапросах. С простыми подзапросами у оптимизатора есть возможность сделать то, чего со сложными подзапросами он сделать не сможет Агрегирующие Если простой (содержащий одну таблицу) подзапрос включает агрегирование, то существуют некоторые ограничения на его трансформацию оптимизатором Возвращающие одну Подзапрос, который возвращает максимум одну запись — часто это значит, запись что он может стать отправной точкой в запросе С выражениями Подзапросы in могут быть переписаны как подзапросы exists. После этого In/Exists они могут быть трансформированы в полусоединения С выражениями Not Подзапросы not In могут быть переписаны как подзапросы not exists. После in/Not exists этого с некоторыми ограничениями они могут быть трансформированы в антисоединения. Важно то, что операция not in не является противоположностью операции in, — столбцы, в которых разрешено хранение значений null, могут вызвать проблемы
270 Глава 9. Трансформации запросов Обратите внимание, что, конечно, один подзапрос может одновременно от- носиться к нескольким из этих категорий: коррелированный подзапрос может одновременно быть и агрегирующим, и возвращающим не более одной записи при проверке на несуществование (но, по крайней мере, когда я вижу такой подзапрос, я знаю, что я должен тщательно его проанализировать, если он не делает то, что я хочу). В этом томе я рассматриваю простые подзапросы, только немного касаясь операций iп/exists, not iп/not exists, уменьшения уровней вложенности за- проса, полусоединений и антисоединений. Поэтому давайте вернемся к сценарию filter_cost_01.sql, с которого мы начали эту главу, и изменим его таким образом, чтобы позволить оптимизатору в версии 9i делать с самым первым тестовым примером то, что он захочет. Как будет вы- глядеть план выполнения? Смотрите сценарий unnest_cost_01.sqL в онлайн-хра- нилище кода. План выполнения (версия 9.2.0.6, без подсказок - происходит уменьшение уровней вложенности запроса) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=100 Card=1000 Bytes=98000) 1 0 HASH JOIN (Cost=100 Card=1000 Bytes=98000) 2 1 VIEW OF 'VW_SQ_r (Cost=64 Card=6 Bytes=156) 3 2S0RT (GROUP BY) Icost=64 Card=6 Bytes=48) 4 3 TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=20000 Bytes=160000) 5 1 TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=20000 Bytes=1440000) План выполнения (версия 9.2.0.6, с указанием использовать фильтрацию) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=35035 Card=1000 Bytes=72000) 1 0 FILTER 2 1 TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=1000 Bytes=72000) 3 1 SORT (AGGREGATE) 4 3TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=3333 Bytes=26664) Оптимизатор переписал исходный запрос, чтобы избежать выполнения фильтрации. В результате оптимизатор выполнил следующую команду (под- сказка nojnerge добавлена в код только для того, чтобы гарантировать, что Oracle не решит использовать какую-нибудь уловку, связанную со слиянием комплексных представлений, чтобы раскрыть запрос и выполнить его совсем по-другому): select /*+ ordered use_hash(outer) */ outer.* from ( select /*+ no_merge */ dept_no, avg(inner.sal) avg_sal from emp inner group by dept_no
Общие подзапросы 271 ) inner, emp outer where outer.dept_no = inner.dept_no arid outer.sal > inner.avg_sal Обратите внимание на строку view с vw_sq_l в плане выполнения. Это указы- вает на то, что оптимизатор выполнил уменьшение уровней вложенности запроса. Измененными именами представлений, которые изменились из-за уменьшения уровней вложенности запроса, являются vw_nsq_l и vw_nso_l (с изменением числовой части сложного SQL-кода, который выполняет более одной операции уменьшения уровней вложенности запроса). Те из вас, кто читал книгу «Oracle Performance Tuning 101» (Osborne McGraw- Hill, 2001), в этом месте вспомнят, что одним из предложений авторов при ра- боте с определенными типами подзапросов является превращение подзапроса в подставляемое представление и вставка этого представления в тело запроса. Это была настолько хорошая идея, что оптимизатор теперь делает это автома- тически. Это происходит практически всегда в версии 9i, но в версии 10g такое решение принимается на основе оценки стоимости. Тестовый пример выдает тот же итоговый план в версии 10g, что и в версии 9i, включая значение кардинальности, равное 1000, вместо 167, которое выдает- ся в версии 10g при выполнении фильтрации (поэтому значение кардинально- сти, рассчитываемое как 5 % из-за переменной связывания, все еще неверно, но это значение ближе к реальному). Однако, посмотрев трассировку 10053, вы об- наружите, что в версии 9i трассировка включает только две секции general plans, тогда как в версии 10g трассировка включает десять секций перед выда- чей итогового решения — это наводит на мысль, что решение уменьшить коли- чество уровней вложенности запроса принято на основе оценки стоимости. Первая секция в трассировке версии 9i сгенерировала стратегию создания экземпляра подставляемого агрегатного представления, а вторая секция выдала вариант соединения агрегатного представления с другой таблицей. Файл трассировки в версии 10g также начался с этих двух секций, но затем продолжился секцией с расчетом результата соединения двух таблиц перед вы- полнением агрегирования (хотя я думал, что это должно было быть заблокиро- вано подсказкой nojnerge), после которой были выданы несколько вариантов очень похожих расчетов. Среди этих последних расчетов были две секции, вы- полняющие оценку стоимости запроса с помощью старого метода фильтрации из версии 8i — стоимость фильтрации была выше стоимости уменьшения уров- ней вложенности запроса, поэтому эти варианты были исключены из рассмот- рения (хотя во втором эксперименте я исправил данные таким образом, чтобы стоимость выполнения фильтрации оказалась меньше стоимости уменьшения уровней вложенности запроса, после чего фильтрация была выбрана автомати- чески). В сценарии unnest_cost_02.sql в онлайн-хранилище кода есть пример, в кото- ром оптимизатор в версии 10g, похоже, выбирает уменьшение уровней вложен- ности запроса, даже если вариант фильтрации имеет меньшую стоимость. Од- нако выбранный путь доступа на самом деле выводится как полусоединение,
272 Глава 9. Трансформации запросов а не как простое соединение после уменьшения уровней вложенности запроса, поэтому может существовать некоторое эвристическое правило, которое блоки- рует фильтрацию для выполнения полусоединений. Изучение файла трасси- ровки 10053 показывает, что был рассмотрен единственный возможный метод выполнения (соединение), поэтому, скорее всего, перед рассмотрением вариан- та фильтрации была выполнена трансформация. Не забывайте о подсказке no_unnest — она может быть вам необходима время от времени. В сценарии unnest_cost_01a.sql в онлайн-хранилище кода показана пара ва- риаций на тему расчета средней зарплаты. В первом примере используется не- коррелированный подзапрос, возвращающий одну запись, — вместо поиска со- трудников с зарплатой, большей, чем средняя зарплата в их отделе, мы находим сотрудников с зарплатой, которая выше средней зарплаты по компании: select outer. * from emp outer where outer.sal > ( select avg(inner.sal) from emp inner ) Как обычно, в версии 8i выполняется фильтрация и не учитывается стои- мость подзапроса. На самом деле в версии 8i трудно заметить разницу между планом выполнения для исходного запроса по сотрудникам с зарплатой выше среднего по отделу и планом выполнения для измененного запроса. План выполнения (версия 8.1.7.4, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=34 Card=1000 Bytes=72000) 1 0 FILTER 2 1 TABLE ACCESS (FULL) OF 'EMP' (Cost=34 Card=1000 Bytes=72000) 3 1 SORT (AGGREGATE) 4 3 TABLE ACCESS (FULL) OF 'EMP' (Cost=34 Card=20000 Bytes=100000) Как и в случае с выполнением фильтрации для коррелированного подзапро- са, который мы рассматривали в начале этой главы, наш план выполнения по- казывает сканирование таблицы етр и расчет среднего значения при сканиро- вании каждой записи. Единственная разница между этим планом и планом для коррелированного подзапроса состоит в том, что кардинальность табличного сканирования при выполнении агрегирования в строке 4 равна 20 000 (для всей таблицы), а не 3334 для каждого отдела, как это было рассчитано в первом пла- не выполнения. Выполнив запрос в версии 9i, мы увидим следующие изменения: План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL ROWS (Cost=35 Card=1000 Bytes=72000) 1 0 TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=1000 Bytes=72000)
Общие подзапросы 273 2 1 SORT (AGGREGATE) 3 2 TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=20000 Bytes=100000) Посмотрите на план выполнения очень внимательно — отступы очень важ- ны- Двигаемся снизу вверх: строка 3 является табличным сканированием, кото- рое передает свой результирующий набор данных строке 2, выполняющей его сортировку для функции агрегирования — на самом деле при этом выполняется не реальная сортировка, а только определение суммы и подсчет количества для вычисления средней зарплаты по компании. Строка 2 передает среднее значе- ние один раз строке 1 как неизвестное, но фиксированное значение. Поэтому В строке 1 используется значение 5 % для переменной связывания, чтобы опреде- лить, что табличное сканирование вернет 1000 записей из 20 000 записей. Обратите внимание на итоговую стоимость в строке 0. Ее значение равно 35, это стоимость выполнения только одного табличного сканирования, то есть значение стоимости неверно. В версии 10g план выполнения идентичен, кроме того, что итоговая (правильная) стоимость равна 70. Другой вариацией на тему расчета среднего значения (в том же сценарии unnest_cost_01a.sql) является добавление дополнительного ограничения в виде дополнительной таблицы. Что, если меня интересуют только результаты опре- деленной группы отделов? Я мог бы создать следующий (не самый лучший) за- прос: select outer.* from emp outer where outer.dept_no in ( select dept_no from dept where dept_group = 1 ) and outer.sal > ( select avg(inner.sal) from emp inner where inner.dept_no = outer.dept_no and inner.dept_no in ( select dept_no from dept where dept_group = 1 ) ) В этом случае SQL выберет только сотрудников из отделов в группе 1 из полного списка сотрудников, используя некоррелированный подзапрос и во Внешнем блоке запроса, и во внутреннем. Лучшим подходом в этом случае было бы использование соединения двух таблиц (предполагая, что это было бы логически эквивалентным) и во внешнем, и во внутреннем блоках запроса. Несмотря на неэффективность запроса, оптимизатор нормально его обрабаты- вает в версии 10g. Я использовал dbms_xplan в этом примере, чтобы определить,
274 Глава 9. Трансформации запросов какие копии emp и dept в исходном запросе соответствуют каким копиям в пла- не выполнения. Но это удалось определить только после тщательного изучения секции с информацией о предикатах (столбец, называющийся instanceBplan_ table, дает вам возможность очень легко определить множество копий одной и той же таблицы в запросе — к сожалению, ни один из инструментов Oracle не выводит эту информацию). 1 Id | Operation Name Rows | Bytes | Cost | 1 е | SELECT STATEMENT see | 51500 | 98 | 1 * 1 | HASH JOIN see | 51500 | 98 | 1 2 | VIEW VW_SQ_1 6 1 156 | 59 | 1 3 | SORT GROUP BY 6 1 78 | 59 | 1 * 4 | HASH JOIN leeee | 126K | 38 | 1 * 5 ITABLE ACCESS FULL DEPT 3 1 15 | 2 | 1 6 ITABLE ACCESS FULL EMP 20000 | 156K | 35 | 1 * 7 | HASH JOIN leeee | 751K | 38 | 1 * 8 | TABLE ACCESS FULL DEPT 3 1 15 I 2 | 1 9 | TABLE ACCESS FULL EMP 20000 | 1406K | 35 | Predicate Information (identified by operation id): 1 - access("DEPT_NO"="OUTER"."DEPT_NO") fi Iter ("OUTER". "SAL">'|'VW_COL_1") 4 - access("INNER"."DEPT_NO"="DEPT_NO") 5 - filter("DEPT_GROUP"=l) 7 - access("OUTER"."DEPT_NO"="DEPT NO") 8 - filter(”DEPT_GROUP"=l) Посмотрев на эти данные, можно увидеть, что оптимизатор в строках 7,8 и 9 превратил внешний подзапрос в простое соединение хэширования. Посмотрев на строку 2, можно увидеть, что оптимизатор также уменьшил количество уровней вложенности запроса, исключив при этом подзапрос, возвращающий среднюю зарплату — и в этом подзапросе оптимизатор (в строках 4, 5 и 6) так- же превратил неэффективную конструкцию внутреннего подзапроса в простое соединение хэширования. Даже в версии 8i делается что-то подобное. И в ней оба подзапроса превра- щаются в соединения хэширования, но после этого для оставшейся части за- проса используется стандартный механизм фильтрации. К сожалению, в версии 9i делается нечто неожиданное — возможно, это по- пытка механизма быть слишком интеллектуальным при неправильном наборе данных. Ниже показан план выполнения в версии 9i: 1 Id | Operation | Name | Rows Bytes TempSpc | Cost | 1 0 | SELECT STATEMENT 1 1 833K 71M 1 262K | 1 * 1 | FILTER 1 1 1 1 1 2 | SORT GROUP BY 1 1 833K 71M 1687M | 262K | 1 * 3 | HASH JOIN 1 1 16M 1430M 1 100 | 1 4 | TABLE ACCESS FULL | EMP | 20000 156K 1 35 | 1 * 5 | HASH JOIN 1 1 30000 2402K 1 44 | 1 6 IMERGE JOIN CARTESIAN 1 1 9 90 1 8 1
Общие подзапросы 275 * 7 | TABLE ACCESS FULL | DEPT 1 3 | 15 I 1 2 8 | BUFFER SORT 1 1 3 | 15 I 1 6 * 9 | TABLE ACCESS FULL | DEPT 1 3 | 15 I 1 2 10 |TABLE ACCESS FULL | EMP | 20000 | 1406K | 1 35 Predicate Information (identified by operation id): 1 - fiIter("OUTER"."SAL">AVG("INNER"."SAL")) 3 - access("INNER"."DEPT_NO"="OUTER"."DEPT_NO" AND "INNER"."DEPT_NO"="DEPT"."DEPT_NO") 5 - access("OUTER"."DEPT NO"="DEPT"."DEPT_NO") 7 - filter("DEPT"."DEPT GROUP"=1) 9 - filter("DEPT"."DEPT_GROUP"=1) Я не буду пытаться точно объяснить, что оптимизатор здесь делает, — важ- но, что он придерживается директивы уменьшения уровней вложенности за- проса (значение параметра _unnest_s u bque г у равно true) и уменьшает уровни вложенности на сколько может, после чего использует слияние комплексных представлений, чтобы найти оптимальный порядок соединения четырех таб- лиц, отложив расчет средней зарплаты на самый последний момент. Достаточно сказать, что в версиях 8i и 10g тест выполнялся меньше секунды, в то время как в версии 9i это заняло 1 мин 22 с процессорного времени на ком- пьютере с частотой 2,8 ГГц. Когда я установил значение параметра _unnest_subquery равным false, план выполнения в версии 9i стал таким же, как в версии 8i; когда я установил значение параметра _complex_view_merging равным false, план выполнения стал таким же, как план выполнения в версии 10g. И наоборот, когда в версии 10g я установил значение параметра _optimizer_squ_bottomup равным false, в версии 10g был сгенерирован катастрофический план выполнения версии 9i — даже когда я переписал запрос, чтобы превратить подзапросы по отделам в соединения (и это оказалось неожиданностью, которую я буду должен ко- гда-нибудь исследовать). Пол усоединен ия Давайте оставим сложность предыдущего примера и вместо этого рассмотрим всего лишь вывод всех сотрудников из определенной группы отделов. Я бы на- писал следующее (см. сценарий semi_01.sql в онлайн-хранилище кода): Select emp.* from emp where emp.dept_no in ( select dept.dept_.no from dept where dept.dept_group = 1 ) Вы видите (основываясь на интуитивном понимании того, какие обычно су- ществуют взаимосвязи между сотрудниками и отделами), что это, возможно, Плохой вариант написания запроса и что простое соединение будет лучше рабо- тать. Считая, что я создал таблицы правильно (то есть коды отделов уникальны
276 Глава 9. Трансформации запросов в таблице отделов), оптимизатор может прийти к тому же выводу. Ниже пока- зан план выполнения по умолчанию в версии 9i, когда есть это важное условие: План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=38 Card=10000 Bytes=770000) 1 0 HASH JOIN (Cost=38 Card=10000 Bytes=770000) 2 1 TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=3 Bytes=15) 3 1 TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=20000 Bytes=1440000) Оптимизатор превратил вариант с использованием подзапроса в простой ва- риант с соединением. Если бы я не создал таблицу dept с соответствующим ог- раничением уникальности, оптимизатор так бы и решал использовать соедине- ние с помощью уменьшения уровней вложенности запроса: План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=44 Card=10000 Bytes=770000) 1 0 HASH JOIN (Cost=44 Card=10000 Bytes=770000) 2 1 SORT (UNIQUE) 3 2TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=3 Bytes=15) 4 1 TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=20000 Bytes=1440000) В версии 8i существует очень небольшое отличие — в план добавляется еще одна строка между соединением хэширования и сортировкой, указывающая на подставляемое представление vw_nso_l при уменьшении уровней вложенности запроса. Если мы затем запретим уменьшение уровней вложенности запроса (с помощью подсказки no_unnest), оптимизатор вернется к механизму фильт- рации: План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=47 Card=3333 Bytes=239976) 1 0 FILTER 2 1 TABLE ACCESS (FULL) OF 'EMP' (TABLE) (Cost=35 Card=20000 Bytes=1440000) 3 1 TABLE ACCESS (FULL) OF 'DEPT' (TABLE) (Cost=2 Card=l Bytes=5) Остается еще один вариант, который может использовать оптимизатор: по- лусоединение (semi-join) — механизм, использующийся только для проверок на существование. «Минуточку! — скажете вы. — В этом запросе нет проверки на существование, а есть подзапрос 1 п». Но операция 1 п всегда может быть пре- вращена в операцию exists, и после такого превращения оптимизатор может решить использовать полусоединение — и это полусоединение может быть по- лусоединением с использованием вложенных циклов, полусоединением слия- ния или полусоединением хэширования. Ниже показаны планы в версиях 8i и 9i с подсказками: План выполнения (версия 9.2.0.6, автотрассировка, с подсказкой nl_sj в подзапросе) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=40035 Card=10000 Bytes=770000)
Общие подзапросы 277 1 0 NESTED LOOPS (SEMI) (Cost=40035 Card=10000 Bytes=770000) 2 1 TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=20000 Bytes=1440000) 3 1 TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=2 Bytes=10) План выполнения (версия 9.2.0.6, автотрассировка, с подсказкой merge_sj в подзапросе) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=282 Card=10000 Bytes=770000) 1 0 MERGE JOIN (SEMI) (Cost=282 Card=10000 Bytes=770000) 2 1 SORT (JOIN) (Cost=274 Card=20000 Bytes=1440000) 3 2 TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=20000 Bytes=1440000) 4 1 SORT (UNIQUE) (Cost=8 Card=3 Bytes=15) 5 4 TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=3 Bytes=15) План выполнения (версия 9.2.0.6, автотрассировка, с подсказкой hash_sj в подзапросе) 0 SELECT STATEMENT Optimizer=ALL ROWS (Cost=63 Card=10000 Bytes=770000) 1 0 HASH JOIN (SEMI) (Cost=63 Card=10000 Bytes=770000) 2 1 TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=20000 Bytes=1440000) 3 1 TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=3 Bytes=15) Так что же такое полусоединение? Мы можем переписать наш пример с под- запросом для поиска сотрудников в группе отделов, как показано ниже: select emp.* from emp, dept where dept.dept_no = emp.dept_no and dept.dept_group = 1 Но при таком переписывании может появиться одна логическая проблема. Если комбинация (dept_no, dept_group) или, по крайней мере, dept_.no не яв- ляется уникальной в таблице dept, то простое соединение может вернуть мно- жество копий каждой соответствующей записи из таблицы етр. Если вы не бу- дете очень осторожны при переходе между соединениями и подзапросами, вы Можете получить неверные результаты. Вот когда используется полусоединение: оно обрабатывает случаи, когда нужное ограничение уникальности просто отсутствует. Полусоединение рабо- тает как простое соединение, но когда одна запись из внешней таблицы соеди- няется с одной записью из внутренней таблицы, больше никакой обработки для этой внешней записи не производится. В форме соединения с использованием вложенных циклов это похоже на внутренний цикл, который всегда прекраща- ется после первого нахождения записи, сохраняя, таким образом, ресурсы. Конечно, как вы только что видели, с учетом ограничений полусоединения у вас может использоваться полусоединение с использованием вложенных цик- лов, полусоединение слияния или полусоединение хэширования, но если вы внимательно посмотрите на полусоединение хэширования в версии 9i, вы увиди- те, что это соединение было выполнено неправильно. Оно построило хэш-таблицу
278 Глава 9. Трансформации запросов в памяти из большего набора данных, а затем начало ее зондировать с помощью небольшого набора данных. Полусоединения и антисоединения (как и традици- онные внешние соединения) в версии 9i работают в следующем строгом поряд- ке: таблица из подзапроса должна появляться после таблицы в главном теле за- проса. Однако в версии 10g, в которой улучшения в коде соединения хэширования дают возможность выполнять внешнее соединение хэширования в любом поряд- ке, мы видим небольшое интересное изменение. План выполнения (на самом деле это план выполнения по умолчанию в моем тестовом примере) выглядит следующим образом: План выполнения (версия 10.1.0.4, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=38 Card=10000 Bytes=770000) 1 0 HASH JOIN (RIGHT SEMI) (Cost=38 Card=10000 Bytes=770000) 2 1 TABLE ACCESS (FULL) OF 'DEPT' (TABLE) (Cost=2 Card=3 Bytes=15) 3 1 TABLE ACCESS (FULL) OF 'EMP' (TABLE) (Cost=35 Card=20000 Bytes=1440000) Обратите внимание на (right semi) в первой строке по сравнению с (semi) в плане выполнения в версии 9i. Полусоединения и антисоединения, как и внеш- ние соединения, в версии 10g не обязаны выполняться в строгом порядке, если они выполняются как соединения хэширования, поэтому меньший набор дан- ных всегда будет выбираться для создания хэш-таблицы. Также обратите вни- мание, что стоимость стала равной исходной стоимости соединения хэширова- ния, когда столбец dept_no был объявлен как уникальный. Антисоединения Полусоединение может работать с подзапросами exists (или с подзапросами in, которые превращаются в подзапросы exists). Есть еще одна специальная операция — антисоединение (anti-join), которая используется в случае подза- просов not exists (или not in). Зная о том, что у нас только две группы отделов, мы можем выбрать сотруд- ников из группы отделов 1, переписав наш специфический запрос таким обра- зом, чтобы он выбирал сотрудников, которые не принадлежат группе отделов 2 (см. сценарий anti_01.sql в онлайн-хранилище кода). select emp.* from emp where emp.dept_no not in ( select dept.dept_no from dept where dept.dept_group = 2 ) Первое, что мы обнаружим, — это то, что вне зависимости от указания под- сказок оптимизатор все равно выдаст следующий план, если или в emp .dept_ по, или в dept .dept_no могут храниться значения null. Что-то блокирует лю-
Общие подзапросы 279 бые трансформации, если в столбцах могут храниться значения null (мы рас- смотрим это чуть позже). План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=2035 Card=1000 Bytes=72000) 1 0 FILTER 2 1 TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=1000 Bytes=72000) 3 1 TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=l Bytes=5) Если мы хотим, чтобы оптимизатор эффективно работал с подзапросами not i п, нам нужно убедиться, что столбцы на обеих сторонах сравнения не могут хранить значения null. Конечно, одним из способов сделать это является до- бавление ограничения notnullK двум столбцам, но также подойдет и предикат dept_no is not null на обеих сторонах запроса. Предполагая, что у нас действительно есть соответствующие ограничения not null на обоих сторонах сравнения, мы получаем следующие планы выпол- нения в версии 9i (в моем случае по умолчанию сработало антисоединение хэ- ширования, для других двух вариантов пришлось использовать подсказки): План выполнения (версия 9.2.0.6 - с подсказкой nl_aj в подзапросе) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=40035 Card=10000 Bytes=770000) 1 0 NESTED LOOPS (ANTI) (Cost=40035 Card=10000 Bytes=770000) 2 1 TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=20000 Bytes=1440000) 3 1 TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=2 Bytes=10) План выполнения (версия 9.2.0.6 - с подсказкой merge_aj в подзапросе) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=282 Card=10000 Bytes=770000) 1 0 MERGE JOIN (ANTI) (Cost=282 Card=10000 Bytes=770000) 2 1 SORT (JOIN) (Cost=274 Card=20000 Bytes=1440000) 3 2TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=20000 Bytes=1440000) 4 1 SORT (UNIQUE) (Cost=8 Card=3 Bytes=15) 5 4TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=3 Bytes=15) План выполнения (версия 9.2.0.6 - с подсказкой hash_aj в подзапросе) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=63 Card=10000 Bytes=770000) 1 0 HASH JOIN (ANTI) (Cost=63 Card=10000 Bytes=770000) 2 1 TABLE ACCESS (FULL) OF 'EMP' (Cost=35 Card=20000 Bytes=1440000) 3 1 TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=3 Bytes=15) Обратите внимание еще раз, что в версии 9i соединение хэширования было сделано с высокой стоимостью (с созданием хэш-таблицы из большей табли- цы). Снова положение исправляется в версии 10g, в плане выполнения которой порядок таблиц был изменен и (anti) было заменено на (right anti): План выполнения (версия 10.1.0.4 - с подсказкой hash_aj в подзапросе) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=38 Card=10000 Bytes=770000)
280 Глава 9. Трансформации запросов 1 0 HASH JOIN (RIGHT ANTI) (Cost=38 Card=10000 Bytes=770000) 2 1 TABLE ACCESS (FULL) OF 'DEPT' (TABLE) (Cost=2 Card=3 Bytes=15) 3 1 TABLE ACCESS (FULL) OF 'EMP' (TABLE) (Cost=35 Card=20000 Bytes=1440000) Мы можем объяснить работу антисоединения, вспомнив, как можно перепи- сать подзапрос not in в виде соединения. Следующий запрос является резуль- татом такого переписывания и дает нам важную информацию о том, как работа- ет антисоединение: select emp. * from emp, dept where dept.dept_no(+) = emp.dept_no and dept.dept_group(+) = 2 and dept.dept_no is null Результатом этого запроса является соединение каждого сотрудника с его отделом, если его отдел находится в группе 2, но из-за внешнего соединения мы также оставляем и все другие записи сотрудников; а затем мы исключаем все записи, для которых соединение действительно нашло отдел. Следовательно, все, что осталось, — это по одной копии каждой записи сотрудника, который не принадлежит отделу в группе 2 и, таким образом (так как мы знаем содержимое данных), принадлежит отделу в группе 1. И опять мы проделали очень много работы, чтобы получить результат, который может (в общем случае) быть не- верным — особенно если на таблице dept нет соответствующего ограничения уникальности. Антисоединение просто является реализацией внешнего соеди- нения, кроме исключения записи из внешней таблицы при нахождении соот- ветствия (избегая таким образом выполнения лишних соединений с внутрен- ней таблицей). Аномалия с антисоединениями Практически так же, как оптимизатор может превращать подзапросы i п в под- запросы exists, он может превращать подзапросы not in в подзапросы not exists. Но иногда этого не происходит без какой-либо явной причины. Срав- ните следующие два запроса — учитывая определение таблиц (см. book_subq.sql в онлайн-хранилище кода), они логически эквивалентны: select book_key from books where NOT EXISTS ( select null from sales where sales.book_key = books.book_key ) select book_key from books
Общие подзапросы 281 where book_key NOT IN ( select book_key from sales ) В версии 10g оптимизатор использует антисоединение хэширования для вы- полнения первого запроса. Более того, в версии 10g второй запрос превращает- ся в первый, и на нем также выполняется антисоединение хэширования. С другой стороны, в версии 9i поведение другое и совершенно непонятное. Второй запрос превращается в первый, и выполняется антисоединение хэширо- вания. Но в первом запросе антисоединение хэширования не выполняется, если не указана подсказка (а стоимость выполнения антисоединения хэширования гораздо меньше, чем операция фильтрации, выполняемая по умолчанию). Вы можете вспомнить, что в таблице параметров оптимизатора, показанной ранее, был параметр версии 9i _unnest_notexists__sq со значением по умолчанию single. Странно, что, похоже, этот запрос может быть точно описан этим пара- метром — для одного подзапроса с проверкой существования не выполняется уменьшение уровней вложенности запроса. ОШИБКИ В АНТИСОЕДИНЕНИЯХ В ВЕРСИИ 91 На MetaLink показаны несколько ошибок, относящихся к антисоединениям в версии 91; практиче- ски все они указаны как исправленные в версии 10.2. По большей части эти ошибки из категории возврата неверных результатов, так что будьте внимательны при рассмотрении планов выпол- нения, в которых указываются антисоединения. На всякий случай лучше иметь список текущих ошибок. В результате можно сказать, что оптимизатор может выполнять все варианты сложной трансформации между различными типами запросов, поэтому можно писать запросы так, как вам удобно; но иногда ожидаемая вами трансформация может не произойти, поэтому вы должны быть готовы иногда писать ваши за- просы таким образом, чтобы помочь оптимизатору. Значения Null и Not In Мы должны рассмотреть проблему со значениями null, которые блокируют Выполнение трансформаций антисоединения (и сопутствующих трансформа- ций). Есть два варианта рассмотрения этой проблемы. Первый состоит в не- формальном замечании, что антисоединение примерно является внешним со- единением, показанным в предыдущем разделе — но внешнее соединение вы- полняет сравнение на равенство между столбцами dept_no таблиц emp и dept, И значения null всегда порождают проблемы, как только вы начинаете сравни- вать их не с помощью is null или is not null. Вторым, несколько более формальным вариантом является комментарий в руководстве «SQL Reference» о том, что COlX not in ('А', 'В', 'С') эквивалентно COIX != 'A' and coIX != 'В' and coIX != 'С
282 Глава 9. Трансформации запросов Строка с операциями AND означает, что каждое из отдельных условий в спи- ске должно быть проверено и должно равняться true, чтобы все выражение было равным true. Если хотя бы одно условие будет равно false или null, все выражение не будет равно true. Для иллюстрации ошибок, которые могут произойти, если вы забудете об этом, сценарий notin.sql в онлайн-хранилище кода выполняет тестовый пример со следующими результами: select** from tl where nl = 99; N1V1 99Ninety-nine 1 row selected. select * from t2 where nl = 99; no rows selected select * from tl where tl.nl not in ( select t2.nl from t2 ) » no rows selected Первый запрос показывает, что в таблице tl есть запись с nl = 99. Второй запрос показывает, что в таблице t2 нет соответствующих записей с nl = 99. Последний запрос показывает, что в таблице tl нет записей, не имеющих соот- ветствующих записей в таблице t2 — несмотря на то, что было только что пока- зано. Проблема в том, что в таблице t2 есть запись, в которой выполняется nl i s null — как только это происходит, последний запрос никогда не вернет данные, потому что каждая сравниваемая запись из таблицы tl сравнивается с этой за- писью, то есть происходит сравнение по условию tl.nl = null (это не то же са- мое, что tl.nl is null), в результате чего никогда не возвращается true. Решением, конечно, является добавление предиката not null на обеих сто- ронах сравнения: select * from tl where tl.nl is not null and tl.nl not in ( select t2.nl from t2 where t2.nl is not null ) Nl Vl 99 Ninety-nine 1 row selected.
Общие подзапросы 283 Из-за проблемы сравнений при наличии значений null оптимизатор может выполнить трансформацию стандартного подзапроса not in только в том слу- чае, если существуют предикаты или ограничения not null. В противном слу- чае единственно возможным планом выполнения является операция filter; более того, сложная оптимизация, которую мы видели при выполнении фильт- рации, не реализована (на текущий момент) для случая, когда операция filter должна обрабатывать выражения not in. Это еще одна причина для определения ограничений базы данных. Если вы знаете, что столбец никогда не должен содержать значения null, сообщите об Этом базе данных, иначе плохие данные (в форме значений null) могут ко- гда-нибудь попасть в этот столбец и привести к тому, что существующие подза- просы not i п внезапно начнут выдавать неправильные результаты без какой бы то ни было явной причины. В завершение обратите внимание, что выражение (очевидно противополож- ное) COlX in ('А', 'В', 'С') Эквивалентно COlX « 'A' or COlX = 'В' or COlX = 'С Строка с операцией OR означает, что только одно из отдельных сравнений В списке должно быть равно true (после чего Oracle не нужно выполнять дру- гие сравнения), чтобы все выражение также было равно true. Кроме преиму- щества в сокращении количества проверок, это также значит, что появление значения null при сравнении не является Проблемой. Это показывает нам, что (й это совсем не очевидно) два оператора i п и not i п не являются точными цротивоположностями друг друга. Подсказка ordered По мере усложнения обработки подзапросов оптимизатором вы можете обнару- жить, что код, который был эффективным, внезапно стал работать неправиль- но, потому что вы сами ему это указали! Подсказки — даже самые простые — могут быть очень опасны, и ниже показан пример, как ситуация может стать очень плохой (см. сценарий ordered.sql в онлайн-хранилище кода). В примере показан запрос, которому нужна подсказка в версии 81, чтобы выполнить соеди- нение таблиц tl и t3 в правильном порядке: select /*+ ordered push_subq */ tl.vl from tl, t3 Where t3.nl = tl.nl and exists ( select t2.id from t2 where t2.nl = 15 and t2.id = tl.id )
284 Глава 9. Трансформации запросов and exists ( select t4.id from t4 where t4.nl = 15 and t4. id = t3. id Это идеально работает в версии 8i, оптимизатор выдает следующий план: План выполнения (версия 8.1.7.4) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost»103 Card=1812 Bytes=45300) 1 0 FILTER 2 . 1 HASH JOIN (Cost=103 Card=1812 Bytes=45300) 3 2 TABLE ACCESS (FULL) OF 'Tl' (Cost=51 Card=1000 Bytes=16000) 4 3 TABLE ACCESS (BY INDEX ROWID) OF 'T2' (Cost=2 Card=l Bytes=8) 5 4 INDEX (UNIQUE SCAN) OF 'T2_PK' (UNIQUE) (Cost=l Card=l) 6 2 TABLE ACCESS (FULL) OF 'T3' (Cost=51 Card=1000 Bytes=9000) 7 1 TABLE ACCESS (BY INDEX ROWID) OF 'T4' (Cost=2 Card=l Bytes=8) 8 7 INDEX (UNIQUE SCAN) OF 'T4_PK' (UNIQUE) (Cost=l Card=l) К сожалению, когда вы обновляете версию до 9i, оптимизатор продолжает следовать указанным подсказкам — а вы в SQL-коде не указали достаточное ко- личество подсказок для использования новых улучшенных возможностей вер- сии 9i. Помните, что трансформация запроса выполняется перед всеми другими стадиями оптимизации. Оптимизатор убирает два подзапроса в исходной команде, уменьшая уровни вложенности запроса. К сожалению, похоже, что уменьшение уровней вложен- ности запроса производится снизу вверх, в результате чего подставляемые представления вставляются сверху вниз в выражение from. Это значит, что по- рядок таблиц в трансформированном запросе теперь t4, 12, tl, t3 - после чего применяется эта подсказка ordered! (подсказка push_subq игнорируется, пото- му что после того, как в версии 9i завершается трансформация, не остается под- запросов). Поэтому новый план выполнения — благодаря подсказке — выгля- дит следующим образом: План выполнения (версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=36 Card=l 8ytes=41) 1 0 NESTED LOOPS (Cost=36 Card=l Bytes=41) 2 1 NESTED LOOPS (Cost=29 Card=7 Bytes=224) 3 2MERGE JOIN (CARTESIAN) (Cost=22 Card=7 Bytes=112) 4 3 SORT (UNIQUE) 5 4 TABLE ACCESS (BY INDEX ROWID) OF 'T4' (Cost=4 Card=3 Bytes=24) 6 5 INDEX (RANGE SCAN) OF 'T4_N1' (NON-UNIQUE) (Cost=l Card=3) 7 3 BUFFER (SORT) (Cost=18 Card=3 Bytes=24) 8 7 SORT (UNIQUE) 9 8 TABLE ACCESS (BY INDEX ROWID) OF 'T2' (Cost=4 Card=3 Bytes=24) 10 9 INDEX (RANGE SCAN) OF ’T2_N1' (NON-UNIQUE) (Cost=l Card=3) 11 2TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=l Card=l Bytes=16) 12 11 INDEX (UNIQUE SCAN) OF 'T1_PK' (UNIQUE) 13 1 TABLE ACCESS (BY INDEX ROWID) OF 'T3' (Cost=l Card=l Bytes=9) 14 13INDEX (UNIQUE SCAN) OF 'T3_PK' (UNIQUE)
Соединения с трансформацией типа «звезда» 285 План выполнения не очень хорош: обратите внимание на соединение слия- ния с декартовым произведением между двумя несвязанными таблицами t2 и t4, которые раньше были в подзапросах. Не обвиняйте в этом Oracle — вы указали подсказки, а подсказки соблюдаются, если они актуальны. Вы просто не указали подсказки, необходимые для сохранения корректности вашего кода В будущем. Соединения с трансформацией (ипа «звезда» Имеет смысл кратко рассмотреть трансформацию типа «звезда», так как она яв- ляется фантастическим примером того, как оптимизатор может переписать за- прос таким образом, чтобы выдать тот же результирующий набор данных из того же набора таблиц, используя при этом совершенно другой порядок действий. Сценарий star_trans.sql в онлайн-хранилище кода создает следующую таблицу: Create table factl ( id, mod_23, mod_31, mod_53, small_vc, padding ) partition by range (id) ( partition p_0500000 values less than( 500001), partition p_1000000 values less than(1000001), partition p_1500000 values less than(1500001), partition p_2000000 values less than(2000001) ) as Mi th generator as ( select --+ materialize rownum id from all_objects where ) Select /*+ ordered use_nl(v2) */ rownum 20 * mod(rownum - 1, 23) 20 * mod(rownum - 1, 31) 20 * mod(rownum - 1, 53) lpad(rownum - 1,20,'0') rpad('x',200) from generator generator Where rownum <= 2000000 rownum <= 3000 id, mod_23, mod_31, mod_53, small_vc, padding vl, v2
286 Глава 9. Трансформации запросов Таблица factl секционирована по диапазонам по столбцу ID и включает три столбца с часто повторяющимися значениями. Я собираюсь создать три би- товых индекса на этой таблице, по одному на каждый из этих столбцов с часто повторяющимися значениями, а затем создать соответствующие таблицы изме- рений для каждого из этих столбцов. На каждой таблице измерения объявлен первичный ключ. Что делает ситуацию еще более интересной: каждая таблица измерения со- держит в 20 раз больше уникальных значений, чем требуется для этой таблицы factl, и будет добавлен дополнительный столбец (столбец повторений) с име- нем гер_пп в каждой таблице измерений, который я буду использовать в до- полнительном предикате при соединении измерений с таблицей factl. Ниже показаны типичная таблица измерения и пример запроса, который вы можете выполнить: create table dim_23 as select rownum - 1 id_23, mod(rownum - 1,23) rep_23, Ipad(rownum - 1,20) vc_23, rpad('x',2000) padding_23 from all_objects where rownum <= 23 * 20 alter table dim_23 add constraint dim_23_pk primary key(id_23); Сбор статистики с помощью dbms_stats select dim23.vc_23, dim31.vc_31, dim53.vc_53, factl.small_vc from dim_23, dim_31, dim_53, factl where factl.mod_23 = dim_23.id_23 and dim_23.rep_23 = 10 /* */ and factl.mod_31 = dim_31.id_31 and dim_31.rep_31 = 10 /* */ and factl.mod_53 = dim_53.id_53 and dim_53.rep_53 = 10 Существуют две абсолютно разные стратегии, которые может использовать оптимизатор для этого запроса. Первая стратегия пытается найти такой поря- док четырех таблиц, который позволяет выполнять соединение каждой табли-
Соединения с трансформацией типа «звезда» 287 цы с последующей наиболее эффективным способом, возможно, приходя к пла- ну выполнения, который сканирует таблицу factl и использует соединения с вложенными запросами или соединения хэширования для каждой из таблиц измерений по очереди, чтобы исключить ненужные данные. Например, оптими- затор может просто пройти по всем данным, выполнив соединение хэширова- ния в три шага, как показано ниже: План выполнения (версия 10.1.0.4, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=10608 Card=423404 Bytes=49538268) 1 0 HASH JOIN (Cost=10608 Card=423404 Bytes=49538268) 2 1 TABLE ACCESS (FULL) OF 'DIM_23' (TABLE) (Cost=25 Card=20 Bytes=560) 3 2 HASH JOIN (Cost=10576 Card=486914 Bytes=43335346) 4 3 TABLE ACCESS (FULL) OF 'DIM_31' (TABLE) (Cost=33 Card=20 Bytes=560) 5 4 HASH JOIN (Cost«=10536 Card»754717 Bytes=46037737) 6 5 TABLE ACCESS (FULL) OF ’DIM_53' (TABLE) (Cost=55 Card=20 Bytes=560) 7 6 PARTITION RANGE (ALL) (Cost=10469 Card=2000000 Bytes=66000000) 8 7 TABLE ACCESS (FULL) OF 1FACT1' (TABLE) (Cost=10469 Card=2M Bytes=66M) Этот план выполнения помещает таблицу трех измерений в память (воз- можно, используя область памяти объемом по меньшей мере в половину значе- ния hash_area_si ze для каждой хэш-таблицы в памяти, если вы не используе- те автоматическую настройку workarea_size_policy), а затем читает каждую хэш-секцию из таблицы фактов по очереди, выполняя зондирование трех хэши- рованных таблиц измерений перед выводом (или исключением из рассмотре- ния) записи из таблицы factl. Следовательно, стоимость выполнения этого запроса близка к стоимости полного табличного сканирования по всем хэш-секциям (стоимостью сканиро- вания и хэширования таблиц измерений и стоимостью зондирований хэш-таб- лицы в памяти можно пренебречь — их значения сравнительно малы). Другой стратегией является определение, что три существующих битовых индекса могут дать нам возможность получить очень эффективный доступ К очень маленькому количеству записей (только 53 в этом примере), которые мы хотим извлечь из таблицы factl, после чего мы можем выполнить соедине- ние трех таблиц измерений очень эффективно, чтобы получить все нужные нам данные по измерениям. В результате оптимизатор действительно переписывает запрос, как показано Ниже, с той необычной особенностью, что он использует каждую таблицу изме- рения дважды: select dim23.vc_23, dim31.vc_31, d1m53.vc_53, vl.small_vc from dim_23,
288 Глава 9. Трансформации запросов dim_31, dim S3, ( select mod_23, mod_31, mod_S3, small_vc from factl where factl.mod_23 in £ select id_23 from dim_23 where dim_23.rep_23 = 10 ) and factl.mod_23 in ( select id_31 from dim_31 where dim_31.rep_31 = 10 ) and factl.mod_S3 in ( select id_S3 from dim_S3 where dim_S3.rep_S3 = 10 ) ) vl where dim_23.id_23 = vl.mod_23 and dim_31.id_31 = vl.mod_31 and dim_53.id_53 = vl.mod_S3 Поэтому мы должны увидеть часть плана выполнения с операцией bi tmap and над тремя битовыми индексами главной таблицы фактов и три последую- щих соединения (возможно, соединения с использованием вложенных циклов, соединения слияния или соединения хэширования) для добавления дополни- тельных столбцов из таблиц измерений. С уверенностью можно сказать, что од- ним из возможных планов для этого запроса (когда у параметра star_ transformation_enabled установлено значение temp_disable) является сле- дующий: План выполнения (версия 10.1.0.4, автотрассировка) SELECT STATEMENT Optimizer=ALL ROWS (Cost=2S6 Card=ll) HASH JOIN (Cost=256 Card=ll) HASH JOIN (Cost=230 Card=13) HASH JOIN (Cost=174 Card=34) TABLE ACCESS (FULL) OF 'DIM_31' (TABLE) (Cost=33 Card=20) PARTITION RANGE (ALL) (Cost=139 Card=S3) +++ TABLE ACCESS (BY LOCAL INDEX ROWID) OF 'FACT1' (TABLE) (Cost=139 Card=53) BITMAP CONVERSION (TO ROWIDS) BITMAP AND * * BITMAP MERGE * * BITMAP KEY ITERATION * * BUFFER (SORT) * * TABLE ACCESS (FULL) OF 'DIM_S3' (TABLE) (Cost=SS Card=20)
Соединения с трансформацией типа «звезда» 289 * * BITMAP INDEX (RANGE SCAN) OF 'FACT1_S3' (INDEX (BITMAP)) * * BITMAP MERGE * * BITMAP KEY ITERATION * * BUFFER (SORT) * * TABLE ACCESS (FULL) OF 'DIM_23' (TABLE) (Cost=2S Card=20) * * BITMAP INDEX (RANGE SCAN) OF 'FACT1_23' (INDEX (BITMAP)) * * BITMAP MERGE * * BITMAP KEY ITERATION * * BUFFER (SORT) * * TABLE ACCESS (FULL) OF 'DIM_31' (TABLE) (Cost=33 Card=20) * * BITMAP INDEX (RANGE SCAN) OF 'FACT1_31' (INDEX (BITMAP)) TABLE ACCESS (FULL) OF 'DIM_S3' (TABLE) (Cost=S5 Card=20) TABLE ACCESS (FULL) OF 'DIM_23' (TABLE) (Cost=2S Card=20) (Обратите внимание: чтобы текст по ширине не превышал ширину страни- цы, я убрал by tes=nnnnn из информации о стоимости, оставив только сами зна- чения стоимости и кардинальности.) ПАРАМЕТР STAFLTRANSFORMATION И ВРЕМЕННЫЕ ТАБЛИЦЫ Параметр star_transformation_enabied может принимать три значения: false (по умолчанию), true и temp_disable. Когда размер таблицы измерения превысит 100 блоков, ваша сессия создаст гло- бальную временную таблицу в памяти для хранения отфильтрованных данных измерения, если вы разрешите выполнение трансформаций типа «звезда», установив значение параметра равным true. В прошлом это приводило к проблемам, вот почему также существует и значение для запрета созда- ния временных таблиц. Порог в 100 блоков контролируется скрытым параметром _temp_tran_block_threshoid, и абсолютное значение этого параметра не зависит от размера блоков табличных пространств, в которых нахо- дятся ваши таблицы измерений. Это еще одна причина, почему надо быть осторожными при пере- мещении объектов в табличные пространства с другим размером блоков. Измените для таблицы из- мерения размер блока, и вы измените количество блоков, в результате чего оптимизатор может вместо глобальной временной таблицы начать использовать базовую таблицу (и наоборот) без вся- кой причины, и последствия могут быть отрицательными. Я разбил план выполнения на части, чтобы было легче увидеть важные опе- рации. Обратите внимание, что строки, отмеченные двумя звездочками (**), яв- ляются тремя копиями одной и той же структуры — метода определения того, как каждая таблица измерения находит секции соответствующей битовой кар- ты в таблице factl. Мы сканируем каждую таблицу измерения по очереди, что- бы получить значения первичных ключей — они, будучи отсортированы, дают нам возможность пройти по соответствующему битовому индексу таблицы фактов, выбирая нужные битовые секции и выполняя их слияние в один бито- вый поток. После того как мы выполнили это для всех трех измерений, у нас будет три битовых потока, к которым мы можем применить операцию bitmap and, конвертируя результирующие биты в соответствующие им идентификато- ры записей. Строка, отмеченная знаком +++, особенно важна. В этой строке оптимиза- тор показывает нам, сколько нужных записей содержится в таблице фактов по его мнению: card = S3 (это верно благодаря тому, что я сгенерировал данные определенным образом), Сравните это с кардинальностью, полученной при ис- пользовании простого соединения хэширования: card = 423 404. Разница весьма значительна. На самом деле разница еще больше, так как итоговая кардинальность
290 Глава 9. Трансформации запросов для всего запроса равна 11 согласно плану трансформации типа «звезда». Раз- ница увеличилась из-за двух различных факторов. о Во-первых, похоже, что есть небольшая ошибка в коде, который обрабатыва- ет часть плана, относящуюся к операции bitmap and. Причиной странных имен измерений является то, что у меня в каждом столбце таблицы factl есть 23,31 и 53 уникальных значения, соответственно (плюс множество дру- гих значений в таблицах измерений, чтобы увеличить их размер). Предика- ты на таблицах измерений выбрали по 20 записей каждый, поэтому в теории они должны были бы выбрать по 20 секций из каждого битового индекса таблицы фактов. Однако расчеты оптимизатора основываются на абсолют- ном значении селективности битовых индексов и не используют реальное количество битовых секций, определенное в каждой из таблиц измерений. В таблице фактов 2 000 000 записей, а селективности трех битовых индексов равны 23, 31 и 53 соответственно, поэтому оптимизатор рассчитывает карди- нальность шага битового доступа как 2 000 000 / (23 х 31 х 53) = 52,925. Это значение является верным в моем примере, но только потому, что я сгенери- ровал мои таблицы измерений таким образом, чтобы 19 из выбранных мною 20 значений не было в таблице фактов. о Вторым фактором, оказывающим влияние, является то, что трансформа- ции типа «звезда» используют нормальные расчеты при выполнении соеди- нения. После использования механизма bitmap and для определения начального набора в таблице factl оптимизатор просто применил стан- дартные расчеты соединения для выполнения соединения трех таблиц изме- рений. А необходимость этого, конечно, под большим вопросом, потому что мы один раз уже выполнили эти расчеты для получения начального количе- ства записей — у каждой соединяемой записи выполняется соединение для увеличения длины записи и какое-то количество записей исключается из рассмотрения или дублируется при втором выполнении соединения. Транс- формации типа «звезда» получают неправильное значение кардинальности, потому что они применяют селективности соединения дважды! Нам даже не нужно знать, как выполняются расчеты соединения, чтобы уви- деть этот второй фактор в действии; мы можем просто использовать коэффици- енты (что может вызвать у вас ужас, шок или удивление), чтобы доказать это. В первом плане выполнения (без трансформации типа «звезда») я начал с 2 000 000 записей, и после трех соединений кардинальность снизилась до 423 404 — коэффициент составил 4,7236. В плане выполнения с трансформаци- ей типа «звезда» у меня была оценка в 53 записи в качестве начальной карди- нальности после выполнения операции bitmap and и перед выполнением со- единений с измерениями. Разделите это значение на тот же коэффициент 4,7236, и вы получите 11,2, а итоговая кардинальность после выполнения со- единений равна 11. Если вы проверите файл трассировки 10053 после того, как разрешили вы- полнение трансформаций типа «звезда», вы обнаружите, что в нем несколько секций general plans. Первая секция — это нормальный план (без трансфор- мации), после которого по очереди выведены отдельные планы для каждой таб-
Соединения типа «звезда» 291 лицы измерения (каждый со своей секцией j oi п orde г [1]) и, наконец, следую- щий заголовок: *************************************** STAR TRANSFORMATION PLANS *********************** После этого вы получаете первый из порядков соединения (снова начинаю- щийся с join order[1]) для выполнения соединения. Странно, что в файле трассировки нет никакой информации о том, какие были использованы расче- ты для определения кардинальности нужного набора данных в таблице фактов. Все, что есть — это внезапно появившаяся предположительная кардинальность (равная 53 записям в моем примере) без какого-либо объяснения. Для завершения обсуждения трансформаций типа «звезда» вы могли бы спросить, почему я использовал такой странный набор данных для демонстрации результатов трансформации. Во-первых, конечно, я хотел сделать очевидными разницу в расчетах и ее причину. Во-вторых, этот набор данных имитирует ши- роко распространенную ошибку дизайна — многие люди помещают множество небольших измерений в одну таблицу со столбцом type. Если вы используете эту стратегию, то один столбец в таблице фактов соот- ветствует поднабору доступных значений в таблице измерения, и необычная се- лективность столбца type приводит к тому, что оптимизатор еще больше уве- личивает разницу между кардинальностью обычного соединения и соединения с трансформацией типа «звезда». Мой предикат гер_23 = 10 мог бы быть ва- шим предикатом ref_type = 'COUNTRY CODE'. Соединения типа «звезда» Чтобы завершить картину, я кратко рассмотрю соединения типа «звезда». На самом деле их не следует рассматривать в этой главе, так как при использова- нии соединения типа «звезда» не выполняется переписывание запроса или ка- кая-либо трансформация. Однако люди иногда путают трансформации типа «звезда» с соединениями типа «звезда», поэтому лучше рассказать о них сейчас, чтобы избежать путаницы. Соединение типа «звезда» просто является специальным случаем оценки порядков соединения, который использует преимущество индекса на множест- ве столбцов в таблице фактов, чтобы выполнить запрос через соединение с де- картовым произведением с таблицами измерений. Возможно, есть некоторые специфические условия, когда это может быть полезно, но я еще не видел ни одного реального рабочего примера, в котором можно было бы получить пре- имущество от использования этой возможности (конечно, вы должны помнить, что соединение типа «звезда» появилось в Oracle версии 7, до появления транс- формации типа «звезда» — это просто может быть примером устаревшей техно- логии). В сценарии starjoin.sql в онлайн-хранилище кода содержится простой при- мер (без данных), чтобы показать этот механизм в действии. У нас есть таблица фактов со следующим определением:
292 Глава 9. Трансформации запросов Create table fact_tab ( idl number not null. id2 numberr not null, id3 numberr not null, small_vc varchar2(10). padding varchar2(100) Constraint f_pk primary key (idl.id2.id3) ): Три столбца IDn соответствуют уникальным столбцам ID трех отдельных таблиц измерений. Выполнив запрос, такой как показан ниже: select /*+ star */ dl.pl, d2.p2, d3.p3, f.small_vc from diml dl, dim2 d2, dim3 d3, fact_tab f where dl.vl = 'abc' and d2.v2 = 'def' and d3.v3 = 1ghi1 and f.idl = dl.id and f.id2 = d2.id and f.id3 = d3.id мы получим следующий план выполнения: План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=6 Card=l Bytes=127) 1 0 NESTED LOOPS (Cost=6 Card=l Bytes=127) 2 1 MERGE JOIN (CARTESIAN) (Cost=6 Card=l Bytes=81) 3 2MERGE JOIN (CARTESIAN) (Cost=4 Card=l Bytes=54) 4 3 TABLE ACCESS (FULL) OF 'DIMI’ (Cost=2 Card=l Bytes=27) S 3 BUFFER (SORT) (Cost=2 Card=l Bytes=27) 6 S TABLE ACCESS (FULL) OF 'DIM2' (Cost=2 Card=l Bytes=27) 7 2BUFFER (SORT) (Cost=4 Card=l Bytes=27) 8 7 TABLE ACCESS (FULL) OF 'DIM3' (Cost»2 Card=l Bytes=27) 9 1 TABLE ACCESS (BY INDEX ROWID) OF 'FACT_TAB' 10 9iNDEX (UNIQUE SCAN) OF 'F_PK' (UNIQUE) Как видите, co строки 2 по строку 8 показано соединение слияния с декарто- вым произведением между тремя таблицами измерений, а также сканирование индекса первичного ключа (в строках 1, 9 и 10) для получения данных из таб- лицы фактов. Следует также указать, что в этом конкретном примере (который не содержит данных) оптимизатор выбрал этот путь доступа к данным без под- сказки star. В файле трассировки 10053 этого примера есть важная информация: без ис- пользования подсказки star файл трассировки в версии 91 показал, что было рассмотрено 24 порядка соединения (4 х 3 х 2 х 1 - 24, так что это был макси- мально возможный список), и показанный выше план был выбран из первого
Соединения типа «звезда» 293 рассмотренного порядка соединения. С использованием подсказки обычная секция general plans файла трассировки исчезает и вместо нее появляется секция с заголовком *************************************** STAR PLANS *********************** После этого заголовка выводятся шесть планов (3 х 2 х 1), и во всех из них таблица f act_tab находится в конце порядка соединения — другими словами, оптимизатор пытается найти только план выполнения, который является со- единением типа «звезда», и другие не рассматривает. Подсказка star не помо- гает оптимизатору найти план выполнения с соединением типа «звезда»; но она не дает оптимизатору тратить время на рассмотрение других вариантов. В об- щем случае цель подсказок — ограничение действий оптимизатора, и подсказка star очень ясно это демонстрирует. Чтобы развить тему, я также создал тестовый пример с семью таблицами из- мерений. Что самое интересное, при использовании подсказки star оптимиза- тор рассмотрел все возможные 5040 порядков соединения, игнорируя тот факт, что значение параметра _optiin1zer_max_permutations было равно 2000. Без использования подсказки star оптимизатор в версии 9i рассмотрел 10 порядков соединения под заголовком general plans, один порядок соединения под заголовком star plans (в версии 8i под этим заголовком были рассмотре- ны все 5040 порядков соединения) и четыре порядка соединения под заголов- ком additional plans. Похоже, что подсказка star дает оптимизатору возмож- ность обойти нормальный код, который определяет баланс между затратами большего количества времени на проверку большего количества порядков со- единения и риском пропустить лучший план выполнения. Заметьте, оптимиза- тор рассмотрел все 5040 порядков соединения за 0,27 с (все эти порядки соеди- нения были простыми без дополнительных настроек для индексного доступа, и это было выполнено на сервере с процессором 2,8 ГГц). The Future Иногда можно видеть, где Oracle использует скрытые параметры и недокумен- тированные функции. В этом разделе описывается пара возможностей, которые вам не следует использовать в рабочей системе — но вы должны знать о них, чтобы понимать, что происходит, когда вы столкнетесь с ними в будущих вер- сиях. В SQL-коде может быть множество вариантов написать один и тот же за- прос, и с течением времени оптимизатор совершенствуется для поиска новых вариантов безопасной и эффективной конвертации между текстами, которые структурированы абсолютно по-разному, но выдают один и тот же результат. Рассмотрим следующий запрос (см. intersectjoin.sql в онлайн-хранилище кода): select n2 from tl where nl < 3 intersect select n2 from t2 where nl < 2
294 Глава 9. Трансформации запросов В принципе, считая, что вы решаете проблему проверки на равенство столб- цов со значениями null, вы можете переписать этот запрос следующим обра- зом: select distinct tl.n2 from tl, t2 where tl.nl < 3 and t2.nl < 2 and t2.n2 = tl.n2 К счастью, проблема сравнения значений null может быть решена (по край- ней мере, внутренне) с помощью недокументированной функции sys_op_map_ nonnull(), которая, похоже, возвращает значение с типом, соответствующим типу входного параметра, даже если это значение, которого обычно в этом типе содержаться не может (обычно это значение 0xFF). В предыдущем примере нам нужно только изменить последний предикат на sys_op_map_nonnull(t2.n2) = sys_op_map_nonnull(tl.п2), чтобы сделать вторую форму запроса правильной трансформацией первого запроса. Посмотрите на план выполнения первого запроса, и вы увидите: План выполнения (версия 10.1.0.4, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=30 Card=30 Bytes=600) 1 0 INTERSECTION 2 1 SORT (UNIQUE) (Cost=10 Card=45 Bytes=360) 3 2 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (TABLE) (Cost=4 Card=45 Bytes=360) 4 3INDEX (RANGE SCAN) OF 'T1_U' (INDEX) (Cost=2 Card=45) 5 1 SORT (UNIQUE) (Cost=20 Card=30 Bytes=240) 6 5 TABLE ACCESS (FULL) OF 'T2' (TABLE) (Cost=14 Card=30 Bytes=240) Но установите значение скрытого параметра _convert_set_to_join рав- ным true (этот параметр появился только в версии 10g), и план изменится сле- дующим образом: План выполнения (версия 10.1.0.4, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=25 Card=33 Bytes=528) 1 0 SORT (UNIQUE) (Cost=25 Card=33 Bytes=528) 2 1 HASH JOIN (Cost=19 Card=33 Bytes=528) 3 2 TABLE ACCESS (FULL) OF 'T2' (TABLE) (Cost=14 Card=30 Bytes=240) 4 2 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (TABLE) (Cost=4 Card=45 ‘ Bytes=360) 5 4INDEX (RANGE SCAN) OF ’T1_I1' (INDEX) (Cost=2 Card=45) План выполнения с соединением хэширования — это в точности то, что вы получите, переписав запрос вручную из формы с использованием пересечения в форму с использованием соединения. И если вы выведете полный отчет пла- на выполнения, вы обнаружите, что access_predicates на соединении хэши- рования на самом деле является sys_op_map_nonnull("t2"."n2") = sys_op_map_nonnull("tl"."n2") Если установлен этот новый параметр, то оптимизатор также может транс- формировать запросы, использующие оператор minus set, в запросы, исполь-
Заключение 295 зующие антисоединения. Трансформация эффективно изменяет первый запрос, который превращается во второй запрос, и разве не хорошо, что теперь вам не придется делать это вручную (ведь при таком переписывании очень легко наде- лать ошибок). select n2 from tl where nl < 3 minus select n2 from t2 where nl < 2 select distinct n2 from tl where nl < 3 and not exists ( select null from t2 where nl < 2 and . sys_op_map_nonnull(t2.n2) = sys_op_map_nonnull(tl.n2) ) Заключение Перед выполнением расчетов оптимизатор может переписать ваш запрос таким образом, чтобы было легче его смоделировать и чтобы потенциально можно было получить больше возможностей его оптимизации. Иногда лучше заблоки- ровать такое переписывание, и существуют подсказки, позволяющие вам сде- лать это. Но оптимизатор выполняет расчет стоимости для исходного и для ре- структурированного запросов, рассматривая все большее количество ситуаций, поэтому теоретически такая блокировка должна стать ненужной (я знаю, что в выпуске 2 версии 10g все трансформации выполняются с учетом стоимости — обычно это хорошо, но вы можете обнаружить, что оптимизация некоторых сложных запросов может теперь занимать больше времени). Есть случаи, когда план выполнения не может адекватно описать то, что со- бирается сделать механизм выполнения. Более того, действия механизма вре- мени выполнения могут очень сильно меняться в зависимости от небольших изменений входного набора данных, поэтому план, который хорошо работает на тщательно сгенерированном тестовом наборе данных, может работать очень плохо на рабочих данных. На возможность оптимизатора трансформировать за- просы может сильно влиять наличие (обычно отсутствие) ограничений not null. Убедитесь, что если в столбце обязательно должны содержаться значения, отличные от null, то этот столбец именно так и должен быть объявлен в базе данных. Таким же образом наличие или отсутствие ограничений уникальности мо- жет привести к изменениям возможных порядков таблиц, когда оптимизатор пытается трансформировать подзапросы. В общем случае, если вы пишете SQL-код с использованием подзапросов, то можете начать писать код, который выглядит как версия решения проблемы на
296 Глава 9. Трансформации запросов естественном языке, так как другим людям будет проще всего понять именно такой вариант. Обычно оптимизатор может трансформировать такие запросы в более эффективное представление. Но иногда вы можете обнаружить, что «очевидная» трансформация не происходит, в этом случае вам придется выпол- нить трансформацию вашего кода вручную. Если вы это делаете, убедитесь, что новый вариант кода логически не отличается от исходного. Трансформации типа «звезда» являются широко распространенной страте- гией соединения в хранилищах данных. Но возможно, что когда вы включите эту функциональность, в части ваших планов выполнения сильно изменится кардинальность. Это может быть вызвано коллизией между вашей стратегией создания таблиц измерений и расчетами, которые использует оптимизатор при выполнении операции bi tmap and. При каждом обновлении версии или даже внедрении нового выпуска может быть включена пара новых трансформаций. Обычно это означает, что оптими- затор может найти лучший вариант выполнения запроса, и иногда новая транс- формация может привести к отрицательным результатам при использовании вашего специфического набора данных. Если производительность важного за- проса сильно изменилась (в лучшую или худшую сторону) после обновления версии, сравните старый план выполнения с новым — могли появиться новые варианты трансформации, о которых вы не знаете. Тестовые сценарии Файлы к этой главе, доступные для загрузки, перечислены в табл. 9.3. Таблица 9.3. Тестовые сценарии к главе 9 Сценарий Комментарии filter_cost_01.sql push_subq.sql Пример выбора пути доступа с использованием подзапроса с фильтрацией Пример использования подзапросов на ранней стадии плана выполнения (только в версии 8.1) ord_pred.sql filter_cost_02.sql Демонстрация подсказки ordered ..predicates Подзапрос с фильтрацией работает быстрее, если управляющая таблица отсортирована filter_cost_01a.sql Как случай может повлиять на производительность подзапроса с фильтрацией scalar_sub_Ol.sql Использование скалярного подставляемого подзапроса вместо традиционного коррелированного подзапроса scalar_sub_02.sql Использование скалярного подзапроса для имитации «детерминированности» в вызовах функций scalar_sub_03.sql with_subq_Ol.sql Использование скалярного подзапроса для изучения хэш-таблицы Простой пример скалярных подзапросов, используемых для генерации больших наборов данных with_subq_02.sql Более сложный пример использования скалярных подзапросов для упрощения решения проблем view_merge_01.sql push_pred.sql Демонстрация изменений, вызванных complex_view_merging Демонстрация включения предикатов соединения в представления
Тестовые сценарии 297 Сценарий Комментарии unnest_cost_01.sql Первый пример с фильтрацией, в котором SQL-код написан с уменьшением уровней вложенности запроса вручную unnest_cost_02.sql Фильтрация, которая вроде бы должна быть выполнена (в версии 10g), но ее не происходит unnest_costL.01a.sql То же, что и unnest_cost_01.sql, с изменениями, в которых требуются усредненные значения seml_01.sql anti_01.sql book_subq.sql notin.sql Примеры, в которых выполняются полусоединения Примеры, в которых выполняются антисоединения Пример аномалии, когда not In делает то, чего not exists сделать не может Демонстрация проблемы со значениями null при использовании подзапросов not In ordered.sql Пример того, как подсказка ordered может вызвать проблемы при обновлении версии •star_trans.sql starjoln.sql Пример трансформации типа «звезда» Простая демонстрация соединения типа «звезда» (не трансформации типа «звезда») Intersectjoln.sql Демонстрация того, как в версии 10g операция над множествами конвертируется в соединение (или антисоединение) setenv.sql Установка стандартизированной тестовой среды для SQL*Plus
*| Г| Кардинальность I " соединения Чему равно максимальное количество таблиц, по которым Oracle может выпол- нить соединение за один раз? Вы можете быть удивлены, узнав, что ответ — две. Не важно, сколько таблиц в вашем запросе — при выполнении соединения Oracle будет обрабатывать только два объекта за один раз. На самом деле вы даже можете сказать, что у оптимизатора нет долгосрочной стратегии для со- единений, он просто берет то, что есть на данный момент, и выполняет соедине- ние со следующей доступной таблицей, чтобы посмотреть, что произойдет. Конечно, это звучит несколько странно, но это недалеко от истины. Чтобы выполнить соединение пяти таблиц, оптимизатор выбирает стартовую таблицу и выполняет ее соединение с другой таблицей; после этого он берет промежу- точный результат и выполняет соединение с еще одной таблицей; берет проме- жуточный результат... и т. д., пока не будут обработаны все пять таблиц; поэто- му абсолютно верно, что Oracle может выполнить соединение только двух таблиц. Но существует стратегия, которая заключается в том, чтобы разумно вы- брать подходящий порядок соединения, а затем методично перебирать вариан- ты изменений этого порядка соединения таким образом, чтобы минимизиро- вать риск потерь времени на действительно плохие порядки соединения и мак- симизировать вероятность нахождения лучшего порядка соединения. Существуют три вопроса, которые нужно рассматривать при обсуждении со- единений: как оптимизатор рассчитывает кардинальность соединения, как оп- тимизатор рассчитывает стоимость соединения и как в действительности вы- полняется каждый тип соединения. В этой главе мы сосредоточимся только на расчете кардинальности. Базовая кардинальность соединения Корпорация Oracle опубликовала пару формул расчета кардинальности соеди- нения в сообщении 68 992 на MetaLink. Первая формула датируется мартом 1999 года и в последний раз обновлялась в апреле 2004 года (на момент написа-
Базовая кардинальность соединения 299 ния этой книги). Последняя версия формулы определения селективности со- единения выглядит следующим образом: Селективность = 1 / max[(NDV(tl.cl), NDV(t2.c2)] * ((кардинальность таблицы tl - количество значений NULL в столбце tl.cl) / кардинальность таблицы tl) * ((кардинальность таблицы t2 - количество значений NULL в столбце t2.c2) / кардинальность таблицы t2)) Кардинальность соединения получается следующим образом: Кардинальность(Pj) = кардинальность(tl) * кардинальность(t2) * селективность(Pj) Эти формулы не полностью согласованы по стилю и требуют небольшого пояснения — например, кардинальность таблицы tl в формуле селективности не означает то же самое, что кардинальность(tl) в формуле кардинальности. Чтобы прояснить значение этих выражений, я переписал формулы следующим образом: Селективность соединения = ((num_rows(tl) - num_nulls(tl.cl)) I num_rows(tl)) * ((num_rows(t2) - num_nulls(t2.c2)) I num_rows(t2)) I greater(num_distinet(tl.cl), num_distinct(t2.c2)) Кардинальность соединения = Селективность соединения * кардинальность по отфильтрованным записям(11) * кардинальность по отфильтрованным записям(12) Пояснение: предполагается, что мы выполняем соединение таблиц tl и t2 по столбцам cl и с2, соответственно. Мы получаем num_rows из user_tables по каждой из двух таблиц, получаем num_nulls и num_di sti net из представле- ния user_tab_col_statisties для двух столбцов, а затем подставляем полу- ченные значения в формулу расчета селективности соединения (далее вы уви- дите, что есть вариант формулы, который использует density вместо num_ di sti net). Однако кардинальность соединения включает кардинальность по отфильт- рованным записям каждой таблицы. Вспомните, что команда SQL в общем слу- чае может включать некоторые предикаты, не являющиеся предикатами соеди- нения. Эти дополнительные предикаты фильтрации должны быть применены к таблицам в первую очередь для получения кардинальности по отфильтрован- ным записям — и возможно, что некоторые из предикатов соединения могут также функционировать как предикаты фильтрации. Формулу проще всего объяснить с помощью примера. Ниже показан фраг- мент сценария join_card_01.sql из онлайн-хранилища кода. Как обычно, в моей демонстрационной среде используются блоки размером 8 Кбайт и экстенты размером в 1 Мбайт, локально управляемые табличные пространства и руч- ное управление размером сегмента, системная статистика (CPU costing) от- ключена, create table tl as select trunc(dbms_random.value(0, 25 )) filter,
300 Глава 10. Кардинальность соединения trunc(dbms_random.value(0, 30 )) jотnl, lpad(rownum,10) vl, rpad('x',100) padding from all_objects where rownum <= 10000 create table t2 as select trunc(dbms_random.value(0, 50 )) filter, trunc(dbms_random,value(0, 40 )) joinl, lpad(rownum,10) vl, rpad('x',100) padding from all_objects where rownum <= 10000 Сбор статистики с помощью dbms_stats select tl.vl, t2.vl from tl, t2 where tl.filter = 1 and t2.joinl = tl.joinl and t2.fi Iter = 1 Во всех тестовых примерах в этой главе я использовал процедуру dbms_ random. valueO для генерации случайных, но предсказуемых данных с помо- щью параметров (low, high) и усечения результата для управления количест- вом уникальных значений в столбцах фильтрации и соединения. Для этого примера получается следующее: tl.filter 25 значений t2.filter 50 значений tl.joinl 30 значений t2.joinl 40 значений Co столбцами фильтрации все просто — учитывая, что в обеих таблицах по 10 000 записей, кардинальность по отфильтрованным записям таблицы tl бу- дет равна 400 (10 000 записей, разделенные на 25 уникальных значений), а кар- динальность по отфильтрованным записям таблицы t2 будет равна 200 (10 000 за- писей, разделенные на 50 уникальных значений). Так как ни в одной из таблиц нет значений null, формулы определения кар- динальности соединения дадут следующий результат: Селективность соединения = (10 000 - 0) / 10 000) * (10 000 - 0) / 10 000) / greater(30, 40) = 1/40 Кардинальность соединения = 1/40 * (400 * 200) = 2000 Можно уверенно сказать, что, выполнив запрос с автотрассировкой (auto- trace), мы увидим следующий план выполнения:
Базовая кардинальность соединения 301 План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=57 Card=2000 Bytes=68000) 1 0 HASH JOIN (Cost=57 Card=2000 Bytes=68000) 2 1 TABLE ACCESS (FULL) OF 'T2' (Cost=28 Card=200 Bytes=3400) 3 2 TABLE ACCESS (FULL) OF 'Tl' (Cost=28 Card=400 Bytes=6800) В плане выполнения кардинальность по отфильтрованным записям таблицы tl равна 400, кардинальность по отфильтрованным записям таблицы t2 равна 200, и кардинальность соединения равна 2000, как мы и спрогнозировали. По- хоже, формулы верны. Мы можем усложнить тест (см. jo1n_card_.02.sql в онлайн-хранилище кода), поместив значение nulI в столбец соединения каждой двадцатой записи табли- цы tl и каждой тридцатой записи таблицы 12: update tl set joinl = null where mod(to_number(vl),20) = 0; обновлено 500 записей update t2 set joinl = null where mod(to_number(vl),30) = 0: обновлено 333 записи С учетом этого изменения данных (и после сбора статистики) нам нужно бу- дет добавить в селективность соединения еще один компонент: Селективность соединения = (10 000 - 500) / 10 000) * (10 000 - 333) / 10 000) / greater(30, 40) = 0.022959125 Кардинальность соединения = 400 * 200 * 0.022959125 = 1836.73 И снова мы видим, что кардинальность, предсказанная формулой, совпадает со значением кардинальности 1837, выведенным в автотрассировке. План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=57 Card=1837 Bytes=62458) 1 0 HASH JOIN (Cost=57 Card=1837 Bytes=62458) 2 1 TABLE ACCESS (FULL) OF 'T21 (Cost=28 Card=200 Bytes=3400) 3 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=28 Card=400 Bytes=6800) Мы можем сделать еще одно, последнее усложнение, добавив значения null в столбцы фильтрации (см. jo1n_card_03.sql в онлайн-хранилище кода) так же, как мы это сделали для столбцов соединения. Следующий код помещает 200 зна- чений null в столбец фильтрации таблицы tl и 100 значений null в таблицу t2: update tl set filter = null where mod(to_number(vl),50) = 0; обновлено 200 записей update t2 set filter = null
302 Глава 10. Кардинальность соединения where mod(to_number(vl),100) = 0; обновлено 100 записей У нас уже есть значение селективности соединения, равное 0,22959125, из предыдущего примера, поэтому все, что нужно сделать, — это определить кар- динальность по отфильтрованным записям каждой таблицы при наличии зна- чений null в столбцах предикатов фильтрации. В главе 3 я показывал вам, как это можно сделать. Формула выглядит следующим образом: Измененная (рассчитанная) кардинальность = базовая селективность * (num_rows - num_nulls) о Для таблицы tl получаем 1/25 х (10 000 - 200) = 392. о Для таблицы t2 получаем 1/50 х (10 000 - 100) = 198. Поместив эти значения в формулу расчета кардинальности соединения, мы получим Кардинальность соединения = 392 * 198 * 0.022959125 = 1781.995 Сравним это значение с результатом автотрассировки, обратив также внима- ние на кардинальность каждого отдельного табличного сканирования: План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=57 Card=1782 Bytes=60588) 1 0 HASH JOIN (Cost=57 Card=1782 Bytes=60588) 2 1 TABLE ACCESS (FULL) OF 'T2' (Cost=28 Card=l'98 Bytes=3366) 3 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=28 Card=392 Bytes=6664) Итак, похоже, что формулы верны — по крайней мере, в очень простых слу- чаях. (Странно, что расчеты в версии 8i немного неверны и показывают карди- нальность равной 199 вместо 198 для таблицы 12, что приводит к кардинально- сти соединения, равной 1791. Возможно, это пример простой ошибки в расче- тах — деление на 5 является распространенной причиной возникновения ошиб- ки в N-м десятичном разряде.) К сожалению, существует множество проблем, которые все еще нужно рас- смотреть. Давайте определим несколько вопросов об ограничениях использова- ния формул. о Что вы будете делать, если у вас два или более столбца соединения? о Что вы будете делать в том случае, если условие соединения включает ска- нирование диапазона по индексу? о Каким образом вы выполните соединение с третьей таблицей? о Почему не похоже, что формула учитывает случаи, когда диапазоны значе- ний столбцов соединения пересекаются только частично? о Оказывают ли какое-либо влияние гистограммы? Проведя эксперименты, мы можем найти ответы на некоторые из этих во- просов. Как обычно, выясняется, что в коде содержится множество различных стратегий, а также некоторые решения, которые кажутся не очень разумными,
Базовая кардинальность соединения 303 обработка некоторых специальных случаев и некоторые вещи, скорее всего, яв- ляющиеся ошибками. Влияние предиката фильтрации только с одной стороны соединения Давайте завершим этот раздел одним очень простым примером, в котором оп- тимизатор «нарушает» правила — или, скорее, использует правило, о котором мы не знали. Давайте вернемся к простоте нашего первого примера (без значе- ний null, простое сравнение на равенство), но создадим пример, в котором фильтрация применяется только к одной из двух таблиц в соединении (см. сце- нарий join_card_01a.sql в онлайн-хранилище кода): create table tl as select trunc(dbms_random.value(0, 100 )) filter, trunc(dbms_random.value(0, 30 )) joinl, lpad(rownum,10) vl, rpad(’x’,100) padding from all_objects where rownum <= 1000 create table t2 as select trunc(dbms_random.value(0, 100 )) trunc(dbms_random.value(0, 40 )) lpad(rownum,10) vl, rpad('x',100) padding from all_objects where rownum <= 1000 filter, joinl, Сбор статистики с помощью dbms_stats select tl.vl, t2.vl from tl, t2 where t2.joinl = tl.joinl -- and tl.filter = 1 and t2.filter = 1 В этом примере в каждой таблице находится по 10 000 записей и в столбце фильтрации находится 100 уникальных значений. Показанный выше запрос производит фильтрацию записей только в таблице 12, в таблице tl фильтрация закомментирована, а в полном сценарии этого тестового примера есть и второй за- прос, в котором фильтрация записей производится только в таблице tl, а в табли- це 12 она закомментирована. В столбце соединения таблицы tl существуют 100 уни- кальных значений, в столбце соединения таблицы t2 — 40 уникальных значений. Если вы посмотрите на формулу селективности, то увидите, что на самом деле не важно, на какой таблице существует условие фильтрации; для расчета
304 Глава 10. Кардинальность соединения селективности требуются только значения num_distinct столбцов соедине- ния: Селективность соединения = ((num_rows(tl) - num_nulls(tl.cl)) / num_rows(tl)) * ((num_rows(t2) - num_nulls(t2.c2)) / num_rows(t2)) / greater(num_distinct(tl.cl), num_distinct(t2.c2)) в этом случае: Селективность соединения = (1000/1000) * (1000/1000) / greater(30, 40) = 1/40 = 0.025 Так как условие фильтрации одно и то же (одно значение из 100) для обеих таблиц, кардинальность соединения не должна зависеть от того, на какой таб- лице применяется фильтрация: Кардинальность соединения = Селективность соединения * кардинальность по отфильтрованным записям(11) * кардинальность по отфильтрованным записям(12) = 0.025 * 10 * 1000 = 250 У нас получается два плана выполнения: План выполнения (версия 9.2.0.6, фильтрация только таблицы Т1) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=ll Card=250 Bytes=7750) 1 0 HASH JOIN (Cost=ll Card=250 Bytes=7750) 2 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=5 Card=10 Bytes=170) 3 1 TABLE ACCESS (FULL) OF 'T2' (Cost=5 Card=1000 Bytes=14000) План выполнения (версия 9.2.0.6, фильтрация только таблицы Т2) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=ll Card=333 Bytes=10323) 1 0 HASH JOIN (Cost=ll Card=333 Bytes=10323) 2 1 TABLE ACCESS (FULL) OF 'T2' (Cost=5 Card=10 Bytes»170) 3 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=5 Card=1000 Bytes=14000) Измените таблицу, на которой выполняется фильтрация, и кардинальность результата изменится. При фильтрации таблицы tl мы получим кардиналь- ность, равную 250, что и подразумевается формулой. При фильтрации таблицы t2 кардинальность становится равной 333. Можете ли вы догадаться, почему так происходит? Является ли совпадением то, что 1000/30 “ 333? Алгоритм расчета стоимости соединения несколько изменяется, когда пре- дикат фильтрации применяется только к одной из сторон соединения — мы мо- жем использовать значения n um_d 1 s 11 n c t из таблицы на другой стороне соеди- нения, а не большее из двух значений num_distinct (и я подозреваю, что это просто очень специфический случай более общего правила, определяющего, как предикаты фильтрации могут влиять на принятие оптимизатором решения, из какой таблицы использовать значение num_disti net). При фильтрации только таблицы t2 мы используем значение num_di sti net, равное 30, из таблицы tl, а не большее значение num_distinct, равное 40, ко- торое требуется использовать по стандартной формуле.
Кардинальность соединения в реальном SQL-коде 305 Кардинальность соединения в реальном SQL-коде Если в вашей системе не используются везде только синтетические ключи, со- стоящие из одного столбца, то вам, скорее всего, приходится писать SQL-код с двумя или более столбцами в условии соединения двух таблиц. Как же в та- ком случае расширить базовую формулу для обработки соединений по несколь- ким столбцам? Стратегия та же, что и при использовании нескольких предика- тов на одной таблице: просто примените формулу для каждого предиката по очереди и перемножьте значения для получения итоговой селективности со- единения (а затем ожидайте ловушки, которая появляется в версии 10g из-за контроля ошибок — sanity checks'). Рассмотрим следующий фрагмент (см. join_card_04.sql в онлайн-хранилище кода): create table tl as select trunc(dbms_random.value(0, 30 )) joinl, trunc(dbms_random.value(0, 50 )) joinZ, lpad(rownum,10) vl, rpad('x’,100) padding from all_objects where rownum <= 10000 create table t2 as select trunc(dbms_random.value(0, 40 )) joinl, trunc(dbms_random.value(0, 40 )) joinZ, lpad(rownum,10) vl, rpad('x',100) padding from all_objects where rownum <= 10000 Сбор статистики с помощью dbms_stats select tl.vl, t2.vl from tl, t2 where t2.joinl = tl.joinl and t2.join2 = tl.joinZ Выполнив этот код с автотрассировкой (в версиях 9i и 81 кардинальность должна быть одна и та же, хотя стоимость может различаться), мы получим следующий план выполнения: План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=60 Card=50000 Bytes=1700000) 1 0 HASH JOIN (Cost=60 Card=50000 Bytes=1700000) 2 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=28 Card=10000 Bytes=170000) 3 1 TABLE ACCESS (FULL) OF 'T2' (Cost=28 Card=10000 Bytes=170000)
306 Глава 10. Кардинальность соединения Значения кардинальности таблиц tl и t2 верны; мы не выполняли фильтра- цию записей при сканировании таблиц. Следовательно, кардинальность соеди- нения, равная 50 000, должна получиться из селективности соединения, равной 1/2000, умноженной на 100 000 000 (то есть 10 000 х 10 000) — но где в нашем определении данных можно найти эти «магические» 2000? Если рассмотреть вариант применения формулы расчета селективности два- жды, то от этого варианта придется отказаться. Важным компонентом в нашем случается является greater(num_distinet(tl.cl), num_distinct(t2.c2)) У нас существуют два компонента соединения, tl. joinl = t2. joinl и tl. join2 = t2.join2. В главе 3 вы узнали, что комбинирование значений селек- тивности отдельных таблиц производится через их перемножение (по крайней мере, это происходит, когда между отдельными предикатами существует опера- ция and). То же самое делается и со значениями селективности соединения: Селективность соединения = {компонент joinl} * {компонент join2} = (10 000 - 0) / 10 000) * (10 000 - 0) / 10 000) / greater(30, 40) * -- используется селективность таблицы t2 (1/40) (10 000 - 0) / 10 000) * (10 000 - 0) / 10 000) / greater(50, 40) = -- используется селективность таблицы tl (1/50) 1/40 * 1/50 = 1/2000 (что и требовалось) Поэтому все, что мы сделали, — рассмотрели части соединения по очереди и использовали наибольшую селективность в каждом случае — даже если это значит, что селективность получается из другой таблицы на каждом шаге рас- четов. ПРИМЕЧАНИЕ Вы могли столкнуться со старой ошибкой, когда оптимизатор не замечает повторяющихся предика- тов (например, добавьте дополнительный предикат t2.join2 = tl.join2 в предыдущий запрос, и кар- динальность станет равной 1000 вместо 50 000, потому что оптимизатор выполнил расчеты для столбцов join2 дважды). Различные аспекты этой проблемы были решены в версии 9i. Затем, как только вы подумали, что теперь знаете, как это работает, вы- полните этот пример в версии 10g, и результат автотрассировки окажется другим: План выполнения (версия 10.1.0.4, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=60 Card=62500 Bytes=2125000) 1 0 HASH JOIN (Cost=60 Card=62500 Bytes=2125000) 2 1 TABLE ACCESS (FULL) OF 'Tl' (TABLE) (Cost=28 Card=10000
Кардинальность соединения в реальном SQL-коде 307 Bytes=170000) 3 1 TABLE ACCESS (FULL) OF 'T2' (TABLE) (Cost=28 Card=10000 Bytes=170000) Как рассчитанная кардинальность, равная 50 000 в плане выполнения в вер- сии 9i, превратилась в 62 500 в предыдущем плане выполнения? Если вы хоро- шо выполняете расчеты в уме, то можете получить ответ очень быстро — вот почему важно убедиться, что в ваших экспериментах в результате получаются удобные, простые числа. При возникновении трудностей я всегда стараюсь вер- нуться назад — в этом случае от кардинальности соединения к селективности соединения: 62 500 = 100 000 000 * селективность соединения. Селективность соединения = 62 500/100 000 000 = 1/1600 Можем ли мы найти значение 1600 где-либо в нашем тестовом примере? Да, можем: 1/1600 = 1/40 х 1/40. Оптимизатор использовал два значения селективности с одной и той же сто- роны соединения вместо использования по одному значению селективности с каждой стороны соединения. На самом деле мы можем найти этому подтвер- ждение в файле трассировки 10053. Есть два указания на это. В разделе single table access path мы видим Table: Tl Multi-column join key card: 1500.000000 Table: T2 Multi-column join key card: 1600.000000 На самом деле если у вас есть подходящие индексы по нескольким столб- цам, то в этой части файла трассировки также выводится количество уникаль- ных ключей в этих индексах с описанием Concatenated index card. Затем, в конце секции NL Join (в которой было определено значение карди- нальности соединения) мы видим следующее: Using multi-column join key sanity check for table T2 Revised join selectivity: 6.2500e-004 = 5.0000e-004 * (1/1600) * (1/5,0000e-004) Join Card: 62500.00 = outer (10000.00) * inner (10000.00) * sei (6.2500e-004) Опять же, при наличии подходящих индексов по нескольким столбцам пер- вая из этих строк могла бы ссылаться на concatenated index cardinality (кардинальность составного индекса) таблицы t2. Интересно, что вы можете решить добавить столбец в индекс, чтобы некоторые запросы смогли обойтись только этим индексом. Побочным эффектом может стать прекращение исполь- зования контроля ошибок (sanity check) в версии 10g для любых соединений, в которых используются только столбцы из исходного определения индекса. Насколько я знаю, такое замещение происходит всегда (по крайней мере, при выполнении простых соединений двух таблиц). Оптимизатор определяет (возможно, просто из-за использования старого алгоритма) самое плохое значе- ние селективности, по очереди выбирая большее из двух значений num_dis- tinct для каждой пары столбцов соединения. Затем он определяет по очереди эквивалентное значение для каждой таблицы, используя только значения num_ distinct этой таблицы, после чего использует меньшее значение селективности
308 Глава 10. Кардинальность соединения (в этом примере значение 1/1600 меньше значения 1/1500). Возможно, пара- метр _optimizer_ jol n_sel_sani ty_check и является параметром, контроли- рующим это поведение. В качестве последней проблемы можно упомянуть, что если значение multi- column join key cardinality отдельной таблицы становится меньше значения l/num_rows для этой таблицы, то, похоже, оптимизатор использует l/num_rows вместо рассчитанного значения. Расширения и аномалии Учитывая, что мы теперь знаем, как выполняются базовые расчеты для про- стых запросов и правильно подобранных данных, мы можем рассмотреть неко- торые из множества возможных вариаций запросов. Соединения по диапазону Специфическим случаем, который мы еще не рассматривали, является вопрос, что делает оптимизатор при выполнении соединений по диапазону. Ответ прост: оптимизатор использует заранее определенное фиксированное значение в каче- стве селективности такого предиката соединения — так же, как он это делает для одиночных таблиц и переменных связывания. Например, выполните сценарий join_card_01.sql еще раз, изменив предикат соединения с t2.joinl = tl.joinl на t2.joinl > tl.joinl Автотрассировка показывает, что кардинальность изменилась с 2000 до 4000. Селективность, равная 1/40 (со стороны соединения, относящейся к t2. jolnl), была заменена фиксированным значением 5 % (1/20). Также обратите внима- ние, что механизм соединения вместо соединения хэширования стал использо- вать соединение слияния. Соединения хэширования могут использоваться только при проверках на равенство; они не могут использоваться в соединениях по диапазону. План выполнения (автотрассировка версии 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=68 Card=4000 Bytes=136000) 1 0 MERGE JOIN (CoSt=68 Card=4000 Bytes=136000) 2 1 SORT (JOIN) (Cost=34 Card=200 Bytes=3400) 3 2 TABLE ACCESS (FULL) OF 'T2' (Cost=28 Card=200 Bytes=3400) 4 1 SORT (JOIN) (Cost=35 Card=400 Bytes=6800) 5 4 TABLE ACCESS (FULL) OF 'Tl' (Cost=28 Card=400 Bytes=6800) Теперь изменим предикат на t2.joinl between tl.joinl - 1 and tl.joinl + 1 и кардинальность соединения станет равной 200. Вспомните, что оптимизатор обрабатывает between :bindl and :bind2 как пару независимых предикатов
Расширения и аномалии 309 и умножает 1/20 на 1/20 для получения селективности, равной 1/400 (0,025 %). Как раз это и происходит в данном случае. Селективность соединения изменилась с исходного значения 1/40 на 1/400, в результате чего кардинальность уменьшилась в те же 10 раз с 2000 до 200. План выполнения (автотрассировка, версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=68 Card=200 Bytes=6800) 1 0 MERGE JOIN (Cost=68 Card=200 Bytes=6800) 2 1 SORT (JOIN) (Cost=34 Card=200 Bytes=3400) 3 2 TABLE ACCESS (FULL) OF 1T2' (Cost=28 Card=200 Bytes=3400) 4 1 FILTER 5 4 SORT (JOIN) 6 5 TABLE ACCESS (FULL) OF 'Tl1 (Cost=28 Card=400 Bytes=6800) Конечно, нельзя не заметить, что исходное выражение соединения t2. joi nl = tl. joinl с рассчитанной кардинальностью, равной 2000, определяет под- множество соединения по диапазону t2.joinl between tl.joinl - 1 and tl. joinl + 1, для которого рассчитана кардинальность, равная 200. Это явно проблема согласованности, которую нужно иметь в виду при выполнении рас- четов для соединений по диапазону. Я объясню важность операции f 11ter в строке 4 в главе 13. Можно заметить небольшую ошибку при выводе, из-за которой строка sort(joiп) под строкой filter теряет значение стоимости, которое должно быть равным 34 (как в строке 2), хотя в операции merge joiп в строке 1 это значение учитывается. Неравенства Следующим специальным случаем являются неравенства: tl.joinl != t2. j oi n 1. В этом случае применяется то же правило, которое мы узнали для селек- тивности одной таблицы: селективность (not(tl.joinl = t2.joinl)) равна просто 1 минус селективность предиката (tl. joinl = 12. joinl). Например, если у нас есть следующий запрос (опять же из join_card_04.sql): select tl.vl, t2.vl from tl, t2 Where t2.joinl != tl.joinl -- (30/40 значений в num_distinct) and t2.join2 != tl.join2 -- (50/40 значений в num_distinct) I в котором мы использовали 1/40 х 1/50 в расчетах, когда оба предиката были равенствами, то теперь мы используем 39/40 х 49/50, потому что у нас два не- 4 равенства. При этом существуют некоторые странные побочные эффекты, даже можно сказать, ошибки. Рассмотрим запрос с операцией дизъюнкции {disjunct, OR) меж- ду двумя предикатами: select tl.vl, t2.vl from
310 Глава 10. Кардинальность соединения tl, t2 where t2.joinl = tl.joinl -- (30/40 значений в num_distinct) or t2.join2 = tl.join2 -- (50/40 значений в num_distinet) Все три главные версии Oracle показывают кардинальность этого запроса равной 2 125 000 (см. join_card_05.sql в онлайн-хранилище кода), но единствен- ная версия, в которой автотрассировка дает разумную информацию о том, что происходит, — это 10g, и в ней результат автотрассировки выглядит следую- щим образом: План выполнения (версия 10.1.0.4, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=120 Card=2125000 Bytes=72250000) 1 0 CONCATENATION 2 1 HASH JOIN (Cost=60 Card=2000000 Bytes=68000000) 3 2 TABLE ACCESS (FULL) OF 'Tl' (TABLE) (Cost=28 Card=10000 Bytes=170000) 4 2 TABLE ACCESS (FULL) OF 'T2' (TABLE) (Cost=28 Card=10000 Bytes=170000) 5 1 HASH JOIN (Cost=60 Card=125000 Bytes=4250000) 6 5 TABLE ACCESS (FULL) OF 'Tl' (TABLE) (Cost=28 Card=10000 Bytes=170000) 7 5 TABLE ACCESS (FULL) OF 'T2' (TABLE) (Cost=28 Card=10000 Bytes=170000) Обратите внимание, что в строке 2 показана кардинальность, равная 2 000 000. В версиях 8i и 9i в этой строке выводится кардинальность, равная 125 000. Это значит, что в этих версиях в строке 0 должна быть выведена общая кардинальность, равная 250 000, — но в обеих версиях показана общая карди- нальность, равная 2 125 000. Проблема с итоговой кардинальностью заключается в том, что она абсолют- но неверна. На самом деле, если вы посмотрите трассировку 10053, вы обнару- жите, что на самом первом шаге оптимизатор получает правильную кардиналь- ность (равную 4 450 000), а затем проходит через долгий процесс, в результате получая неверное значение (если вы укажете в запросе подсказку /*+ по_ expand */, то Oracle выполнит одно соединение, а в плане выполнения будет введена правильная кардинальность, но соединение окажется неэффективной операцией с использованием вложенных циклов). ИСПОЛЬЗОВАНИЕ ПОДСКАЗОК В некоторых случаях может понадобиться с помощью подсказок указать оптимизатору, что нужно задействовать определенный механизм выполнения соединения для расчета правильного значения кардинальности. На различных форумах Oracle достаточно часто появляются сообщения с примера- ми SQL-кода, когда некоторую функциональность (или улучшение) требуется отключить с помощью подсказки, потому что эта функциональность подходит не для каждого случая.
Расширения и аномалии 311 Значение 2 000 000, которое появляется при выполнении первого соедине- ния хэширования, возможно, получается из предиката t2.Join2 = tl.joln2 при использовании селективности, равной 1/50. Я думаю, что значение 125 000 во втором соединении хэширования получается с помощью применения селек- тивности, равной 1/40, к предикату соединения t2. joinl = tl. joinl, а затем применением ненужного и неверно использованного коэффициента 5 % (коэф- фициент селективности переменных связывания, bind selectivity factor) для ис- ключения записей, которые ранее были определены предикатом j oi n2. Есть два способа узнать правильное значение кардинальности; один из них состоит в том, чтобы просто переписать запрос в эквивалентную форму, пока- занную ниже вместе с ее планом выполнения: select tl.vl, t2.vl from tl, t2 where t2.join2 = tl.join2 union all select tl.vl, t2.vl from tl, t2 where t2.joinl = tl.joinl and t2.join2 != tl.join2 План выполнения (версия 10.1.0.4, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=120 Card=4450000 Bytes=139300000) 1 0 UNION-ALL 2 1 HASH JOIN (Cost=60 Card=2000000 Bytes=56000000) 3 2 TABLE ACCESS (FULL) OF 'Tl' (TABLE) (Cost=28 Card=10000 Bytes=140000) 4 2 TABLE ACCESS (FULL) OF ’T2’ (TABLE) (Cost=28 Card=10000 Bytes=140000) S 1 HASH JOIN (Cost=60 Card=2450000 Bytes=83300000) 6 5 TABLE ACCESS (FULL) OF 'Tl' (TABLE) (Cost=28 Card=10000 Bytes=170000) 7 5 TABLE ACCESS (FULL) OF 'T2' (TABLE) (Cost=28 Card=10000 Bytes=170000) Как видите, при явном указании операции uni on all вместо того, чтобы по- зволить оптимизатору выполнить неявное объединение, вы получаете совер- шенно другую кардинальность во второй половине запроса. Альтернативным способом определить, что кардинальность, равная 2 125 000, неверна и что верное значение — 4 450 000, является возврат к фор- муле объединения предикатов, которую мы видели в главе 3:
312 Глава 10. Кардинальность соединения Селективность (предикат! ИЛИ предикат2) = селективность (предикат!) + селективность (предикат2) - селективность (предикат! И предикат2) -- в противном случае вы учтете пересечение предикатов дважды Из тестовых примеров, которые мы до этого использовали (в основном это join_card_01.sql и join_card_04.sql), мы уже знаем индивидуальные значения се- лективности; tl.joinl = t2.joinl 1/40 tl.join2 = t2.join2 1/50 tl.joinl = t2.joinl and tl.join2 = t2.join2 1/2000 Таким образом, селективность tl.joinl = t2.joinl or tl.join2 = t2. join2 должна быть следующей: 1/40 + 1/50 - 1/2000 = 89/2000 = 0.0445 Примените это значение к 100 000 000 записей, получающимся в результате выполнения соединения с декартовым произведением двух таблиц без фильт- рации, и кардинальность соединения станет равной 4 450 000 — что и требова- лось. Ясно, что есть случаи, когда код, используемый для определения значений селективности и кардинальности соединения, не является самосогласованным. Вам может потребоваться предпринять некоторые действия, чтобы обойти про- блемы, которые это может вызвать. Пересечения диапазонов значений Существует внутренняя проблема реализации. До этого момента мы использо- вали специально подобранные данные, но сейчас мы поступим иначе. Преиму- ществом наших данных является то, что количество уникальных значений в столбцах соединения практически совпадает, и диапазоны (наименыпее/наи- болыпее значения) значений в столбцах соединения также практически совпа- дают. Другими словами, предполагается, что по двум нашим таблицам будет выполнено нормальное соединение. Давайте посмотрим, что произойдет с кардинальностью соединения, когда пересечение диапазонов значений не такое точное. Пример (см. join_card_06,sql в онлайн-хранилище кода) создает две таблицы по 10 000 записей каждая, а за- тем выполняет команду select с одним простым предикатом: tl.joinl = t2.joinl В обоих столбцах joinl содержится по 100 уникальных значений, и в начале выполнения теста эти значения находятся в диапазоне от 0 до 99. Но мы выпол- няем тест несколько раз, каждый раз пересоздавая таблицу tin изменяя наимень- шее/наиболыпее значения таблицы tl. joinl, делая каждый раз сдвиг значения t2. joi nl таким образом, что мы можем увидеть тесты, в которых значение tl. j oi nl находится в диапазоне от -100 до -1 или от 100 до 199. В табл. 10.1 показа- но влияние выполнения нескольких тестов в версиях 9i и 10g на кардиналь- ность. Вспомните, что в столбце t2. joinl всегда содержатся значения от 0 до 99.
Расширения и аномалии 313 Таблица 10.1. Проблемы с кардинальностью соединений Наименьшее значение tl .joint Наибольшее значение tl Joint Рассчитанная кардинальность Реальное количество записей -100 -1 1 0 -50 49 1 000 000 486 318 -25 74 1 000 000 737 270 0 99 1 000 000 999 920 25 124 1 000 000 758 631 50 149 1 000 000 513 404 75 174 1 000 000 262 170 99 198 1 000 000 10 560 100 199 1 0 Посмотрите на проблему: до точки, в которой таблицы перестают пересе- каться вообще, кардинальность рассчитывается из предположения, что происхо- дит пересечение на 100 %, и это значение не изменяется. (На самом деле в версии 8i дела обстоят еще хуже: в этой версии оптимизатор даже не замечает, что пересечения нет вообще, и продолжает показывать кардинальность, равную 1 000 000.) Если вы хотите увидеть графическое изображение того, что проис- ходит, то на рис. 10.1 показаны состояния 0, 50 и 100 двух таблиц — визуально хорошо видно, что пересечение становится все меньше по мере того, как две таб- лицы смещаются друг относительно друга, но результаты расчетов при этом не изменяются. Если вы выполняете соединение двух таблиц, ожидая, что записи будут исклю- чены из рассмотрения, потому что записи из одной таблицы не существуют в дру- гой таблице, то оптимизатор может сгенерировать неподходящий план выполне- ния, потому что правила его расчетов не соответствуют вашему знанию данных. Гистограммы Я был не совсем честен насчет пересечений, потому что иногда оптимизатор ис- пользует другие расчеты для определения селективности соединения, и эта аль- тернатива, похоже, учитывает влияние пересекающихся диапазонов значений.
314 Глава 10. Кардинальность соединения Посмотрите на результаты, показанные в табл. 10.2, полученные опять же в вер- сиях 9i/10g и основанные на тех же данных, которые использовались в преды- дущем разделе, но с наличием тщательно подобранной гистограммы, построен- ной на двух столбцах с каждой стороны соединения. Таблица 10.2. Гистограммы помогают получить более верную кардинальность соединений Наименьшее значение tl.jolnl Наибольшее значение tl Joinl Рассчитанная кардинальность Реальное количество записей -100 -1 1 000 000 0 -50 49 1 000 000 486 318 -25 74 1 000 000 737 270 -22 79 622 025 767 437 0 99 1 007 560 999 920 25 124 767 036 758 631 50 149 538 364 513 404 75 174 286 825 262 170 99 198 1 000 000 (10 615) 10 560 100 199 1 000 000 0 При наличии гистограмм я получил приемлемую, хотя и не идеальную, точность на большом диапазоне пересечений. При наличии изначально вы- бранных мною гистограмм рассчитанная кардинальность возвращалась к зна- чению 1 000 000, когда наименьшее значение в таблице tl went становилась ниже -22 или выше 77. Обратите внимание, что в записи 99 я вывел в скобках кардинальность соеди- нения, равную 10 615. Когда я изменил определения гистограмм, оптимизатор получал точные значения в диапазоне значений столбца tl. joinl между -99 и +99, а затем возвращался к рассчитанной кардинальности, равной 1 000 000, когда значение tl. joinl достигало ±100. Гистограммы полезны, хотя из-за них и появляется новая проблема, когда наборы данных вообще не пересекаются. Так что же особенного в моем финальном выборе гистограмм и почему один набор определений гистограмм дает лучшие результаты, чем другой? В конце концов, учитывая, что я генерировал мои данные с помощью процедуры dbms_ random, value (), в них не должно быть экстремальных значений. Ответ состоит в том, что один набор гистограмм состоял из 85 групп, а дру- гой — из номинальных 254 групп. ВЫБОР ПРАВИЛЬНОГО КОЛИЧЕСТВА ГРУПП Вспомните, что максимальное (на данный момент) количество групп (buckets), которое вы можете указать при создании гистограммы, равно 254. Гистограмма становится частотной гистограммой (frequency histogram), когда количество групп превышает количество уникальных значений. Если гистограмма на столбце будет полезна, вы можете выбрать максимальное количество групп в качестве лучшего значения в первом приближении — стоимость дополнительного количества групп (значение по умолчанию равно 75) не является существенной по сравнению с потенциальным преимуществом, которое вы можете получить от дополнительной точности.
Расширения и аномалии 315 Так как у меня только 100 уникальных значений в столбцах, то гистограммы с 254 группами на самом деле становятся высокоточными частотными гисто- граммами со 100 конечными точками (endpoints) и точной картиной текущих данных — поэтому у оптимизатора была хорошая возможность их использо- вать, даже в соединении. Гистограммы с 85 группами были более распространенными сбалансирован- ными по высоте гистограммами (height balanced histograms), и одна из них (на самом деле одна на стороне таблицы t2 соединения) действительно показала наличие часто встречающегося значения (popular value). Похоже, что если: о вы работаете в версии 9i или 10g, О и у вас есть гистограммы на обоих концах соединения с проверкой на равен- ство, о и по крайней мере одна гистограмма является частотной гистограммой или показывает наличие часто встречающегося значения, то у оптимизатора есть метод (каким-то образом сравнивающий данные гисто- граммы для оценки количества записей уникальных значений в каждой таб- лице пересечения), позволяющий определять соединения, в которых происхо- дит только частичное пересечение диапазонов значений в столбцах, по которым выполняется соединение. Но не рассматривайте это как указание строить гис- тограммы везде где только можно. Однако полезно знать, что вы можете прове- рить их влияние, когда найдете важные участки SQL-кода, в которых карди- нальность соединения рассчитывается абсолютно неправильно. ЧАСТОТНЫЕ ГИСТОГРАММЫ И DBMS_STATS Существует проблема при получении частотной гистограммы из пакета dbms_state. У меня есть при- меры наборов данных, в которых только 100 уникальных значений, но SQL-код, использованный Oracle в версиях 91 и 10д (до версии 10.2) для генерации гистограммы, не мог построить частотную гистограмму, пока я не указал 134 группы в базовом тесте. Необходимо рассмотреть пару побочных эффектов. Во-первых, в этой ситуации в версии 8i также используется преимущество Гистограмм, но используемые расчеты должны быть другими, потому что точ- ность результатов гораздо хуже, если только гистограмма не является идеаль- ной гистограммой частотного распределения. Например, в табл. 10.3 показаны результаты, которые вы можете получить в версии 8i при использовании тех же гистограмм из 85 групп, которые мы использовали ранее в версиях 9i и 10g. Таблица 10.3. Гистограммы в версии 8i приводят к другим расчетам при выполнении соединения Наименьшее Значение tl.joinl Наибольшее Рассчитанная Реальное количество значение tl Joint кардинальность записей -100 -1 1 000 000 0 -50 49 147 201 486 318 -25 74 350 499 737 270 ~ю 89 497 618 895 925 . продолжение S
316 Глава 10. Кардинальность соединения Таблица 10.3 (продолжение) Наименьшее значение tl Joinl Наибольшее значение tl Joinl Рассчитанная кардинальность Реальное количество записей 0 99 616 242 999 920 25 124 447 791 758 631 50 149 328 109 513 404 75 174 167 392 262 170 99 198 41 034 10 560 100 199 1 000 000 0 Другой проблемой является поиск определения гистограммы, которая будет работать, если вы не сможете получить гистограмму частотного распределения. В основном это сводится к выбору правильного количества групп, чтобы сде- лать часто встречающееся значение видимым, если такое значение существует. Похоже, оптимизатор ищет часто встречающиеся значения, сравнивая количе- ство групп, указанных при создании гистограммы, с количеством хранящихся конечных точек. Если вы взглянете на представление user_tab_hi stograms, то увидите, что максимальное значение столбца endpoint_number (предполагая, что вы не переключились со сбалансированной по высоте гистограммы на гис- тограмму с частотным распределением) равно количеству хранящихся записей минус единица, если не существует часто встречающихся значений. Чтобы показать, насколько непростым является поиск верного количества групп в гистограмме, посмотрите на табл. 10.4, в которой содержатся данные, использованные для тестов в join_card_06.sql. В таблице выведены количества групп и показано, позволяет ли это количество групп найти часто встречаю- щиеся значения — вспомните, что в этом тесте я только изменяю количество групп в гистограмме, не изменяя сами данные. Последний столбец в таблице показывает влияние, которое оказывает выбор количества групп на наш тесто- вый запрос, когда пересечение диапазонов значений наших двух таблиц равно 50 %. Результаты на самом деле взяты из тестов, выполненных в версии 8i, но те же результаты (с несколько меньшей разницей) также были получены и при выполнении тестов в версиях 9i и 10g. Таблица 10.4. Получение правильного количества групп зависит от везения Количество Найдены часто Рассчитанная кардинальность при 50 % пересечении диапазонов значений групп встречающиеся значения 76 Да 367 218 77 Нет 1 000 000 78 Да 348 960 79 Да 355 752 80 Нет 1 000 000 81 Да 346 040 82 Нет 1 000 000 83 Да 336 838
Расширения и аномалии 317 Проблема зависимости от гистограмм для получения верных значений кар- динальности соединения заключается в следующем: вы не можете заново гене- рировать гистограммы, когда данные изменились настолько, что все часто встре- чающиеся значения исчезли для выбранного вами количества столбцов. С дру- гой стороны, вы не можете оставить гистограммы неизмененными, если данные меняются таким образом, что наибольшее или наименьшее значения начинают сдвигаться, так как именно процент пересечения диапазонов значений вы и пы- таетесь определить с помощью гистограммы. ПРИМЕЧАНИЕ Интересно, что оптимизатор может использовать density из user_tab_columns вместо 1/numjdlstinct при применении предикатов фильтрации, но, похоже, не делает этого при применении предикатов соединения (по крайней мере, не напрямую; возможно, что селективность соединения получается из density каким-то еще не известным мне способом). Эту проблему трудно решить — особенно это касается знания данных, опре- деления нескольких критических случаев и применения специального механиз- ма к каждому из этих случаев. Переходное замкнутое выражение Не важно, сколько вы знаете, — постоянно появляются новые примеры, кото- рые требуют дополнительного исследования. Что произойдет, если включить в ваш запрос дополнительный предикат по столбцам соединения, например, как показано в следующем примере? select tl.vl, t2.vl from tl, t2 where tl.joinl = 20 -- 30 уникальных значений and t2.joinl = tl.joinl -- 40/30 уникальных значений and t2.join2 = tl.join2 -- 40/50 уникальных значений I Набор данных используется из join_card_04.sql, но сам тестовый сценарий находится в файле join_card_07.sql в онлайн-хранилище кода. В комментариях указано количество уникальных записей в каждом столбце, в порядке, в кото- ром имена столбцов появляются в строке. Исходный план выполнения (то есть без предиката с константой tl.joinl® = 20) выглядел следующим образом: План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=60 Card=50000 Bytes=1700000) 1 0 HASH JOIN (Cost«60 Card“50000 Bytes»1700000) 2 1 TABLE ACCESS (FULL) OF 'Tl' (Cost»28 Card-10000 Bytes=170000) 3 1 TABLE ACCESS (FULL) OF ’T2’ (Cost=28 Card-10000 Bytes=170000)
318 Глава 10. Кардинальность соединения При наличии дополнительного предиката план выглядит следующим обра- зом: План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=57 Card=1667 Bytes=56678) 1 0 HASH JOIN (Cost=57 Card=1667 Bytes=56678) 2 1 TABLE ACCESS (FULL) OF 'T2' (Cost=28 Card=250 Bytes=4250) 3 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=28 Card=333 Bytes=5661) Как оптимизатор получил такую большую разницу между значениями кар- динальности всего плана? (Кстати, они являются верными значениями.) Это обнаруживается, если посмотреть на более детальный план выполнения (из па- кета dbms_xplan, например). 1 Id | Operation | Name | | Rows | Bytes | I Cost | 1 0 | SELECT STATEMENT 1 1 | 1667 | 56678 | 1 57 | 1* 1 | HASH JOIN 1 1 | 1667 | 56678 | 1 57 | 1* 2 | TABLE ACCESS FULL 1 T2 | 1 250 | 4250 | 1 28 | | * 3 | TABLE ACCESS FULL 1 Tl | 1 333 | 5661 | 1 28 | Predicate Information (identified by operation id): 1 - access("T2"."JOIN2"="T1"."J0IN2") 2 - filter("T2"."JOINl"=20) 3 - filter("Tl"."JOINl"=20) Обратите особое внимание на вторую строку секции Predicate Infor- mation. В ней находится предикат t2.joinl = 20..., но в нашем исходном SQL-коде его нет, оптимизатор получил его с помощью механизма, называемо- го переходным замкнутым выражением (transitive closure'). Также обратите вни- мание, что предиката t2. joinl = tl.joinl больше нет; при применении пере- ходного замкнутого выражения этот предикат стал лишним и был удален. Переходное замкнутое выражение означает, что оптимизатор подразумевает следующее: если colB = colA и colA = {constant X} то colB = {constant X} В нашем случае t2.joinl = tl.joinl и tl.joinl = 20 поэтому t2.joinl = 20 и условие соединения может быть исключено. Итак, мы изменили наш SQL-код с соединения двух столбцов с одним пре- дикатом фильтрации на соединение с одним столбцом, но двумя предикатами
Расширения и аномалии 319 фильтрации. Поэтому давайте еще раз применим стандартную форму базовой формулы и внесем в нее значения — помните, что в нашем примере оба столбца соединения называются joiп2, а оба столбца фильтрации называются joinl: Селективность соединения = ((num_rows(tl) - num_nulls(tl.cl)) I num_rows(tl)) * ((num_rows(t2) - num_nulls(t2.c2)) I num_rows(t2)) I greater(num_distinct(tl.cl). num_distinct(t2.c2)) Кардинальность соединения = селективность соединения * кардинальность после фильтрации (tl) * кардинальность после фильтрации (t2) Селективность соединения = ((10000 - 0)/ 10000) * ((10000 - 0)/ 10000) / greater(40, 50) = 1/50 -- tl.join2. t2.join2 (num_distinct) Кардинальность соединения = , (1 I 50) * 10000/30 * 10000/40 = -- в tl.joinl 30 значений, в t2.joinl 40 значений 333 * 250/50 = 1,666.66 Вы получите одни и те же результаты и в версии 9i, и в версии 10g. (Вы мо- жете вспомнить, что, впервые рассматривая соединение с двумя столбцами, мы видели, что в версии 10g был произведен специальный контроль ошибок, кото- рый использовал оба значения селективности из одной таблицы. Но в этом слу- чае применение переходного замкнутого выражения удалило один из двух пре- дикатов соединения, поэтому контроль ошибок не был произведен.) Но, как обычно, все можно испортить. Что произойдет, если вы явно добави- те дополнительный лишний предикат таким образом, чтобы выражение where стало выглядеть так: where tl.joinl = 20 -- 30 уникальных значений and t2.joinl = tl.joinl -- 40/30 уникальных значений and t2.jo1n2 = tl.join2 -- 40/50 уникальных значений and t2.joinl = 20 -- 40 уникальных значений Вот что вы получите из dbms_xplan в версии 9i (рассчитанная кардиналь- ность равна 52 в версии 10g): 1 Id 1 | Operation | Name | Rows | Bytes | Cost | 1 0 1 | SELECT STATEMENT 1 1 42 | 1428 | 57 | 1* 1 1 HASH JOIN 1 1 42 | 1428 | 57 | 1* 2 | | TABLE ACCESS FULL 1 T2 | 1 250 | 4250 | 28 | 1* 3 | | TABLE ACCESS FULL 1 Tl | 333 | 5661 | 28 | Predicate Information (identified by operation id);
320 Глава 10. Кардинальность соединения 1 - access("T2"."JOIN1"="T1"."J0IN1" AND "Т2"."JOIN2"="T1"."J0IN2") 2 - fliter("T2"."JOIN1"=20) 3 - filter("Tl"."JOIN1"=20) Очевидно, что произошло что-то не то, потому что значение рассчитанной кардинальности уменьшилось с достаточно точного значения 1667 до абсолют- но неверного 42. Посмотрите на секцию Predicate Information — предикат t2.joinl = = tl.joinl появился снова. Оптимизатору не потребовалось использовать пе- реходное замкнутое выражение для генерации предиката t2.joinl = 20, поэто- му предикат соединения не был удален. В результате оптимизатор дважды учел селективность столбца 12. joinl: один раз в селективности соединения (дос- туп) и второй раз в кардинальности после фильтрации (фильтр). Повторив рас- четы, которые мы выполнили, когда впервые рассматривали селективность двух столбцов, мы получим следующую селективность соединения для этого запроса Селективность соединения = {компонент joinl} * {компонент join!} = (10 000 - 0) / 10 000) * (10 000 - 0) / 10 000) / greater(30, 40) * (10 000 - 0) / 10 000) * (10 000 - 0) / 10 000) / greater(50, 40) = 1/40 * 1/50 = 1/2000 Затем мы применим этот результат к значениям кардинальности после фильтрации, которые получили ранее в этом примере, для получения итогового результата: Кардинальность соединения = (1 / 2000) * 10 000/30 * 10 000/40 = 333 * 250/2000 = 41.625 Результат отличается в версии 10g из-за контроля ошибок при наличии не- скольких столбцов — вместо селективности соединения, равной 1/2000, опти- мизатор выбирает индивидуальные значения селективности из таблицы с мень- шим значением результата (в данном случае это таблица 12, выдающая 1/40 х х 1/40 “ 1/1600). Поэтому в версии 10g мы получаем Кардинальность соединения » (1/1600) * 10 000/30 * 10 000/40 = 333 * 250/2000 = 52.031 Раньше люди находили ошибки оптимизатора, которые позволяли им ис- пользовать различные планы выполнения, повторяя простые предикаты не- сколько раз. В последних версиях (9i и выше) оптимизатор был улучшен для удаления повторяющихся предикатов, но его все еще можно обмануть с помо-
Три таблицы 321 щью уловок в генерации предикатов при применении переходного замкнутого выражения. Предыдущий пример как раз является случаем, когда что-то идет не так. Другим примером является следующий: select tl.vl, t2.vl from tl, t2 where tl.joinl = 20 and t2.joinl = tl.joinl and t2.joinl = tl.joinl -- дублированный предикат соединения and t2.join2 = tl.join2 Обратите внимание на дублированный предикат. Оптимизатор должен был бы его найти и удалить, и, если бы дублирование произошло на столбцах j oi п2, именно это бы и произошло. К сожалению, запрос был изменен перед тем, как было обнаружено дублиро- вание. Оптимизатор удалил один из повторяющихся предикатов с помощью пе- реходного замкнутого выражения, оставив второй — и рассчитал кардиналь- ность как равную 42 (в версии 9i) или 52 (в версии 10g), как и в предыдущем примере. Более того, как мы видели в главе 6, на механизм выполнения переходного замкнутого выражения влияет в версиях 8i и 9i (но не в версии 10g) установка параметра query_rewri te_enabled равным true. При этой установке предика- ты соединения в моем примере не будут удаляться при генерации дополнитель- ных предикатов фильтрации. Выполните еще раз сценарий join_card_07.sql, ус- тановив перед этим query_rewrite_enabled равным true, и кардинальность соединения изменится с верного значения 1667, которое мы получили в начале этого раздела, на неверное 42, полученное в последнем примере. Три таблицы Хотя я сказал в начале этой главы, что Oracle всегда выполняет за один раз со- единение только двух таблиц, следует рассмотреть хотя бы один пример соедине- ния трех таблиц, потому что не очевидно, откуда берутся необходимые значения селективности, когда выполняется соединение третьей таблицы с предыдущей парой таблиц. Мой пример (join_card_08.sql в онлайн-хранилище кода) демонстрирует слож- ный случай, когда происходит выполнение соединения третьей таблицы со столбцами и второй, и первой таблиц. Чтобы расчеты были простыми, начнем с 10 000 записей в каждой таблице. Запрос и его план выполнения выглядят следующим образом: select tl.vl, t2.vl, t3.vl from tl,
3*?*Э Глава 10. Кардинальность соединения t2, t3 where t2.joinl = tl.joinl -- 36/40 уникальных значений and t2.join2 = tl.join2 -- 38/40 уникальных значений and • t3.join2 = t2.join2 -- 37/38 уникальных значений and t3.join3 = t2.join3 -- 39/42 уникальных значений and t3.join4 = tl.join4 -- 41/40 уникальных значений План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=109 Card=9551 Bytes=573060) 1 0 HASH JOIN (Cost=109 Card=9551 Bytes=573060) 2 1 TABLE ACCESS (FULL) OF 'T3' (Cost=28 Card=10000 Bytes=200000) 3 1 HASH JOIN (Cost=62 Card=62500 Bytes=2500000) 4 3 TABLE ACCESS (FULL) OF 'Tl' (Cost=29 Card=10000 Bytes=200000) 5 3 TABLE ACCESS (FULL) OF 'T2' (Cost=28 Card=10000 Bytes=200000) Как бы ни выглядел план выполнения на первый взгляд, порядком соедине- ния для этого запроса является tlkt2kt3. Oracle хэширует таблицу t3 в па- мяти, затем хэширует таблицу tl в памяти, после чего начинает читать таблицу t2. Для каждой записи в таблице t2 Oracle зондирует хэш таблицы tl для по- иска соответствия, поэтому первым соединением является tl ► t2; и если пер- вое зондирование прошло удачно, Oracle зондирует хэш таблицы t3 для поиска соответствия, поэтому вторым соединением является t2 ► t3 — хотя техниче- ски вы должны сказать, что вторым соединением является (tlkt2)kt3. Нашей задачей является определение того, как оптимизатор получает про- межуточную кардинальность, равную 62 500, для хэша tl ► t2, и как он затем получает кардинальность, равную 9551, выполняя соединение таблицы t3 с промежуточным результирующим набором данных. Мы рассмотрим этот про- цесс по шагам. Первое соединение выполняется с таблицей t2 — обратите внимание, что я не использовал предикаты фильтрации и значения null, чтобы сохранить простоту примера, но это только вопрос повторного применения формулы и ис- пользования правильных значений. У нас есть два столбца в соединении таб- лиц tl и t2, поэтому мы используем селективность каждого из этих столбцов по очереди (не забудьте проверить выполнение контроля ошибок по множеству столбцов, если вы работаете в версии 10g): Селективность соединения = {компонент joinl} * {компонент ]‘о1п2} = ((10 000 - 0) / 10 000) * (10 000 - 0) / 10 000)) / greater (36 , 40) * ((10 000 - 0) / 10 000) * (10 000 - 0) / 10 000)) /
Три таблицы 323 greater (38 , 40) = 1/1600 Кардинальность соединения = 1/1600 * 10 000 * 10 000 = 62 500 Теперь у нас есть промежуточная таблица, в которой 62 500 записей, и мы должны выполнить ее соединение с другой таблицей — на этот раз с тремя столбцами соединения. Мы снова применяем формулу: Селективность соединения = {компонент join2} * {компонент join3} * {компонент ]о1п4} = ((10000 - 0) / 10000) * (10000 - 0) / 10000)) / greater( 37, 38) * -- или должно быть greater( 37, 40) ((10000 - 0) / 10000) * (10000 - 0) / 10000)) / greater( 39, 42) * ((10000 - 0) / 10000) * (10000 - 0) / 10000)) I greater( 41, 40) = 1/65 436 Кардинальность соединения = 1/65436 * 62500 * 10000 = 9551 (что и требовалось) По этим расчетам появляется интересный вопрос. При выполнении расчетов селективности соединения я использовал значения num_di stinet, num_nuUs и num_rows из таблицы со столбцами join3 и Joiп2, и принадлежность этих столбцов определяется псевдонимами таблиц в соединении в исходном SQL- коде. (Именно поэтому я добавил вопрос в середину расчетов — должен ли я использовать greater(37, 40) вместо greater( 37, 38)?) Среди прочего возникает вопрос: что делать со столбцами, в которых могут храниться значения null (которые мы рассмотрим в следующем разделе), и как вы можете случайно (или намеренно) изменить рассчитанную кардинальность, выбрав другие предикаты соединения? Посмотрим снова на исходный запрос; он включает следующие предикаты: t2.joinl = tl.joinl and t2.join2 = tl.join2 and t3.join2 = t2.join2 -- но t2.join2 = tl.join2 and t3.join3 = t2.join3 and t3.join4 = tl.join4 Если мы изменим предикат в комментарии, то увидим, что следующий код будет эквивалентным правильным списком предикатов. Вспомните, что пере- ходное замкнутое выражение применяется только тогда, когда есть константа, поэтому мы можем выполнить такое изменение вручную; автоматически оно выполнено не будет:
324 Глава 10. Кардинальность соединения t2.joinl = tl.joinl and t2.join2 = tl.join2 and t3.join3 = t2.join3 and t3.join2 = tl.join2 -- было t3.join2 = t2.join2 and t3.join4 = tl.join4 И в версии 9i план выполнения с этим вариантом соединения будет выгля- деть следующим образом: План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=109 Card=9074 Bytes=544440) 1 0 HASH JOIN (Cost=109 Card=9074 Bytes=544440) 2 1 TABLE ACCESS (FULL) OF 'T3' (Cost=28 Card=10000 Bytes=200000) 3 1 HASH JOIN (Cost=62 Card=62500 Bytes=2500000) 4 3 TABLE ACCESS (FULL) OF 'Tl' (Cost=29 Card=10000 Bytes=200000) 5 3 TABLE ACCESS (FULL) OF 'T2' (Cost=28 Card=10000 Bytes=200000) Обратите особое внимание, что рассчитанная кардинальность итогового ре- зультата снизилась с 9551 до 9074 — просто из-за того, что мы сделали небольшое и абсолютно правильное переупорядочение предикатов (что вы можете сделать просто в «косметических» целях, чтобы ваш код соответствовал определенному стилю кодирования), в результате чего формула изменилась cgreater(37, 38) nagreater(37, 40). Возможно, это ошибка в оптимизаторе. Мы получили про- межуточную кардинальность, равную 62 500 в строке 3, используя селектив- ность столбца ] от п2 из таблицы tl, а затем использовали ту же селективность при выполнении соединения промежуточного результата с таблицей t3 для по- лучения итоговой кардинальности, равной 9074. Но в предыдущей версии кода мы использовали селективность из таблицы 11 для получения промежуточного результирующего набора данных, а затем использовали селективность таблицы t2 для выполнения соединения промежуточного результирующего набора дан- ных с таблицей t3. В идеальной системе оптимизатор, конечно, должен был бы правильно обрабатывать селективность столбца на всех этапах выполнения сложного соединения. Существует еще один момент, который нужно рассмотреть при переходе к версии 10g. В первом варианте очередности предикатов итоговая кардиналь- ность остается равной 9551; но если вы используете второй вариант очередно- сти предикатов, итоговая кардинальность станет равной 9301 (а не 9074). Помните контроль ошибок по множеству столбцов? Взгляните еще раз на второй вариант очередности предикатов. В первом варианте очередности пре- дикатов вы видите два возможных варианта контроля ошибок — t2ctlnt3 с t2. Во втором варианте очередности предикатов вы также видите два возмож- ных варианта контроля ошибок — t2ctlnt3ctl. Контроль ошибок при со- единении t3 с tl используется во втором варианте очередности предикатов, по- этому мы не только используем селективность таблицы tl для столбца join2, но мы также используем селективность таблицы tl для столбца joi п4, в то вре- мя как ранее мы использовали селективность таблицы t3 для столбца ]oin4. Если это вас все-таки не очень смущает, вы всегда можете ухудшить ситуа- цию. Если вы решите включить в запрос и t3. ] oi n2 = t2. ] oi п2, и t3. ] oi n2 =
Значения null 325 = tl.]oi п2, то оптимизатор использует оба этих предиката для расчета итого- вой кардинальности, значение которой в результате уменьшается с нескольких тысяч до 239. Будьте очень осторожны при написании многотабличных соеди- нений. Значения null После всех проблем с соединением трех таблиц, перестановкой предикатов и контролем ошибок из-за чего еще могут возникнуть трудности при выполне- нии соединений? Если вы еще не обновили версию до 9i или 10g, то приготовь- тесь к сюрпризам, связанным со значениями null. Я был не совсем честен, когда ранее рассматривал обработку значений null — я решил проигнорировать одну особенность, которая, в принципе, не важна при рассмотрении только двух таблиц. Но стратегия обработки значений null из- менилась при переходе от версии 8i к версии 9i. На самом деле это изменение было настолько значительным, что формула селективности соединения в сооб- щении 68992.1 на MetaLink больше не верна. Основываясь на тестах, которые я провел, можно сказать, что, когда количе- ство значений null в столбце соединения превышает 5 % от количества записей в таблице, логика расчетов меняется. Сценарий join_card_09.sql из онлайн-хранилища кода создает три таблицы и выполняет их соединение по столбцу, который может хранить значения null. Рассчитанная кардинальность в версии 8i очень сильно отличается от рассчи- танной кардинальности в версиях 9i и 10g. create table tl as select mod(rownum-l,15) nl, Ipad(rownum.10) vl, rpad('x',100) padding from all_objects where rownum <= 150 update tl set nl = null where nl = 0; Таблица tl объявлена co 150 записями и в столбце nl содержатся по десять копий каждого из значений от 1 до 14 и десять копий значения null. Поэтому num_rows = 150, num_distinct = 14, a num_nulls = 10. Создайте таблицы t2 и t3 таким же образом, но вставьте 120 записей в таб- лицу t2 с помощью mod(nl,12) и 100 записей в таблицу t3 с помощью mod(nl, 10). В результате в таблице t2 в столбце nl будут содержаться значе- ния от 1 до И, а в таблице t3 будут содержаться значения от 1 до 9. Итак, все три таблицы содержат по десять записей для каждого значения в столбце n 1, и в столбце nl содержится по десять значений null в каждой таб- лице — и это количество значений null превышает критический порог в 5 %. Теперь мы выполним запрос, которой выполняет соединение всех трех таблиц по столбцу nl, и посмотрим план выполнения в версиях 8i и 9i.
326 Глава 10. Кардинальность соединения Select /*+ ordered */ tl.vl, t2.vl, t3.vl from tl, t2, t3 where t2.nl = tl.nl and t3.nl = t2.nl План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=8 Card=9000 Bytes=378000) 1 0 HASH JOIN (Cost=8 Card=9000 Bytes=378000) 2 1 TABLE ACCESS (FULL) OF 'T3' (Cost=2 Card=140 Bytes=1960) 3 1 HASH JOIN (Cost=5 Card=900 Bytes=25200) 4 3 TABLE ACCESS (FULL) OF 'Tl' (Cost=2 Card=90 Bytes=1260) 5 3 TABLE ACCESS (FULL) OF 'T2' (Cost=2 Card=110 Bytes=1540) План выполнения (версия 8.1.7.4, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=5 Card=8250 Bytes=346500) 1 0 HASH JOIN (Cost=5 Card=8250 Bytes=346500) 2 1 TABLE ACCESS (FULL) OF 'T3' (Cost=l Card=150 Bytes=2100) 3 1 HASH JOIN (Cost=3 Card=900 Bytes=25200) 4 3 TABLE ACCESS (FULL) OF 'Tl' (Cost=l Card=100 Bytes=1400) 5 3 TABLE ACCESS (FULL) OF 'T2' (Cost=l Card=120 Bytes=1680) В версии 9i было рассчитано значение кардинальности, равное 9000, в то время как в версии 8i было получено значение 8250. Но обратите внимание, что в строке 3 обе версии Oracle показывают одно и то же значение кардинальности (900) для соединения таблиц tl и 12, хотя они и показывают различные значе- ния кардинальности для отдельных табличных сканирований в строках 4 и 5. В качестве стартовой точки вы, возможно, захотите определить реальное ко- личество возвращаемых записей. о Существует девять уникальных (не равных null) значений в столбце n 1 таб- лицы tl, по 10 записей на значение, что в результате дает 90 записей. о Для каждой из этих записей, полученных в результате соединения, сущест- вует 10 записей в таблице t2 — поэтому соединение таблицы tic таблицей t2 дает в результате 900 записей. о Для каждой из соединенных записей существует 10 записей в таблице 13, поэтому итоговое соединение включает 9000 записей. И в версии 9i указан правильный результат. Первая информация о том, откуда взялась эта разница, появляется из более детального explai n plan. Используйте explai n plan и dbms_xplan в версии 91, и вы обнаружите, что три дополнительных предиката (фильтрации) появились из следующего: tl.nl is not null t2.nl is not null t3.nl is not null
Значения null 327 Эти предикаты появились потому, что они верны — в конце концов, если tl.nl = tl.п2, то ни в одном из столбцов не может находиться значение null. Это влияет на расчеты потому, что если эти предикаты появляются как преди- каты фильтрации, то вам нужно изменить часть формулы расчета селективно- сти, которая обрабатывает значения null: коэффициент (num_rows - num_ nulls) / num_rows. Поэтому расчеты в старом стиле версии 8i выглядят следующим образом: Соединение таблицы Т1 с таблицей Т2: таблицы, в которых 100 и 120 записей, соответственно: Селективность соединения = (100 - 10) / 100) * (120 - 10) / 120) / greater(9, 11) = 0.075 Кардинальность соединения = 0.075 * 100 * 120 = 900 Соединение промежуточного набора данных с таблицей ТЗ: таблица, в которой 150 записей: Селективность соединения = (120 - 10) / 120) * -- стоимостной оптимизатор использует значения таблицы t2 с одной стороны (150 - 10) / 150) / -- соединения и значения таблицы t3 с другой стороны соединения greater(ll, 14) = 0.0611111 Кардинальность соединения = 0.061111 * 900 * 150 = 8250 Но расчеты в новом стиле версии 9i выглядят следующим образом: Соединение таблицы Т1 с таблицей Т2: таблицы, в которых 100 и 120 записей, соответственно: Селективность соединения = 1 / greater(9, 11) = 0.09090909 Кардинальность соединения = 0.09090909 * 90 * 110 = 900 Соединение промежуточного набора данных с таблицей ТЗ: таблица, в которой 150 записей: Селективность соединения = 1 / greater(ll, 14) = 0.0714285 Кардинальность соединения = 0.0714285 * 900 * 140 = 9000 (что и требовалось)
328 Глава 10. Кардинальность соединения Обратите внимание, как в версии 9i учитывается влияние предикатов is not null в строке кардинальности соединения в виде кардинальности после фильт- рации каждой отдельной таблицы. Это дает нам более правильное значение по сравнению со стратегией версии 8i, которая учитывает в расчетах количество значений null в столбце nl дважды: первый раз в первом соединении и затем еще раз во втором соединении, выдавая в результате слишком низкое итоговое значение кардинальности. Конечно, при обновлении версии с 8i до 9i запросы, включающие соедине- ние множества таблиц по столбцам, содержащим значения null, могут внезап- но изменить их планы выполнения, потому что выросла рассчитанная карди- нальность. Если рассчитанное значение кардинальности увеличилось на одном шаге выполнения запроса, то оптимизатор может решить, что следующим ша- гом должно стать соединение хэширования или слияния вместо соединения с использованием вложенных циклов. Я не знаю, почему эта новая функциональность применяется только тогда, когда количество значений null превышает 5 % от количества записей, так как кажется разумным применять это всегда. Но, возможно, это ограничение было введено, чтобы уменьшить количество запросов, у которых может измениться план выполнения при обновлении версии. Проблемы реализации Есть множество способов плохо реализовать системы на базе Oracle, поэтому общим правилом является следующее: то, что скрывает полезную информацию от оптимизатора, — плохо. Одной из простых и очень популярных стратегий плохой реализации является хранение всех справочных данных в одной табли- це со столбцом type. Результат может быть катастрофическим для работы оп- тимизатора. Практически во всех случаях оптимизатор рассчитает абсолютно неверные значения кардинальности для большинства простых соединений с этой справочной таблицей. Например (см. сценарий type_demo.sql в онлайн- хранилище кода): create table tl as with generator as ( select --+ materialize rownum id from all_objects where rownum <= 3000 ) select trunc(dbms_random.value(0,20)) classl_code, trunc(dbms_random.value(0,25)) class2_code, rownum id, Ipad(rownum,10,'0') small_vc from generator vl, generator v2 where
Проблемы реализации 329 rownum <= 500000 Мы создаем таблицу, в которой 500 000 записей и для которой еще нужны две справочные таблицы, чтобы превратить бессмысленные коды в описания. Я ограничил этот тестовый пример только двумя наборами справочных данных с одинаковым количеством записей в этих наборах, чтобы избежать некоторых странностей, о которых я расскажу в следующем разделе, и чтобы расчеты были прозрачными. Рассмотрим следующий запрос: select tl.small_vc, typel.description from tl, typel where tl.id between 1000 and 1999 and typel.id = tl.classl_code and typel.type = 'CURRENCY' and typel.description = 'GBP' Все, что делает этот запрос, — выбирает часть записей из моей большой таб- лицы и выполняет их соединение со справочной таблицей для превращения кода в описание для того, чтобы извлечь данные с этим описанием. Это типич- ный пример того, как пользовательский запрос использует таблицу типов. (SQL- код этого тестового примера в онлайн-хранилище кода не использует констан- ты 'CURRENCY' и 'GBP', но я думаю, что пара значений со знакомыми кодами поможет лучше понять этот пример.) Так как же должен выглядеть план выполнения? Это зависит от того, что вы сделали с вашими справочными таблицами. Ниже показан первый вариант справочной таблицы — в таблице хранится только информация по валютам (или ' CLASSI', как это на самом деле было указано в примере): create table typel as select rownum-1 id, 'CLASSI' type, lpad(rownum-l,10,'0') description from all_objects where rownum <= 20 На основе этого определения можно сказать, что в запросе мы обращаемся к 1000 записей из базовой таблицы и что ограничение в одно описание из 20 (rownum <= 20) в справочной таблице дает нам 50 записей в итоговом наборе данных. Ниже показан план выполнения: План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_R0W5 (Cost=287 Card=50 Bytes=1950) 1 0 HASH JOIN (Cost=287 Card=50 Bytes=1950) 2 1 TABLE ACCESS (FULL) OF 'TYPE1' (Cost=2 Card=l Bytes=20) 3 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=284 Card=1001 Bytes=19019) Конечно, рассчитанная кардинальность соединения равна 50 записям. Мы можем проверить формулы, показанные в их самой простой форме, когда от- сутствуют значения null:
330 Глава 10. Кардинальность соединения Селективность соединения = 1 / greater(20, 20) = 1/20 Кардинальность соединения = 1/20 * (20/20 * (500 000 * 1001/500 000)) = 50 Теперь давайте создадим справочную таблицу, в которой будут храниться два набора данных, и посмотрим, что произойдет, create table type2 as select rownum-1 id, 'CLASSI' type, Ipad(rownum-1,10.'0') description from all_objects where rownum <= 20 union all select rownum-1 id, 'CLASS2' type, lpad(rownum-l,10,'0') description from all_objects where rownum <= 25 update type2 set description = Ipad(rownum-1,10,'0') В справочной таблице у нас находится 45 записей и 45 уникальных описа- ний — но теперь у нас 25 различных значений столбца ID и два значения столб- ца type. Человек может точно определить 20 записей, которые принадлежат типу, используемому для соединения, и понять, что происходит, но оптимиза- тор просто выполняет расчеты. Выполним запрос еще раз (изменив имя спра- вочной таблицы), и план выполнения будет выглядеть следующим образом: План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=287 Card=25 Bytes=975) 1 0 HASH JOIN (Cost=287 Card=25 Bytes=975) 2 1 TABLE ACCESS (FULL) OF 'TYPE2' (Cost=2 Card=l Bytes=20) 3 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=284 Card=1001 Bytes=19019) Значение кардинальности неверно — но не настолько неверно, как я ожидал. В принципе, селективность соединения должна была измениться с 1/20 на 1/25 — что продиктовано тем, что, к сожалению, тип, который нас не интересу- ет, имеет большее количество уникальных значений в столбце ID, чем тип, кото- рый нас интересует. Более того, фильтрация таблицы type2 изменилась с 1/20 на (1/2 х 1/45 = 1/90). Поэтому мы ожидаем увидеть в формулах следующие значения: Селективность соединения = 1 / greater(25, 20) = 1/25 Кардинальность соединения = 1/25 * (45/90 * (500 000 * 1001/500 000)) = 20 Это неправильно, но легко увидеть, откуда появились неверные значения: оптимизатор должен был использовать значение 1/25 в качестве селективно- сти, но, похоже, что на самом деле он использовал значение 1/20 — и мы можем в этом убедиться, проверив трассировку 10053: в этом примере оптимизатор действительно использовал меньшее значение num_di sti net для расчета селек- тивности соединения.
Проблемы реализации 331 Я еще не знаю, на основе каких правил оптимизатор сделал такой выбор, но посмотрите на значение sei в строках кардинальности соединения, которые я извлек из файлов трассировки всех трех версий Oracle. Оптимизатор исполь- зовал значение 1/20, а не 1/25, что приводит к итоговой кардинальности (в вер- сиях 9i и 10g), равной 25. 10g Join Card: 25.03 = outer (0.50) * inner (1001.00) * sei (5.0000e-002) 9i Join cardinality: 25 = outer (1) * inner (1001) * sei (5.0000e-002) [flag=0] 8i Join cardinality: 50 = outer (1) * inner (1002) * sei (5,0000e-002) [flag=0] Очень полезно сравнивать файлы трассировки разных версий — иногда вы можете ясно видеть, как же трудно понять, что происходит. Обратите внима- ние, что в файле трассировки в версии 9i были выведены те же значения расче- тов (с учетом небольшой ошибки при округлении), что и в трассировке версии 8i, но при этом итоговый результат равен 25. Ясно, что версия 9i обрабатывает промежуточные результаты с большей точностью, но при выводе округляет их до ближайшего целого (посмотрите на значение outer(1) в строке версии 9i последнего примера). Использование высокоточных расчетов контролируется скрытым параметром _optimizer_new_join_card_computation с описанием «рассчитывает кардинальность соединения с помощью неокругленных входных значений» и значением по умолчанию, равным true, в версии 91. В файле трас- сировки 10g этот подход получает дальнейшее развитие, и промежуточные ре- зультаты выводятся с точностью до двух знаков после запятой. Наконец, в вер- сии 8i производятся обычные округления и усечения данных, что в данном конкретном случае означает, что в результате получается верный ответ по не- правильной причине. ПРАВИЛА СЕЛЕКТИВНОСТИ Везде в этой книге я говорил, что существуют случаи использования оптимизатором значения l/num_rows, когда значение селективности, полученное из нескольких предикатов, падает ниже критического порога. Это может быть правдой, но, как вы видите из outer(0.5) в фрагменте для вер- сии 10д, иногда оптимизатор использует селективность, которая меньше значения l/num_rows. Возможно, что мое предположение неверно, и оно кажется верным из-за вариаций в стратегии ок- ругления. Возможно, то, что я увидел, является округлением оптимизатором промежуточного набо- ра данных до 1 — из-за чего складывается впечатление, что у рассчитанной селективности порог оказался меньше, чем l/num_rows. Конечно, когда результаты неверны из-за странного распределения данных, например из-за столбца type, мы всегда надеемся, что ситуацию можно испра- вить с помощью генерации гистограмм. Ясно, что проблема возникла из-за того, что у нас два типа данных в одной таблице, поэтому давайте создадим кумуля- тивную частотную гистограмму на столбце type и посмотрим, что произойдет: План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=287 Card=22 Bytes=858) 1 0 HASH JOIN (Cost=287 Card=22 Bytes=858)
332 Глава 10. Кардинальность соединения 2 1 TABLE ACCESS (FULL) OF 'TYPE2' (Cost=2 Card=l Bytes=20) 3 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=284 Card=1001 Bytes=19019) He знаю, поверите ли вы, но результат стал еще хуже. Оптимизатор опреде- лил, что существует неравномерное распределение по столбцу type, и знает, что существует 20 записей с type = 1 CLASSI1 и 25 записей с type = 1CLASS2 1, но он по-прежнему не имеет информации, что нужное нам описание принадле- жит типу 1 CLASSI1 и, таким образом, должно быть обработано как одно из 20, а не одно из 45. (В результате, так как мы соединили несколько наборов данных в один набор данных со столбцом type, мы попали в ловушку столбцов с зависи- мыми данными, которая описана в главе 6 в разделе «Коррелированные столб- цы».) В тестовом примере, который я использовал в этой главе, расчеты отличаются от правильного значения в два раза — но я сгенерировал данные таким образом, что у меня были объединены только два набора справочных данных примерно одинакового размера. Если вы объедините несколько наборов справочных дан- ных и количество записей в этих наборах данных будет сильно различаться, то ошибка в расчетах может стать огромной. Рассмотрим, например, насколько большой может быть ошибка, когда в од- ном наборе справочных данных 3 записи, а в другом — 6000, со средним количе- ством записей на набор данных около двух сотен. Кардинальность простого со- единения легко может отличаться от правильного ответа в 100 или более раз. Если вы обнаружите, что попали в такую ловушку, существует простое (хотя, возможно, и затратное) решение. Пересоздайте таблицу в виде таблицы с секционированным списком (Jist partitioned table) с секционированием по столб- цу type. Если вы это сделаете и если каждый запрос к этой таблице будет вы- полнять сравнение значений столбца type с константами — следите за cursor_ sharing — то вы превратите единую таблицу в отдельные таблицы на каждый набор данных, так как оптимизатор будет использовать статистику уровня сек- ции для выполнения расчетов. Затраты заключаются в покупке лицензии на ис- пользование секционирования. Трудности! Я не могу назвать что-либо ошибкой, если только не знаю, что делает Oracle, и не могу при этом доказать, что его действия являются неправильными. Слиш- ком много людей говорят «Это ошибка», когда на самом деле они имеют в виду «Я не знаю, почему это произошло». В случае стоимостного оптимизатора и кардинальности соединения очень легко получить странные значения в очень простых примерах. К сожалению, я не могу определить, откуда появились эти значения, поэтому я не могу ре- шить, на самом ли деле я вижу ошибку или это просто неожиданный побочный эффект некоторой функциональности кода оптимизатора. Например (см. join_ card_10.sql в онлайн-хранилище кода): create table tl as select trunc(dbms_random.value(0, 100)) filter,
Трудности! 333 trunc(dbms_random.value(0, 30 )) joinl, trunc(dbms_random.value(0, 20 )) join2, lpad(rownum,10) vl, rpad('x',100) padding from all_objects where rownum <= 10000 create table t2 as select trunc(dbms_random.value(0, 100)) filter, trunc(dbms_random.value(0, 4000 )) joinl, trunc(dbms_random.value(0, 50 )) join2, lpad(rownum,10) vl, rpad('x',100) padding from all_objects where rownum <= 10000 Мы собираемся выполнить соединение двух таблиц по двум столбцам с фильтрацией. Обратите внимание, что столбцы фильтрации имеют идентич- ные определения (хотя сами данные генерировались случайным образом). Хотя, что еще более важно, один из столбцов соединения значительно отлича- ется от соответствующего столбца другой таблицы. В столбце tl.joinl содер- жатся только 30 уникальных значений в диапазоне от 0 до 29, а в столбце t2. joinl содержатся номинальные 4000 уникальных значений в диапазоне от 0 до 3999. (На самом деле в этом столбце находилось 3668 уникальных значе- ний из-за случайной генерации данных.) После генерации данных мы выполним два запроса — они оба содержатся в следующем SQL-коде; второй запрос может быть получен с помощью удале- ния комментария перед одним предикатом фильтрации и указания коммента- рия перед другим, select tl.vl, tl.vl from tl, t2 where tl.joinl = tl.joinl and tl.joinl = tl.joinl and tl.filter = 10 -- and t2.fi Iter = 10 Так как два столбца фильтрации идентичны, то с учетом небольших откло- нений вы можете подумать, что оптимизатор выдаст одну и ту же кардиналь- ность, когда столбец фильтрации используется для идентификации 1 % данных соединения. Есть небольшое отклонение от этого правила в предельных случа- ях, и похоже, что Oracle предпринимает попытки исправить это. Ниже показа- ны планы выполнения. План выполнения (версия 9.2.0.6, автотрассировка. Фильтрация по столбцу tl) 0 SELECT STATEMENT Optimizer=CHOOSE (Cost=57 Card=7 Bytes=266)
334 Глава 10. Кардинальность соединения 1 0 HASH JOIN (Cost=57 Card=7 Bytes=266) 2 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=28 Card=100 Bytes=2000) 3 1 TABLE ACCESS (FULL) OF 'T2' (Cost=28 Card=10000 Bytes=180000) План выполнения (версия 9.2.0.6, автотрассировка. Фильтрация по столбцу t2) 0 SELECT STATEMENT Optimizer=CH00SE (Cost=57 Card=500 Bytes=19000) 1 0 HASH JOIN (Cost=57 Card=500 Bytes=19000) 2 1 TABLE ACCESS (FULL) OF 'T2' (Cost=28 Card=100 Bytes=2100) 3 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=28 Card=10000 Bytes=170000) Почему же значения рассчитанной кардинальности настолько различаются? Согласно формулам: Селективность соединения = {компонент joinl} * {компонент join2} = 1 / greater (30, 3, 668) * 1 I greater (20, 50) 0.00000545256 Кардинальность соединения = 0.00000545256 * 10 000 * 100 = 5.4526 Так почему же оптимизатор определил селективность (значение которой мы можем видеть в трассировке 10053) точно равной 5.0000е-004, когда мы приме- няем фильтрацию к таблице t2 (3668 уникальных значений), и равной 7.1744е-006, когда мы применяем фильтрацию к таблице tl (30 уникальных значений)? Как мы видели ранее, одна из проблем появляется потому, что у обеих на- ших тестовых команд имеется предикат фильтрации только на одной стороне соединения — поэтому стандартная формула изменяется, чтобы использовать значения num_distinet с другой стороны соединения. Но затем, если вы измените тест, сгенерировав столбец tl.joinl в виде dbms_random.value(0,30) + 1, то рассчитанная кардинальность теста изме- нится; и снова, если вы измените тест для генерации столбца tl.joinl в виде 50 * dbms_random.value(0,30), то рассчитанная кардинальность теста изме- нится очень сильно. Но вспомните специальные расчеты, описанные в главе 6, когда оптимизатор выдал неожиданные результаты при небольшом количестве уникальных значений в большом диапазоне. В этом тесте с соединением стол- бец соединения tl.joinl содержит 30 уникальных значений и соединяется со столбцом с диапазоном значений, равным 4000. Возможно, странные значения просто являются результатом того же алгоритма, который мы видели ранее, но примененного другим способом. Обычно проблемы появляются как результат трех возможных условий. о Количество уникальных значений в столбце одной таблицы значительно от- личается от количества уникальных значений в соответствующем столбце другой таблицы.
Особенности 335 о Два диапазона значений значительно отличаются друг от друга, о Произведение отдельных значений селективности, используемых в формуле расчета селективности соединения, значительно больше количества записей в таблицах, участвующих в соединении. Конечно, все три условия должны оказывать некоторое влияние на карди- нальность соединения, но я бы предпочел, чтобы они делали это так, чтобы это влияние было интуитивно понятным. Если вы выполните этот пример в версии 10g, вы обнаружите, что произош- ли изменения, и не обязательно в лучшую сторону. Из-за влияния контроля ошибок на множестве столбцов вы получаете одну и ту же рассчитанную кар- динальность вне зависимости от того, выполняется фильтрация на таблице 11 или таблице t2. В обоих случаях селективность соединения стала равной 0,0001 (l/num_rows для таблицы t2), поэтому кардинальность соединения те- перь равна 100. К сожалению, это значение слишком велико и указывает на другую ситуацию, когда вы можете обнаружить значительные изменения в пла- нах выполнения при переходе с одной версии на другую. Особенности Что бы еще ни произошло, можно гарантировать, что в новых участках кода найдутся ошибки оптимизатора (или аномалии, или ограничения). Ниже, на- пример, показана ошибка при обработке индексов с упорядочением значений по убыванию, что приводит к ошибке при расчете кардинальности в версии 9i (эта ошибка исправлена в версии 10g). Код взят из сценария descenth’ngjjug.sql в он- лайн-хранилище кода, create table tl as select mod(rownum,200) nl, mod(rownum,200) n2, rpad(rownum,215) vl from all_objects where rownum <= 3000 create table t2 as select trunc((rownum-l)/15) nl, trunc((rownum-l)/15) n2, rpad(rownum,215) vl from all_objects where rownum <= 3000 create index t2_il on t2(nl /* desc */ ); Сбор статистики с помощью dbms_stats select tl.nl, t2,n2, tl.vl
336 Глава 10. Кардинальность соединения from tl, t2 where tl.n2 = 45 and t2.nl = tl.nl Значение кардинальности, выведенное для этого запроса, зависит от того, используете ли вы нормальный индекс или убираете комментарий в команде 'create index' и используете индекс с упорядочением значений по убыванию. Ниже показаны два плана выполнения: План выполнения (версия 9.2.0.6, автотрассировка - нормальный индекс со структурой бинарного дерева) 0 SELECT STATEMENT Optimizer=CHOOSE (Cost=33 Card=225 Bytes=51750) 1 0 HASH JOIN (Cost=33 Card=225 Bytes=51750) 2 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=16 Card=15 Bytes=3330) 3 1 TABLE ACCESS (FULL) OF ’T2' (Cost=16 Card=3000 Bytes=24000) План выполнения (версия 9.2.0.6, автотрассировка - индекс со структурой бинарного дерева с упорядочением значений по убыванию) 0 SELECT STATEMENT Optimizer=CHOOSE (Cost=33 Card=l Bytes=230) 1 0 HASH JOIN (Cost=33 Card=l Bytes=230) 2 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=16 Card=15 Bytes=3330) 3 1 TABLE ACCESS (FULL) OF 'T2' (Cost=16 Card=3000 Bytes=24000) Обратите внимание, что кардинальность соединения изменилась с 225 до всего лишь 1, потому что изменился индекс. Также обратите особое внимание, что кардинальность изменилась, хотя план выполнения даже не использует ин- декс! (У меня есть еще более странный пример такого типа в сценарии delete_ anomaly.sql в онлайн-хранилище кода, в котором план выполнения изменяется потому, что я создаю битовый индекс на столбце, который находится в списке select запроса — я еще даже не рассматривал, почему так происходит.) Проблема с индексом, в котором значения упорядочены по убыванию, за- ключается в том, что из-за этого индекса оптимизатор сгенерировал неожидан- ный дополнительный предикат, который мы можем увидеть, если переключим- ся с автотрассировки на dbms_xplan: 1 Id 1 | Operation | Name| Rows | Bytes | Cost | 1 0 1 SELECT STATEMENT 1 1 1 1 230 | 33 | 1* 1 1 | HASH JOIN 1 1 1 1 230 | 33 | 1* 2 | | TABLE ACCESS FULL 1 Tl | 15 I 3330 | 16 | 1 3 | TABLE ACCESS FULL 1 T2 | 3000 | 24000 | 16 I Predicate Information (identified by operation id): 1 - access("T2"."N1"="T1"."Nl" AND SYS_OP_DESCEND("T2"."Nl")=SYS_OP_DESCEND("Tl"."Nl")) 2 - fiIter("Tl".”N2"=45)
Альтернативная точка зрения 337 В версии 9i оптимизатор включает предикат sys_op_descend() в расчет се- лективности. В версии 10g оптимизатор определяет (я так предполагаю), что предикат sys_op_descend() представляет собой то же самое, что и предикат t2. nl = tl. п 1, и, таким образом, не учитывает значение этого предиката два- жды. Альтернативная точка зрения Если вы чувствовали себя несколько неуверенно, читая описания расчетов зна- чений кардинальности соединения на предыдущих страницах, то существует еще один способ понять то, что происходит. Этот способ не меняет происходя- щего и не меняет способ вывода результатов; это просто другое представление, которое можно использовать, чтобы было легче понять, как выполняются рас- четы. Рассмотрим запрос из join_card_01.sql: select tl.vl, t2.vl from tl, t2 where tl.filter =1 --25 значений and t2.joinl = tl.joinl -- 50/30 значений and t2.filter =1 --50 значений Примените простые предикаты фильтрации и выполните соединение с де- картовым произведением над результирующими записями. У нас есть 400 запи- сей из таблицы tin 200 записей из таблицы t2, поэтому соединение с декарто- вым произведением возвращает 80 000 записей. Но есть еще один предикат, который применяется, чтобы уменьшить эти 80 000 записей до итогового набора данных — предикат, представляющий усло- вие соединения. Чтобы применить этот предикат, мы взглянем на него с двух различных сторон: t2.joinl = ;неизвестное_значение или :неизвестное_значение = tl.joinl Затем мы просто определим, у какого из этих двух условий большая селек- тивность. И, как вы узнали в главе 3, селективность столбец = -.перемен- ная_связывания равна или density, или 1/ num_distinet. Если у нас множество условий, мы просто применяем правила (которые мы также видели в главе 3) для объединения предикатов, учитывая специальный контроль ошибок, добавленный в версии 10g, и получаем ответ. Я бы хотел поблагодарить Бенуа Дажвиля (Benoit Dageville) из корпорации Oracle за то, что он натолкнул меня на эти идеи во время разговора на Oracle World 2004. Я считаю эти идеи очень полезным способом визуализации резуль- татов применения различных условий.
338 Глава 10. Кардинальность соединения Заключение Если вы все еще работаете в версии 8i, то формула, предоставленная на Meta- Link для расчета значений селективности и кардинальности соединения, вер- ная, хотя нужно заметить, что: О формула расчета селективности просто должна быть повторена по всем столбцам в соединении с использованием N столбцов; О единственный предикат соединения, включающий проверку по неограни- ченному диапазону, использует фиксированное значение селективности, равное 5 %; О единственный предикат соединения, включающий проверку по ограничен- ному (between) диапазону, использует фиксированное значение селектив- ности, равное 0,25 %. Формула расчета селективности соединения в соединении с множеством таблиц всегда использует базовую табличную селективность из таблицы, кото- рая явно указана в предикате (она не обязательно является селективностью, ис- пользованной при получении предыдущего промежуточного результата). Это значит, что вы можете сделать допустимое изменение в тексте, переупорядочив соединения между таблицами, и обнаружить, что рассчитанная кардинальность изменилась. При обновлении версии с 8i на 9i базовая формула, предоставленная на MetaLink, все еще работает во многих случаях, но существует альтернативная стратегия обработки значений null, которая учитывает их в формуле расчета кардинальности соединения, а не в формуле расчета селективности соединения. Это значит, что некоторые запросы (в особенности соединения множества таб- лиц по столбцу со значениями null) могут рассчитывать большие значения кардинальности после обновления версии, а следовательно, их планы выполне- ния могут измениться. Когда вы обновляете версию до 10g, для соединений с множеством столбцов начинают использоваться две новые стратегии — контроль ошибок по множест- ву столбцов (multicolumn sanity check) и контроль ошибок по составному ин- дексу (concatenated index sanity check). Это значит, что кардинальность соеди- нений с множеством столбцов опять же может измениться (увеличиться) при обновлении версии. Все еще встречаются ситуации, когда вы можете изменить кардинальность соединения, добавив дополнительные предикаты, которые технически являют- ся лишними. В некоторых случаях не всегда является очевидным, что предикат на самом деле лишний. С другой стороны, существуют исправления кода в вер- сиях 9i и 10g, которые определяют некоторые из классических лишних преди- катов и игнорируют их. Это значит, что у некоторых запросов может произойти значительное увеличение кардинальности соединения при обновлении с вер- сии 8i до версии 9i или 10g. Соединения на таблицах с небольшим процентом пересечения диапазонов значений могут выполнять более правильные расчеты кардинальности, а зна- чит, и генерировать лучшие планы выполнения, если вы создадите гистограм-
Тестовые сценарии 339 мы для столбцов с самым низким процентом пересечения диапазонов значений с одной стороны или с обеих сторон соединения. Тестовые сценарии Файлы к этой главе, доступные для загрузки, перечислены в табл. 10.5. Таблица 10.5. Тестовые сценарии к главе 10 Сценарий Комментарии join_card_01.sql Простой пример, демонстрирующий использование формул селективности и кардинальности соединения join_card_02.sql Демонстрация того, что формулы продолжают работать со значениями null в столбцах соединения join_card_03.sql Демонстрация того, что формулы продолжают работать со значениями null и в столбцах соединения, и в столбцах фильтрации join_card_01a.sql join_card_04.sql join_card_05.sql join_card_06.sql join_card_07.sql Пример «неправильного» значения num_distinct, использованного в формуле Два предиката соединения с AND между ними Два предиката соединения с OR между ними Влияние изменения наименьшего/наибольшего значений столбца соединения Влияние переходного замкнутого выражения (генерации предикатов) на соединения join_.card_08.sql join_card_09.sql type_demo.sql join_card_10.sql descending_bug.sql Выполнение соединения трех таблиц Изменения при обработке значений null — соединение трех таблиц Результат хранения всех ссылочных данных в одной таблице Что это — ошибка, побочный эффект или три одинаковых особых случая? Ошибка при расчете кардинальности при наличии индексов с упорядочением значений по убыванию delete_anomaly.sql setenv.sql Странный пример изменения кардинальности Установка стандартизированной тестовой среды для SQL*Plus
1*| Соединения I с использованием вложенных циклов После определения кардинальности соединения оптимизатор должен опреде- лить стоимость выполнения этого соединения тремя методами: соединениями с использованием вложенных циклов (nested loop join), соединениями хэширова- ния (hash join) или соединениями слияния (merge join). Как я упоминал в гла- ве 9, соединение типа «звезда» (star join) и трансформация типа «звезда» (star transformation) не являются механизмами соединения. Они являются страте- гиями соединения, поэтому здесь не рассматриваются. В этой и следующих двух главах происходит рассмотрение по очереди всех трех механизмов соеди- нения, начиная с выполняемых механизмом соединения операций и заканчивая расчетом стоимости. Во время рассмотрения механизмов важно помнить, что могут возникать не- которые противоречащие друг другу эффекты. Механизм времени выполнения может не выполнять в точности того, что показывает план выполнения; расче- ты стоимости также могут не совпадать с тем, что показывает план выполне- ния, и расчеты стоимости могут не совпадать с тем, что реально делает меха- низм времени выполнения. Мы начнем рассмотрение с соединения с использованием вложенных цик- лов, потому что его работу легче всего продемонстрировать и расчет его стои- мости известен больше других. Основной механизм Рассмотрим следующий запрос: select tl.cola, t2.colb from table_l tl, table 2 t2
Основной механизм 341 where tl.colx = {value} and t2.idl = tl.idl Если бы мы написали код для эмуляции соединения с использованием вло- женных циклов на этих двух таблицах, то он выглядел бы следующим образом (увы, часто люди пишут код на PL/SQL или Java именно так): for rl in (select rows from table_l where colx = {value}) loop for r2 in (select rows from table_2 that match current row from table_l) loop output values from current row of table_l and current row of table_2 end loop end loop Посмотрев на этот код, вы можете увидеть две циклические конструкции. Внешний цикл проходит по таблице table_l, внутренний — по таблице table_2 (возможно, много раз). Из-за структуры приведенного псевдокода две таблицы в соединении с использованием вложенных циклов обычно называются внеш- ней таблицей (outer table) и внутренней таблицей (inner table). Внешняя табли- ца также обычно называется управляющей (driving table), хотя я никогда не слы- шал, чтобы внутреннюю таблицу называли управляемой (driven table). «ВНЕШНИЕ» И «ВНУТРЕННИЕ» ТАБЛИЦЫ Термины «внешняя» и «внутренняя» для таблиц применимы только к соединениям с использовани- ем вложенных циклов. Применительно к соединениям хэширования эти таблицы называются «соз- дающей» (build) и «зондирующей» (probe), для соединений слияния используются определения «первой» и «второй» таблиц. Однако можно обнаружить, что трассировка 10053 всегда использует термины «внешняя» и «внутренняя», чтобы определить в операции соединения первую и вторую таблицы, соответственно. Я рассмотрю это подробнее в следующих главах. План выполнения для соединения с использованием вложенных циклов с индексом на внутренней таблице может быть представлен в двух различных формах начиная с версии 9i и выше: в первой форме оптимизатор использует индекс на внутренней таблице для сканирования индекса по уникальному ключу (unique scan), а во второй форме для сканирования диапазона по индексу (range scan). Но вторая форма не используется, если для внешней таблицы гарантиро- ванно возвращается одна запись. План выполнения (автотрассировка, версия 9.2.0.6 - сканирование внутренней таблицы по уникальным значениям, традиционная форма) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=324 Card=320 Bytes=11840) 1 0 NESTED LOOPS (Cost=324 Card=320 Bytes=11840) 2 1 TABLE ACCESS (FULL) OF 'DRIVER' (Cost=3 Card=320 Bytes=2560) 3 1 TABLE ACCESS (BY INDEX ROWID) OF 'TARGET' (Cost=2 Card=l Bytes=29) 4 3 INDEX (UNIQUE SCAN) OF 'T_PK' (UNIQUE) (Cost=l Card=l) План выполнения (автотрассировка, версия 9.2.0.6 - сканирование диапазона внутренней таблицы по индексу, новая форма)
342 Глава 11. Соединения с использованием вложенных циклов 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=322 Card=319 Bytes=11803) 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'TARGET' (Cost=2 Card=l Bytes=29) 2 1 NESTED LOOPS (Cost=322 Card=319 Bytes=11803) 3 2 TABLE ACCESS (FULL) OF 'DRIVER' (Cost=3 Card=319 Bytes=2552) 4 2 INDEX (RANGE SCAN) OF 'T_PK' (UNIQUE) (Cost=l Card=l) Вторая форма соединения с использованием вложенных циклов появилась в версии 9i и представляет собой эффективную оптимизацию, известную как упреждающая выборка из таблицы (table prefetching), которая может умень- шить количество логических операций вввода-вывода (то есть уменьшить ко- личество внутренних блокировок (latching) и, возможно, количество физиче- ских операций вввода-вывода) при выполнении соединений с использованием вложенных циклов на таблицах большого размера. Обратите внимание, что вывод стоимости в новом плане не отражает моди- фицированную форму плана (или возможную экономию ресурсов). В плане выполнения ссылка на вторую таблицу (третья строка в плане выполнения с традиционной формой) просто вынесена за пределы вложенного цикла (то есть на первую строку плана выполнения с новой формой). В принципе, можно было бы ожидать, что стоимость и кардинальность нового плана выполнения будут выглядеть следующим образом: План выполнения (возможный, с полной информацией о стоимости предварительной выборки) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=322 Card=319 Bytes=11803) 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'TARGET' (Cost=322 Card=319 Bytes=11803) 2 1 SORT (ROWID LIST) (Cost=??? Card=319 Bytes=???) 3 2 NESTED LOOPS (Cost=??? Card=319 Bytes=???) 4 3 TABLE ACCESS (FULL) OF 'DRIVER' (Cost=3 Card=319 Bytes=2552) 5 3 INDEX (RANGE SCAN) OF 'T_PK' (UNIQUE) (Cost=l Card=l) На самом деле, каким бы ни был план выполнения, механизм времени вы- полнения все равно может использовать метод, показанный в традиционном плане выполнения — на данный момент я видел использование новой функцио- нальности механизмом времени выполнения только в одной очень специфиче- ской ситуации. Примеры, которые я выбрал, чтобы показать вам структуру планов выполне- ния, взяты из теста, который также демонстрирует интересную особенность в поздних версиях 9i — оба плана выполнения получены из одного и того же за- проса (см. сценарий prefetch_test_02.sql в онлайн-хранилище кода), в котором доступ ко второй таблице теоретически должен выполняться с помощью скани- рования индекса по уникальному ключу для получения одной записи по индек- су первичного ключа. Полученный план выполнения на самом деле переключался с одного меха- низма на другой при изменении количества записей в управляющей таблице: как видно в строке с TABLE ACCESS (FULL) OF 'DRIVER', для 320 записей ис- пользовался традиционный план выполнения, а для 319 записей оптимизатор переключился на сканирование диапазона по индексу, чтобы использовать но- вый механизм. Рассмотренный случай является примером специфического и нереалистичного случая, когда новый механизм действительно применяется
Основной механизм 343 во время выполнения, и если вы проверите количество логических операций ввода-вывода и выполнение внутренних блокировок (latch activity) во время выполнения этого примера, то обнаружите явные изменения в использовании ресурсов. Этот сценарий находится в онлайн-хранилище кода, но вам может понадо- биться поэкспериментировать для нахождения точки смены механизма каждый раз, когда вы захотите ее найти. Продемонстрированный тест нельзя в точности повторить, и, вероятно, его выполнение зависит от предыдущей деятельности в вашей системе (сценарий prefetch_test.sql выполняет тесты автоматически, a prefetch_test_Ol.sql показывает, что этот эффект зависит от включенной оцен- ки затрат ресурсов процессора). Когда я изучал файлы трассировки 10053 двух различных планов выполнения, я не смог обнаружить чего-либо, что объяснило бы, почему оптимизатор сменил механизм выполнения соединения. Результаты теста я не смог воспроизвести в версии 10g. Для визуального представления соединения с использованием вложенных циклов существуют два стандартных изображения. Каждое из них имеет свои достоинства и недостатки в качестве инструмента для объяснения механизма работы соединения. На первом изображении (рис. 11.1) записи из одной табли- цы просто соединяются с записями из другой таблицы стрелками, указываю- щими направление деятельности. На черно-белой диаграмме это позволяет лег- ко увидеть соединения между записями одной таблицы и соответствующими записями другой таблицы. Рис. 11.1. Соединение с использованием вложенных циклов Второе изображение (рис. 11.2) включает демонстрацию работы с помощью индекса второй таблицы, так как в случае соединения с использованием вло- женных циклов обычно используется индекс. Из рис. 11.2 становится понятно, почему параметр optimizer_1ndex_ caching может быть подходящим параметром для настройки в типичной OLTP-системе. По умолчанию этот параметр равен 0 (для обратной совмести- мости) и может принимать значения от 0 до 100. Он используется оптимизатором для определения процента тех индексных блоков внутренней (второй) таблицы,
344 Глава 11. Соединения с использованием вложенных циклов которые должны кэшироваться при выполнении соединений с использованием вложенных циклов. Рис. 11.2. Соединение с использованием вложенных циклов с указанием используемого индекса ПАРАМЕТР OPTIMIZER_INDEX_CACHING Параметр optlmlzerjndex_caching используется для настройки расчетов стоимости использования индексных блоков внутренней таблицы в соединениях с использованием вложенных циклов и для расчетов стоимости индексных блоков, использующихся в итераторах значений входного списка. Этот параметр не используется в расчетах стоимости выполнения простых сканирований индекса по уникальному ключу или сканирований диапазона по индексу в одной таблице. Учитывая, что в OLTP-системах часто присутствуют соединения, где «не- сколько записей» из первой таблицы используются для выполнения соедине- ния с другой таблицей, чтобы получить по каждой записи из первой таблицы «несколько записей» из второй таблицы, значение параметра в районе 75 явля- ется разумным начальным предположением (с ударением на слово «предполо- жение») для типичных OLTP-систем. Плотная упаковка содержимого в обыч- ном индексе позволяет предположить, что индексированный доступ может вы- полнить операцию физического чтения во время первой итерации в цикле и что это физическое чтение заполнит буфер, который может повторно использовать- ся в последующих итерациях. Преимущество второго рисунка состоит в том, что он позволяет продемонст- рировать и старый, и новый механизмы для выполнения соединения с исполь- зованием вложенных циклов. Старый механизм находит первую запись во внешней (управляющей) таб- лице, проходит по индексу и обращается ко всем совпадающим записям из внутренней таблицы по очереди; затем он повторяет эту операцию для второй и третьей записей внешней таблицы. Это приводит к выборке отсортированных
Основной механизм 345 записей из второй таблицы (а, а, а, Ь, Ь, Ь, с, с, с), что позволяет с помощью по- следующего выражения order by получить SORT (ORDER BY) NOSORT. Новый механизм находит первую запись во внешней таблице, проходит по индексу и останавливается на листовом блоке, получая только соответствую- щие идентификаторы записей внутренней таблицы; далее он повторяет эту опе- рацию для второй и третьей записей внешней таблицы. Когда все нужные идентификаторы записей найдены, механизм времени выполнения может от- сортировать их и обратиться к внутренней таблице за один проход, выбирая за- писи в том порядке, в котором они встречаются в таблице — в данном случае (а, Ь, Ь, а, а, с, Ь, с, а, с). Такой подход может уменьшить количество логических операций ввода-вы- вода — судя по диаграмме, предполагается, что первые две записи внутренней таблицы могут находиться в одном блоке, а это значит, что они могут быть по- лучены с помощью одной операции, а не двух. Использование этого механизма может даже привести к снижению количества физических операций ввода-вы- вода, так как один блок с первыми двумя записями может быть извлечен из бу- фера за один раз вместо двух обращений к таблице, которые бы потребовались при использовании традиционного механизма. С другой стороны, это означает, что при указании выражения order by может потребоваться сортировка, кото- рая при использовании традиционного механизма не потребовалась бы. Оче- видно, что выбор плана должен быть основан на расчете стоимости; однако на этот выбор также может повлиять мониторинг кэша, выполняемый процессом СКРТ (контрольной точки), так как событие 10299 описано как «Trace prefetch tracking decisions made by CK.PT» («Решения по отслеживанию упреждающих выборок, принимаемые СКРТ»). Изучив и одно, и другое изображение, довольно просто увидеть, что стои- мость выполнения соединения с использованием вложенных циклов можно приблизительно определить, ответив на следующие вопросы. О Какова стоимость получения всех необходимых записей из первой таблицы? О Сколько записей будет в первой таблице? О Какова типичная стоимость нахождения соответствующих записей во вто- рой таблице, учитывая информацию, полученную из текущей записи первой таблицы? ПРИМЕЧАНИЕ Во время рассмотрения соединений с использованием вложенных циклов часто остается незаме- ченной одна деталь: мы выбираем несколько столбцов из первой таблицы перед началом поиска во второй таблице, поэтому мы можем использовать более точные пути доступа ко второй таблице (было время, когда компания Oracle советовала задавать префиксы локальных индексов на секцио- нированной таблице именно по этой причине — но эта операция давно стала автоматизированной частью секционирования). На основе этих трех вопросов мы можем вывести очень простую формулу оценки стоимости соединения с использованием вложенных циклов: Стомость получения данных из первой таблицы + кардинальность результата из первой таблицы * стоимость одного обращения ко второй таблице
346 Глава 11. Соединения с использованием вложенных циклов Например, в следующем небольшом фрагменте трассировки 10053 из версии 9i мы видим расчет стоимости выполнения типичного соединения с использова- нием вложенных циклов: NL Join Outer table: cost: 14847 cdn: 4973 rcz: 35 resp: 14847 Inner table: T2 Access path: tsc Resc: 51 Join: Resc: 268470 Resp: 268470 Эти цифры показывают, что последовательная стоимость (resc) соединения таблицы t2 с предыдущей таблицей (которая может быть промежуточным ре- зультирующим набором данных из предыдущего соединения) равна 268 470. Эта цифра получена следующим образом 14 847 + -- стоимость получения данных из внешней таблицы 4973 * -- количество записей во внешней таблице 51 -- стоимость одного обращения к внутренней таблице (t2) Обратите внимание на два значения resp — они показывают стоимость па- раллельного выполнения, но, так как файл трассировки получен из запроса, в котором степень параллелизма равна единице, эта стоимость совпадает с по- следовательной стоимостью. Рабочий пример Если вернуться на несколько страниц назад и взглянуть на исходный пример (prefetch_test_02.sql в онлайн-хранилище кода), то у вас могут появиться сомне- ния насчет правильности формулы. Ниже еще раз показан традиционный план выполнения: План выполнения (автотрассировка, версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=324 Card=320 Bytes=11840) 1 0 NESTED LOOPS (Cost=324 Card=320 Bytes=11840) 2 1 TABLE ACCESS (FULL) OF 'DRIVER' (Cost=3 Card=320 Bytes=2560) 3 1 TABLE ACCESS (BY INDEX ROWID) OF ’TARGET’ (Cost=2 Card=l Bytes=29) 4 3 INDEX (UNIQUE SCAN) OF ’T_PK' (UNIQUE) (Cost=l Card=l) Строка 2 показывает, что стоимость сканирования управляющей таблицы равна 3 при возвращенных 320 записях. Строка 3 показывает, что стоимость од- ного обращения к целевой таблице равна 2. Из строки 4 можно сделать вывод, что стоимость равна 1 на обращение к индексу плюс 1 на обращение к самой таблице. Применив формулу, мы получим следующее Итоговая стоимость =3 + (320 * 2) = 643 Но полученная стоимость равна 324 — в чем же ошибка? Ответ заключается в комбинации ошибок округления и способа, с помощью которого получаются промежуточные результаты. Для этого примера я включил оценку стоимости использования ресурсов процессора со следующими настройками:
Рабочий пример 347 begin dbms_stats.set_system_stats('MBRC',8); dbms_stats.set_system_stats('MREADTIM',20); dbms_stats.set_system_stats('SREADTIM',10); dbms_stats.set_system_stats('CPUSPEED',500); end; I Эти настройки означают, что процессор работает на частоте 500 МГц (или со скоростью, равной 500 млн операций Oracle в секунду) и что чтение одного блока занимает 10 мс. Другими словами, 5 000 000 операций равны чтению од- ного блока. Если вы посмотрите на фрагмент полного отчета из plan_table (который отдельно показывает стоимость операций ввода-вывода и стоимость использо- вания ресурсов процессора), то вы увидите следующие цифры (я убрал из этого фрагмента многие детали, включая кардинальность): План выполнения (версия 9.2.0.6 - запрос из plan_table) 0 SELECT STATEMENT (all_rows) 10 Cost = 322, CPU = 7010826 1 0 NESTED LOOPS 10 Cost = 322, CPU Cost =7010826 2 1 TABLE ACCESS DRIVER (full) 10 Cost = 2 CPU Cost = 68643 3 1 TABLE ACCESS TARGET (by index rowid) 10 Cost = 1, CPU Cost = 21695 4 3 INDEX UNIQUE T_PK (unique scan) 10 Cost = 0, CPU Cost = 14443 Посмотрите на стоимость операций ввода-вывода, и вы увидите, что она со- ответствует следующей формуле: 322 (строка 1) =2 (строка 2) + 320 * 1 (строка 3) Посмотрите на стоимость использования ресурсов процессора, и вы увиди- те, что она соответствует следующей формуле: 7 010 826 (строка 1) = 68 643 (строка 2) + 320 * 21 695 (строка 3) (на самом деле стоимость использования ресурсов составляет 7 011 043, что дает ошибку в 217 из 7 миллионов) Учитывая, что для оптимизатора 5 000 000 операций равны одной однобло- ковой операции ввода-вывода, можно понять, что выведено в отчете автотрас- сировки. Oracle вывел стоимость как стоимость операций ввода - выво- да + стоимость использования ресурсов процессора / 5 000 000: Стоимость = стоимость операций ввода-вывода + стоимость использования ресурсов процессора 324 = 322 + ceiling(7 010 826/5 000 000) 2 = 1 + ceiling(68 643/5 000 000) 1 = 0 + ceiling(21 695/5 000 000) Таким образом, упрощенный вывод автотрассировки приводит к путанице: он выводит только целочисленные значения; с механизмом округления в вер- сии 9i дела обстоят еще хуже — он всегда округляет до большего целочисленно- го значения (эта проблема менее заметна в версии 10g, в которой округление происходит до ближайшего целочисленного значения, и поэтому верные значе- ния получаются намного чаще).
348 Глава 11. Соединения с использованием вложенных циклов Контроль ошибок В главе 6 я упомянул, что в этой главе среди прочего будет рассмотрен особый случай, в котором стандартная формула индексированного доступа к таблице не используется. Следующий код генерирует несколько необычный набор дан- ных, а затем выполняет к нему простой запрос с соединением двух таблиц по трем столбцам (в онлайн-хранилище кода есть два относящихся к этому приме- ру сценария: в целях сравнения сценарий join_cost_03.sql показывает тот же са- мый пример до изменения, которое приводит к необычному распределению данных, а сценарий join_cost_03a.sql показывает этот пример с уже измененны- ми данными). create table tl as select rpad('x',40) trunc(dbms_random.value(0,25)) trunc(dbms_random.value(0,25)) lpad(rownum,10,'0') rpad('x',200) from all_objects where rownum <= 10000 ind_pad, nl, n2, small_vc, padding -- Критическое изменение данных - делается только в сценарии join_cost_03a.sql update tl set n2 = nl; commi t; create index tl_il on tl(ind_pad,nl,n2) pctfree 91 create table driver as select rownum id, ind_pad, nl, n2 from ( select distinct ind_pad, nl, n2 from tl ) alter table driver add constraint d_pk primary key(id); -- Сбор статистики с помощью dbms_stats select tl. small_vc from tl where
Контроль ошибок 349 tl.1nd_pad = rpad('x',40) and tl.nl = 0 and tl.n2 = 4 select /*+ ordered use_nl(tl) index(tl tl_il) */ tl.small_vc from driver d, tl where d. i d =5 and tl.ind_pad = d.ind_pad and tl.nl = d.nl and tl.n2 = d.n2 В исходном тестовом примере существует 625 различных комбинаций для пар столбцов (n 1, п2) таблицы tl, так что управляющая таблица имеет 625 за- писей, а в специальном тестовом примере, в котором п2 копируется в п 1, управ- ляющая таблица имеет 25 записей. Так как tl содержит 10 000 записей и ком- бинации столбцов (nl, п2) распределены случайным образом, мы можем предположить, что на одну пару значений приходится 16 записей (10 000/625) в исходном тестовом примере и 400 записей (10 000/25) в измененном тестовом примере. Из-за ограничения первичного ключа в управляющей таблице оптимизатор «знает», что первый предикат (d. 1 d = 5) во втором запросе точно вернет толь- ко одну запись, найденную в управляющей таблице. Чтобы повысить уровень распределения из стандартной формулы, сначала мы протестируем запрос индексированного доступа к одной таблице — таблице tl. Согласно стандартной формуле соединения с использованием вложенных циклов, значение стоимости этой операции нужно умножить на кардиналь- ность управляющей таблицы, когда мы переключаемся на соединение двух таб- лиц. План запроса, получающего данные из одной таблицы, выглядит следую- щим образом: План выполнения (автотрассировка, версия 9.2.0.6) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=14 Card=16 Bytes=928) 1 0 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=14 Card=16 Bytes=928) 2 1 INDEX (RANGE SCAN) OF 'T1_I1' (NON-UNIQUE) (Cost=4 Card=16) Хотя мы знаем, что кардинальность, равная 16, на самом деле неверна, похо- же, что план выполнения достаточно верно использует стандартную формулу кардинальности для доступа к одной таблице. Повторим расчет селективности. О Селективность ind_pad = 1. О Селективность nl = 1/25. О Селективность п2 = 1/25. О Общая селективность = 1/625. О Таким образом, кардинальность = 10 000/625 = 16.
350 Глава 11. Соединения с использованием вложенных циклов Таким же образом мы можем проверить значения blevel (2), leaf_blocks (1107) и cluster!ng_factor (6153) в представлении user_indexes и помес- тить их в стандартную формулу расчета стоимости: Стоимость = blevel + ceiling(ceneKTHBHocTb * leaf_blocks) + ceiling(ceneKTHBHocTb * clustering_factor) = 2 + ceiling(1107/625) + ceiling(6153/625) = 2+2+10 =4+10 -- обратите внимание на стоимость использования индекса (строка 2 плана выполнения) = 14 -- общая стоимость обращения к таблице (строка 1 плана выполнения) Но давайте посмотрим, что произойдет при генерации плана выполнения для соединения таблиц. Мы знаем, что получим только одну запись из управ- ляющей таблицы, так что кардинальность результата должна остаться равной 16, как и в первом запросе, а стоимость должна быть следующей: Стоимость получения одной записи из управляющей таблицы + 1 (кардинальность управляющей таблицы) * 14 (стоимость запроса к одной таблице) Вместо этого мы видим: План выполнения (автотрассировка, версии 8.1.7.4, 9.2.0.6 и 10.1.0.4) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=292 Card=16 Bytes=1712) 1 0 NESTED LOOPS (Cost=292 Card=16 Bytes=1712) 2 1 TABLE ACCESS (BY INDEX ROWID) OF 'DRIVER' (Cost=l Card=l Bytes=49) 3 2 INDEX (UNIQUE SCAN) OF 'D_PK' (UNIQUE) 4 1 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=291 Card=16 Bytes=928) 5 4 INDEX (RANGE SCAN) OF 'T1_I1' (NON-UNIQUE) (Cost=45 Card=16) Обратите внимание, что этот план выполнения использует традиционную структуру вложенного цикла даже в версиях 9i и 10g — как вы помните, одним из упомянутых мною ранее условий, при которых используется новая структу- ра, является то, что управляющая таблица должна возвращать более одной за- писи. Кардинальность плана выполнения действительно осталась равна 16, но по- смотрите, что произошло со стоимостью. Стоимость использования индекса возросла с 4 до 45, а инкрементная стоимость обращения к таблице резко воз- росла с 10 до 291 - 45 = 246. Откуда взялись эти цифры? Для ответа на этот вопрос вернемся к представлению user_i ndexes: select blevel, a v g_l ea f _Ы о ck s_p e r_k ey, avg_data_blocks_per_key from user_1ndexes where table_name = 'Tl' and index_name = 'T1_I1'
Контроль ошибок 351 BLEVEL AVG_LEAF_BLOCKS_PER_KEY AVG_DATA_BLOCKS_PEREY 2 44 24 О Стоимость использования индекса равна blevel + avg_leaf_blocks_per_ key - 1. О Возрастающая стоимость обращения к таблице равна avg_data_blocks_ per_key. В специфическом случае соединения, в котором используется равенство все- го индекса, оптимизатор изменяет расчет стоимости, чтобы использовать зара- нее рассчитанные значения, хранящиеся в представлении user_i ndexes (хотя я не могу объяснить, зачем вычитается единица). Как мы видели в главе 6, эти значения основываются на знании реального количества уникальных ключей, а не на умножении значений селективности отдельных столбцов. Итак, теоре- тически это дает нам более реалистичное значение стоимости соединения. К сожалению, расчет кардинальности в этом примере не использовал эту возможность (однако, как мы видели в главе 10, в версии 10g действительно имеется контроль ошибок на основе суммарной кардинальности индекса, что иногда используется), поэтому в данном случае присутствует серьезная разни- ца между стоимостью и кардинальностью. В более сложных случаях из-за этой разницы оптимизатор помещает две таблицы в самое начало плана выполнения, так как он считает, что записей для получения мало и стоимость их получения высока. Так как действительное ко- личество записей гораздо больше того количества, которое рассчитал оптими- затор, последствия такого поведения могут привести во время выполнения к интенсивному использованию ресурсов в следующих операциях плана выпол- нения. Конечно, хотя этот конкретный специфический пример может быть полезен, он имеет интересный, а иногда и негативный, побочный эффект. Вы можете за- хотеть добавить дополнительный столбец к индексу для увеличения точности и уменьшения количества лишних записей, по которым некоторые запросы вы- нуждены проходить в целевой таблице. Также вы можете добавить столбец к индексу, чтобы часто используемый запрос стал запросом, работающим толь- ко с индексом (index-only query), и не обращался к самой таблице (из главы 5 мы знаем, что это может неблагоприятно повлиять на расчет стоимости из-за влияния на clustering_factor). Но если у вас есть другие запросы, которые используют проверку равенства всего индекса, но не используют новый столбец, то они изменят свои расчеты стоимости с метода хранимых результатов (stored result method) на метод про- изведения значений селективности (product of selectivities method). И, как мы только что видели, существуют индексы, для которых это изменение может очень сильно изменить расчет стоимости, что может привести к абсолютно дру- гому плану выполнения для соединений.
352 Глава 11. Соединения с использованием вложенных циклов Заключение Стоимость соединения с использованием вложенных циклов подсчитать легко, но есть и особые случаи, и такие случаи являются еще одной причиной для проведения тщательных исследований перед добавлением новых столбцов в индексы. Тестовые сценарии Файлы для загрузки к этой главе перечислены в табл. 11.1. Таблица 11.1. Тестовые сценарии к главе 11 Сценарий Комментарии prefetch.test_02.sql Демонстрация существования двух разных планов выполнения для соединения с использованием вложенных циклов с доступом по уникальным значениям prefetch_test.sql prefetch_test_Ol.sql Автоматический проход по 999 профилям Исходный тест, без включения оценки стоимости использования ресурсов процессора, для демонстрации того, что планы выполнения не отличаются join_cost_03.sql Демонстрация расчетов стоимости в специфических случаях — исходный тест join_cost_03a.sql Демонстрация расчетов стоимости в специфических случаях — внесено изменение, чтобы продемонстрировать специфический случай setenv.sql Устанавливает стандартизированную среду для SQL*Plus
Соединения ** хэширования Перед тем как начать рассказывать о методе, который использует оптимизатор для оценки стоимости соединения хэширования, я опишу сам механизм. Для этого есть две веские причины. Первая состоит в том, что механизм не очень хо- рошо известен, вторая — вы должны знать механизм, прежде чем надеяться, что поймете, как работает расчет стоимости. Во всех предыдущих главах я отключал оценку стоимости использования ресурсов процессора (CPU costing — параметр системной статистики в версии 9i) и работал с настраиваемым вручную значением workarea_size_policy, а не с автоматическим значением — новой возможностью, добавленной в версии 9i. Часто это является разумной стратегией, потому что не оказывает никакого эф- фекта на работу оптимизатора — до тех пор, пока дело не касается хэширования и сортировки. Таким образом, в этой и следующей главах я рассмотрю четыре различных варианта с включенной и выключенной оценкой стоимости использования ре- сурсов процессора и со значением workarea_size_policy, устанавливаемым автоматически или вручную. Так как соединения хэширования могут выполняться на трех уровнях эф- фективности (отражаемых в vjsysstat как операции рабочей области — опти- мальные, в один проход и в несколько проходов), в результате получается 12 вариантов для рассмотрения. Я не вижу необходимости детально рассматри- вать каждый из 12 вариантов, но рассмотрю ключевые вопросы различных уровней эффективности и прокомментирую важные различия, появляющиеся из-за четырех различных вариантов настроек среды. Возможно, самым важным вопросом, который мы должны рассмотреть, яв- ляется следующая проблема: формула из раздела 9.2 «Performance Guide and Reference» для расчета стоимости, которую я упомянул в главе 1, не содержит явных компонентов ввода-вывода, отражающих перенос данных из памяти на диск соединением хэширования. В отсутствие какой бы то ни было достовер- ной информации нам придется действовать методом проб и ошибок. Другой трудностью, с которой мы столкнемся, является то, что различные версии опти- мизатора могут выдавать совершенно разную стоимость для одного и того же соединения хэширования в одних и тех же условиях.
354 Глава 12. Соединения хэширования Введение При выполнении соединения хэширования мы получаем набор данных и пре- вращаем его в эквивалент хэш-кластера в памяти одной таблицы (предполага- ется, что у нас достаточно памяти), используя внутреннюю хэш-функцию на столбце (столбцах) соединения для генерации хэш-ключа. После этого мы начинаем получать данные из второй таблицы, применяя ту же хэш-функцию к столбцу (столбцам) соединения при чтении каждой записи и проверяя, находим ли мы соответствующую запись в хэш-кластере в памяти. Так как мы используем хэш-функцию на столбце (столбцах) соединения для того, чтобы распределить данные в хэш-кластере случайным образом, понятно, что соединение хэширования может работать только тогда, когда условием со- единения является равенство. Вы можете возразить, что проверка на несущест- вование (not exists) тоже является возможным условием, но в этом случае ус- ловие равенства на самом деле используется для обнаружения того, что равенст- ва нет. Мы будем называть первую таблицу создающей таблицей (build table), так как на ее основе мы «создаем» хэш-кластер в памяти, а вторую — зондирующей таблицей (probe table), так как мы «зондируем» с ней хэш-кластер в памяти. КАКАЯ ИЗ ТАБЛИЦ ЯВЛЯЕТСЯ «ВНЕШНЕЙ»? К сожалению, нет интуитивного понимания терминов «внешняя таблица» и «внутренняя таблица» для соединения хэширования (в отличие от случая традиционного соединения с использованием вложенных циклов). Вероятно, из-за этого руководства уже столько времени описывают механизм соединения хэширо- вания наоборот. Видимо, когда-то один из создателей руководств посчитал, что термин «внутрен- няя таблица» просто означает «первая». И теперь руководства годами утверждают, что внутренняя таблица используется для создания хэш-кластера — хотя во всех других источниках в описании тер- минов «внутренняя» и «внешняя» (то есть в описании вложенных циклов, в файле трассировки 10053 и в подсказке pq_distrlbute) вы обнаружите, что выражение «внутренняя таблица» обознача- ет вторую таблицу в порядке соединения. Давайте создадим тестовый пример, чтобы увидеть различные части этого процесса в действии. Как обычно, в моей демонстрационной среде используют- ся блоки размером 8 Кбайт, локально управляемые табличные пространства, однородные экстенты размером в 1 Мбайт, ручное управление размером сег- мента и (несмотря на мои утверждения в начале этой главы) выключенная сис- темная статистика и ручная настройка hash_area_size (см. сценарий hash_ opt.sql в онлайн-хранилище кода): alter session set workarea_size_poliсу = manual; alter session set hash_area_size = 1048576; create table probe_tab as select 10000 + rownum id, trunc(dbms_random.value(0,5000)) nl, rpad(rownum,20) probe_vc, rpad(’x',500) probe_padding from
Введение 355 all_objects where rownum <= 5000 alter table probe_tab add constraint pb_pk primary key(id); create table build_tab as select rownum id, 10001 + trunc(dbms_random.value(0,5000)) id_probe, rpad(rownum,20) build_vc, rpad('x',500) build_padding from all_objects where rownum <= 5000 alter table build_tab add constraint bu_pk primary key(id); alter table build_tab add constraint bu_fk_pb foreign key (id_probe) references probe_tab; -- Сбор статистики с помощью dbms_stats select bu.build_vc, pb.probe_vc, pb.probe_paddi ng from build_tab bu, probe_tab pb where bu.id between 1 and 500 and pb.id = bu.id_probe План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=106 Card=500 Bytes=278500) 1 0 HASH JOIN (Cost=106 Card=500 Bytes=278500) 2 1 TABLE ACCESS (BY INDEX ROWID) OF 1BUILD_TAB' (Cost=42 Card=500 Bytes=15000) 3 2 INDEX (RANGE SCAN) OF 'BU_PK’ (UNIQUE) (Cost=3 Card=500) 4 1 TABLE ACCESS (FULL) OF ’PR0BE_TAB’ (Cost=60 Card=5000 Bytes=2635000) Я создал тестовые данные для демонстрации нескольких аспектов соедине- ний хэширования. В частности, я создал таблицы таким образом, чтобы пока- зать ошибку в одном широко распространенном (и, возможно, необоснован- ном) утверждении о соединениях хэширования. Соединение хэширования практически всегда описывается как механизм соединения, использующий таб- личные сканирования, но это не обязательное условие. Как вы увидите, мой пример получает один из наборов данных с помощью индексного доступа.
356 Глава 12. Соединения хэширования Другим широко распространенным утверждением является то, что соедине- ние хэширования лучше применять для соединения таблицы небольшого раз- мера с таблицей большого размера (при этом термины «небольшая» и «боль- шая» редко бывают точно определены). Если вы проверите статистику этих двух таблиц, то обнаружите, что они обе одинакового размера. Утверждение о небольшой и большой таблицах на самом деле необходимо рассматривать с точки зрения небольшого и большого наборов данных, которые вы извлекаете из таблиц — и даже в этом случае вы должны точно указать, что имеете в виду, если не хотите, чтобы вас опровергли. Демонстрационный сценарий включает несколько вариантов запроса для рассмотрения этих утверждений. Итак, что же оптимизатор решает сделать с запросом? Очевидно, что одним из вариантов является выполнение соединения с использованием вложенных циклов, которое будет использовать индекс на probe_tab(id) для выборки со- ответствующих записей из probe_tab по мере получения каждой записи из bui ld_tab. Но стоимость использования вложенных циклов оказывается слиш- ком высокой — стоимость при указании соответствующей подсказки равна 542. Также мы можем рассмотреть соединения слияния, но и в этом случае стои- мость оказывается слишком высокой — при указании соответствующей под- сказки она равна 445. Чтобы понять, как Oracle выполняет соединение, представьте себе, что он разделяет SQL-код на два разных компонента: select bu. i d, bu.build_vc, bu.id_probe from build_tab bu where bu.id between 1 and 500 select pb.probe_vc, pb.probe_padding, pb.id from probe_tab pb Эти два компонента выбирают столбцы соединения и все ссылочные столб- цы в списки select, используя все доступные предикаты из основного запроса для ограничения выборки записей. В этом случае такой механизм делает псев- дозапрос к build_tab достаточно селективным, но псевдозапрос к probe_tab вообще не имеет никаких ограничений. Это отсутствие ограничений на выбор- ку из зондирующей таблицы объясняет, почему исключение секций (partition elimination) часто не выполняется, когда второй таблицей в соединении хэши- рования является секционированная таблица. Если только оптимизатор не ре- шает выполнить предварительный запрос (preliminary query, называемый со- кращающим подзапросом — pruning subquery} к создающей таблице или если декомпозированный запрос к зондирующей таблице не включает ссылок на
Введение 357 столбцы, определяющие разбиение на секции, то в этом случае у Oracle нет ни- какой информации о том, как выполнить исключение секций. Обратите внимание, что я включил столбец ID в список select для bui ld_ tab. Это не кажется необходимым, но оптимизатор использует этот столбец в своих расчетах, поэтому я добавил его в запрос. После генерации двух псевдозапросов оптимизатор оценивает количество записей и размер записи (то есть общий объем данных) двух наборов данных. Стоимость и кардинальность двух запросов получаются с помощью стандарт- ных расчетов. Размер в байтах определяется из кардинальности запросов и (обычно) из значений в столбце avg_col_len в user_tab_columns. НАСКОЛЬКО БОЛЬШОЙ ЯВЛЯЕТСЯ «ЗАПИСЬ»? Как правило, размер в байтах в планах выполнения получается из столбцов avg_coljen в user_tab „columns. Устаревшая команда analyze исключает байты длины для столбца, но при вызове dbms_stats.gather_table_stats байты длины учитываются. Так как на выбор создающей таблицы в соединении хэширования влияет размер наборов данных, переключение с analyze на dbmsjstats может (в принципе) изменить порядок соединения хэширования или даже привести к тому, что оп- тимизатор задействует другой механизм соединения. Для такого особого случая, как select * from table, оптимизатор, похоже, использует avg_rowjen из userjables в качестве размера записи, если статистика сгенерирована с помощью dbms_stats, и sum(avg_coljen), если статистика сгенерирована с помощью команды analyze (оптимизатор мо- жет определить, как была сгенерирована статистика, проверив столбец global_stats в user_tables или в user tab columns). Если мы посмотрим на названия и длины столбцов в представлении user_ tab_columns, мы получим результаты, показанные в табл. 12.1. Таблица 12.1. Расчет размера записи Таблица Столбец avg_coljen (dbmsjstats) Итого для таблицы avg_coljan Итого для таблицы (анализ) (dbms_stats) (анализ) Buildjab Build j/c 21 20 Build Jab Id 4 3 Build Jab Id_probe 5 30 4 27 Probe Jab Id 5 4 Probe Jab Probe_padding 501 500 Probe Jab Probej/c 21 527 20 524 Итак, как мы видим в плане выполнения, для build_tab имеется 500 запи- сей по 30 байт общим размером в 15 000 байт и для probe_tab — 5000 записей по 527 байт общим размером в 2 635 000 байт. Если вы внимательнее посмотри- те на план выполнения, то увидите, что размер выходной записи, который ис- пользует оптимизатор, равен 557 байт (получается из 278 500/500). Это означа- ет, что оптимизатор просто добавил длины входных записей, но не учел того, что столбцы соединения были подсчитаны дважды. Размер в байтах изменится на 13 500, 2 620 000 и 275 500 байт, если мы пере- ключимся с пакета dbms_stats на старую команду analyze.
358 Глава 12. Соединения хэширования После определения размера данных оптимизатор назначает меньший набор данных создающей таблицей и создает хэш-таблицу. Так как hash_area_size равен 1 Мбайт, а у нас только 15 000 байт данных, мы знаем, что наша хэш-таб- лица поместится в памяти без необходимости ее переноса на диск. Теперь мы можем определиться с понятием «маленький набор данных» — это набор дан- ных, который полностью помещается в объеме памяти, указанном в hash_area_ si ze, с учетом дополнительных накладных расходов. Оптимальное соединение хэширования Соединения хэширования попадают в категорию операций рабочей области и, начиная с версии 9i, разделяются по категориям оптимальных соединений (optimal), соединений в один проход (onepass) и соединений в несколько прохо- дов (multipass) в vjsysstat, в которой можно найти статистику 'work аге а executions - optimal', 'workarea executions - onepass' и 'workarea exe- cutions - multipass'. Согласно нашим оценкам, объем данных, которые мы хотим получить из соз- дающей таблицы, так мал, что эти данные помещаются в объеме, указанном в h a s h_a г е a_s i z е. Это и есть определение оптимального соединения хэширования. В качестве простого примера оптимального соединения хэширования на рис. 12.1 показано, как происходит соединение. 1) Хэширование создающей таблицы г 2) Хэширование зондирующей таблицы 12.1. Оптимальное соединение хэширования При этом выполняются следующие шаги. 1. Oracle получает первый набор данных и создает массив «хэш-групп» (hash buckets) в памяти. Хэш-группа — это не только стартовая точка для связного
Введение 359 списка записей из создающей таблицы. Запись принадлежит хэш-группе, если номер хэш-группы совпадает с результатом, полученным Oracle при вызове внутренней хэш-функции с переданными этой функции значениями столбца (столбцов) соединения в этой записи. Похоже, количество хэш-групп в хэш-таблице всегда является четной степенью двойки (обычным количест- вом хэш-групп для соединений хэширования небольшого размера являются 1024 или 4096). Хотя вся структура на самом деле является сложным набо- ром фиксированных массивов и связных списков, удобно представлять себе хэш-таблицу в виде квадратного массива ячеек с записями из первой (соз- дающей) таблицы, случайным образом распределенными по этому массиву. 2. Oracle начинает получать данные из второй таблицы, используя для получе- ния записей наиболее подходящий в данном случае механизм доступа, и при- меняет ту же хэш-функцию к значениям столбца (столбцов) соединения для расчета номера соответствующей хэш-группы. Затем Oracle проверяет, есть ли хотя бы одна запись, соответствующая этой хэш-группе — это и называет- ся зондированием хэш-таблицы (probing the hash table). 3. Если в соответствующей хэш-группе нет ни одной записи, Oracle может тут же исключить запись из зондирующей таблицы. Если же в соответствующей хэш-группе есть записи, Oracle выполняет проверку по столбцу (столбцам) соединения на точное соответствие. Вы помните из главы 9, что всегда воз- можна ситуация, когда два разных значения, переданных в хэш-функцию, приводят к одному и тому же результату (хэш-коллизия). Так как записи с разными значениями в столбце (столбцах) соединения могут попасть в одну и ту же хэш-группу соединения, мы должны выполнять точную проверку. Все записи, которые успешно прошли точную проверку, могут сразу быть выведе- ны в результат (или переданы на следующий шаг плана выполнения). Есть большая разница между тем, как Oracle использует память для хэширо- вания и память для сортировки. Как можно понять из комментария о хэш-кол- лизии на шаге 3, соединение хэширования работает наиболее эффективно (с учетом производительности процессора), если в каждой хэш-группе не больше одной записи. Для этого Oracle требуется большое значение hash_area_size при выполнении соединения хэширования, чтобы он мог создать большое количест- во хэш-групп, так как это помогает минимизировать возникновение хэш-коллизий. ОБРАТНЫЙ ВЛОЖЕНННЫЙ ЦИКЛ (THE BACKWARDS NESTED LOOP) Если вы знаете, что такое хэш-кластеры одной таблицы (single-table hash clusters), вы понимаете, что оптимальное соединение хэширования является просто обратным вложенным циклом в динами- чески создаваемом хэш-кластере одной таблицы. Мы создаем хэш-кластер одной таблицы в локаль- ной памяти из записей, полученных из создающей таблицы, а затем для каждой записи из зонди- рующей таблицы проверяем этот хэш-кластер с помощью хэш-ключа. Главным преимуществом оптимального соединения хэширования является то, что создающая таб- лица размещается в локальной памяти, а не является хэш-кластером одной таблицы в буферном кэше. Это значит, что стоимость использования внутренних блокировок (latch), буферов и согласо- ванности чтения (read consistency), которая появляется при доступе к таблице, просто отсутствует при зондировании хэш-таблицы.
360 Глава 12. Соединения хэширования Учитывая, что в нашем случае мы имеем оптимальное соединение хэширо- вания (то есть соединение хэширования в памяти), логично предположить, что итоговая стоимость запроса в плане выполнения, равная 106, складывается из следующих значений. О Стоимость получения данных из создающей таблицы (42). О Стоимость получения данных из зондирующей таблицы (60). О Небольшая дополнительная стоимость выполнения процессором хэширова- ния и сравнения. Как бы то ни было, увеличение стоимости всего на четыре единицы из-за ра- боты процессора кажется несколько подозрительным (особенно потому, что мы не включили оценку стоимости использования ресурсов процессора). И дейст- вительно, этот пример может создать обманчивое впечатление, потому что hash_area_size, равный 1 Мбайт, гораздо больше размера набора данных соз- дающей таблицы, равного 15 Кбайт. Соединение хэширования в один проход (The Onepass Hash Join) Конечно, ситуация может быть более сложной, что мы и увидим, увеличив в таблицах размер столбцов и выбрав увеличившиеся в размере столбцы, чтобы оба набора данных стали такими большими, что перестали помещаться в объеме памяти, указанном в hash_area_size (см. сценарий hash_one.sql в онлайн-хра- нилище кода). Значимые изменения в коде и результирующий план выполне- ния показаны ниже: create table probe_tab as select 10000 + rownum id, trunc(dbms_random.value(0,5000)) nl, rpad(rownum,20) probe_vc, rpad('x',1000) probe_padding from all_objects where rownum <= 10000 create table build_tab as select rownum 10001 + trunc(dbms_random.value(0,5000)) rpad(rownum,20) rpad('x',1000) from all_objects where rownum <= 10000 id, id_probe, build_vc, build_paddi ng -- Сбор статистики с помощью dbms_stats
Введение 361 select bu.bu1ld_vc, bu.build_padding, -- Дополнительная длина в записи создающей таблицы pb.probe_.vc, pb.probe_padding from build__tab bu, probe__tab pb where bu.id between 1 and 2000 -- Дополнительные записи в наборе данных из создающей таблицы and pb.id = bu.id_probe План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=»ALL_ROWS (Cost=1127 Card=2000 Bytes=4114000) 1 0 HASH JOIN (Cost=1127 Card=2000 Bytes=4114000) 2 1 TABLE ACCESS (FULL) OF 'BUILD_TAB' (Cost=2S5 Card=2000 Bytes=2060000) 3 1 TABLE ACCESS (FULL) OF ’PROBE_TAB' (Cost=2SS Card=10000 Bytes»10270000) Мы можем выполнить те же расчеты со значениями из user_tab_columns, чтобы выяснить, что количество байт действительно равно количество записей х х сумма длин столбцов, что дает нам 2 Мбайт данных для небольшого набора данных и 10 Мбайт для большого набора данных. Так как «небольшой» набор данных теперь больше, чем доступная память, Oracle должен применить более сложную стратегию для обработки хэш-табли- цы, и это отражается в стоимости выполнения соединения (1127) — теперь она намного превышает стоимость двух сканирований таблиц (255), которые требо- вались для получения данных в первом случае. Разница в стоимости в основном является результатом оценки оптимизато- ром увеличившегося количества операций ввода-вывода во время выполнения. Так как хэш-таблица слишком большая, чтобы поместиться в памяти, часть этой таблицы должна быть сохранена на диск и впоследствии быть получена обратно с диска. Более того, такая же часть зондирующей таблицы также долж- на быть сохранена на диск, а позже получена обратно с диска. На рис. 12.2 пока- зано, как работает механизм, если хэш-таблица в четыре раза превышает размер доступной памяти. На этой диаграмме мы видим большую часть компонентов, которые Oracle использует при выполнении соединения хэширования. Они также участвуют и в оптимальном соединения хэширования, но я проигнорировал их в исходной диаграмме, чтобы сделать ее проще. Самым важным, что отсутствовало на рис. 12.1, был объем памяти (неболь- шой), зарезервированный для битовой карты, отображающей хэш-таблицу в виде одного бита на одну хэш-группу в хэш-таблице. Когда запись в создающей таб- лице относится к определенной хэш-группе, устанавливается соответствующий бит.
362 Глава 12. Соединения хэширования Битовая карта А" 4а) Прекращение поиска (бит не установлен) \ 4в) Сохранение хэш-секций \ зондирующей таблицы на диск 46) Поиск для । ячейки хэш-секции(й), 1 находящихся в памяти J 2) Сохранение хэш-секций создающей таблицы на диск Рис. 12.2. Соединение хэширования в один проход 1) Хэширование создающей таблицы Остальной объем памяти из указанного в hash_area_s1ze разбивается на блоки (известные как кластеры, clusters, или слоты, slots) с размером, опреде- ленным в параметре _hash_multiblock_io__count (параметр считается уста- ревшим в версии 10g). Похоже, этот параметр всегда имел значение 9 в более ранних версиях Oracle, но теперь он стал гораздо изменчивее и его значение ус- танавливается индивидуально для каждого оптимизируемого запроса. ПРИМЕЧАНИЕ Скрытые параметры _smm_min_auto_io_size и _smm_max_auto_io_size, возможно, должны влиять на количество операций ввода-вывода при хэширований, начиная с версии 91, но, похоже, этого не происходит. На диаграмме видно, что некоторые из кластеров памяти были использова- ны для создания хэш-таблицы, которая была разделена на четыре отдельные хэш-секции (partitions), — это понятие в Oracle обозначает слишком много раз- ных вещей. Количество хэш-секций, похоже, всегда является степенью двойки, и это количество хэш-секций было выбрано, чтобы оставить несколько запас- ных кластеров памяти для выполнения операций ввода-вывода по сохранению хэш-таблицы на диск (нижняя правая хэш-секция разделена на две части, что- бы показать, что она состоит из двух блоков, один из которых в этот момент со- храняется на диск).
Введение 363 Запомните, что хэш-таблица на самом деле является коллекцией связных списков — я изобразил хэш-секции в виде квадратов с одним и тем же количе- ством блоков только ради удобства, чтобы показать «логическую идентич- ность» хэш-групп в хэш-таблице. В любой момент каждая хэш-секция может состоять из разного количества блоков памяти. Вы можете получить больше информации об использовании памяти, устано- вив событие 10104, трассировку соединения хэширования, перед выполнением тестового запроса. Трассировка запроса из сценария hash_one.sql, например, включает следующую информацию, показывающую описанные мною ранее ком- поненты: Number of partitions: 8 Number of slots; 13 Multiblock 10: 9 Block size(KB): 8 Cluster (slot) size(KB): 72 Bit vector memory allocation(KB): 32 Per partition bit vector length(KB): 4 - - 13 кластеров no 9 блоков в каждом - - Количество блоков на кластер - - Размер блока - - Размер кластера в килобайтах: 9 * 8 Кбайт = 72 Кбайт -- Общий объем памяти для битовой карты: 8*4 Кбайт = 32 Кбайт -- Объем памяти, выделяемой на битовую карту каждой Хэш-секции У меня значение hash_area_size равнялось 1 Мбайт и, как видите, Oracle решил использовать восемь хэш-секций для хэш-таблицы и имеет достаточно доступной памяти для 13 кластеров по 72 Кбайт — общим объемом в 936 Кбайт. Оставшаяся часть разделяется на 32 Кбайт для битовой карты (по 4 Кбайт на каждую хэш-секцию) и 40 Кбайт для накладных расходов на управление. Порядок выполнения соединения следующий. 1. Получается первый набор данных и распределяется по хэш-таблице. При ис- пользовании какой-либо хэш-группы устанавливается соответствующий бит в битовой карте. 2. Когда память заканчивается, кластеры сохраняются на диск. При сохране- нии данных на диск используется экономичная стратегия, в рамках которой в памяти сохраняется как можно большее количество полных хэш-секций как можно более длительное время. Когда выборка из создающей таблицы закончится, есть вероятность, что некоторые хэш-секции все еще будут пол- ностью находиться в памяти, в то время как у всех остальных только не- сколько кластеров (но по крайней мере один) останутся в памяти. Может оказаться, что только у одной хэш-секции несколько кластеров остались в па- мяти, а у остальных хэш-секций в памяти осталось только по одному класте- ру. Вне зависимости от результата Oracle имеет детальную информацию о том, где находятся данные из каждой хэш-секции. Более того, если хэш- группа была использована (вне зависимости от того, находятся ли связан- ные с ней данные в памяти или на диске), то соответствующий бит будет установлен в битовой карте, которая всегда полностью находится в памяти. После этого Oracle перестраивает хэш-таблицу, стараясь получить как мож- но больше полных хэш-секций в памяти, сохраняя части других хэш-секций на диск. В рамках такой перестройки хэш-таблицы Oracle резервирует некоторые
364 Глава 12. Соединения хэширования кластеры (как минимум один на хэш-секцию) для обработки зондирующей таблицы. 3. После перестройки хэш-таблицы Oracle начинает получать записи из второ- го набора данных, применяя одну и ту же хэш-функцию к столбцу (столб- цам) соединения каждой записи. Результат хэш-функции используется для проверки соответствующего бита в битовой карте (деталь, которую я опус- тил в описании первого примера). 4. Oracle выполняет одно из следующих возможных действий, в зависимости от результатов проверки. а) Событие: бит не установлен (0). Действие: соответствия не найдено и запись исключается из рассмотре- ния. б) Событие: бит установлен (1) и соответствующая хэш-группа принадле- жит хэш-секции, находящейся в памяти. Действие: проверка хэш-группы — если запись из зондирующей таблицы соответствует записи из создающей таблицы, она выводится в результат; в противном случае она исключается из рассмотрения. в) Событие: бит установлен (1), но соответствующая хэш-группа принадле- жит хэш-секции, находящейся на диске. Действие: откладывание обработки записи из зондирующей таблицы для дальнейшей обработки. Запись может соответствовать некоторой записи из создающей таблицы, находящейся на диске, но в данный момент полу- чение с диска соответствующей хэш-секции из создающей таблицы для проверки записей будет слишком затратным. Отложенные записи из зондирующей таблицы собираются в наборы, кото- рые сравниваются с секциями хэш-таблицы, предварительно сохраненными на диск. Так же, как мы использовали несколько запасных кластеров для сохране- ния хэш-таблицы на диск, мы используем несколько запасных кластеров памя- ти для сбора записей из зондирующей таблицы, которые, возможно, имеют со- ответствующие записи в создающей таблице. Эти записи из зондирующей табли- цы сохраняются на диск по мере заполнения кластеров. Дойдя до конца зондирующей таблицы, мы остаемся с соответствующими парами хэш-секций из создающей таблицы и зондирующей таблицы на диске. В этот момент Oracle имеет полную информацию о том, где находятся дан- ные и сколько записей находится в каждой хэш-секции, поэтому он получает с диска соответствующую пару хэш-секций (одну из создающей таблицы и одну из зондирующей таблицы) и выполняет соединение хэширования между ними. Для дополнительной оптимизации Oracle может поменять роли двух хэш-секций, так как в этот момент он точно знает, сколько данных в каждой хэш-секции, и может использовать меньшую хэш-секцию для создания нового хэша в памяти. Итак, в случае выполнения соединения хэширования с большим объемом данных хэш-таблица может быть сохранена на диск вместе с данными из зонди- рующей таблицы. Стоимость соединения должна учитывать стоимость опера-
Введение 365 ций ввода-вывода сохранения данных на диск и получения их обратно с диска во второй фазе соединения. Этот тип соединения хэширования называется опе- рацией рабочей области в один проход (pnepass workarea execution), потому что набор данных из зондирующей таблицы получается обратно с диска за один раз. Для обоснованного расчета стоимости соединения нам нужно знать допол- нительное количество операций ввода-вывода, в какой форме это делается (то есть типичный размер операций ввода-вывода) и как оптимизатор рассчитыва- ет стоимость каждой операции ввода-вывода. Например, в самом худшем возможном случае Oracle должен будет полу- чить наборы данных из обеих таблиц, сохранить большую их часть на диск, а затем получить их обратно с диска. Поэтому стоимость, скорее всего, должна возрасти в три раза (учитывая, что мы получаем, сохраняем на диск и обратно получаем с диска практически весь объем данных). Конечно, мы должны учесть тот факт, что, так как мы сохраняем на диск и получаем обратно с диска хэш-секции, мы можем использовать более эффек- тивные операции ввода-вывода, чем при первом получении данных. Мы также должны учесть, что мы не собираемся сохранять на диск каждую запись дан- ных, потому что часть записей будет соединена и выведена в результат при пер- вом получении данных из зондирующей таблицы. Если мы проверим план выполнения тестового запроса, то увидим следую- щие размеры даннных: Size (bytes) of build data: 2 060 000 Size (bytes) of probe data: 10 270 000 Так как значение hash_area_si ze в тестовом примере равно 1 Мбайт, а объ- ем данных в создающей таблице примерно равен 2 Мбайт, мы должны сохра- нить на диск примерно половину создающей таблицы (1 Мбайт) и получить этот объем обратно с диска (еще 1 Мбайт). Так как мы собираемся сохранить около половины создающей таблицы на диск, то мы, скорее всего, сохраним на диск примерно половину зондирующей таблицы (5 Мбайт) и получим этот объем обратно с диска (еще 5 Мбайт). Учитывая эти данные, можно надеяться, что дополнительная стоимость со- ставит 12 Мбайт (что равно 1536 блокам) операций ввода-вывода. Конечно, мы видели в файле трассировки 10104, что размер многоблочного ввода-вывода со- ставил 9 блоков, поэтому Oracle должен сохранять на диск и получать обратно с диска по 9 блоков за один раз, что составляет примерно 170 операций вво- да-вывода. К сожалению, итоговая стоимость соединения равна 1127. Так как два таб- личных сканирования, которые выполняются на первом этапе получения дан- ных, имеют стоимость, равную 255 каждое, мы должны объяснить появление дополнительных 1127 - 2 х 255 = 617 единиц стоимости. Эта цифра мало похо- жа на ту, которую мы получили теоретически. Конечно, расчеты, выполняемые оптимизатором, не полностью соответству- ют моему описанию работы соединения хэширования, и так как мое описание в значительной степени базируется на реальном количестве операций ввода-вывода,
366 Глава 12. Соединения хэширования вы можете решить, что расчет стоимости не обязательно показывает то, что ре- ально происходит во время выполнения. Что может запутать еще больше: если вы выполните запрос в версиях 8i и 10g, вы получите значения стоимости 1079 и 1081, соответственно. Разница между этими двумя результатами возникает из-за двух табличных сканирований, а не из-за самого соединения — помните, что, начиная с версии 9i, к стоимости таб- личного сканирования добавляется единица. Что реально происходит в этом случае, так это то, что механизм времени вы- полнения действительно выполнил соединение в один проход, как мы и ожида- ли, а оптимизатор рассчитал, что соединение будет выполнено в два прохода. Более того, механизм времени выполнения в версии 9i использует размер кла- стера, равный девяти блокам, хотя оптимизатор в своих расчетах базируется на спрогнозированном размере кластера, равном восьми блокам (в версиях 8i и 10g оптимизатор в расчетах базируется на спрогнозированном размере в де- вять блоков, что дает меньшую стоимость). Также в модели расчета стоимости подразумевалось, что механизм времени выполнения сохранит на диск всю зондирующую таблицу и получит всю ее об- ратно — потому что в расчетах не было определено, что будет выполнено соеди- нение в один проход, которое имеет некоторые преимущества при выполнении ввода-вывода. Последняя трудность появляется, когда необходимо получить стоимость многоблочных операций чтения и записи, используемых для сохранения на диск хэш-секций и получения их обратно с диска; оптимизатор применяет ту же стратегию для настройки значений многоблокового чтения, что мы видели в главе 2, и мы можем использовать результаты табличных сканирований для расчета стоимости соединения хэширования. Так как оптимизатор прогнозиру- ет размер кластера равным восьми блокам в расчетах версии 9i, он использует значение 6,588 для расчета стоимости. Эта проблема подчеркивает одну из трудностей при определении того, как оптимизатор рассчитывает стоимость соединения хэширования. Мы не знаем, какой размер кластера спрогнозирован оптимизатором — эта цифра не указана в файле трассировки 10053, так что мы не знаем, сколько блоков прогнозирует оптимизатор для одной операции ввода-вывода. Более того, мы не можем гарантировать, что код в механизме времени вы- полнения следует тем же предположениям, которые встроены в оптимизатор — так что если мы попробуем выполнить тонкую настройку объема памяти для получения правильного значения стоимости, то все равно не сможем гаранти- ровать, что механизм времени выполнения будет вести себя именно так, как требуется. Определив все странности и конфликтующие части имеющейся информа- ции, мы наконец можем определить, что значение 617 может быть получено следующим образом: Размер большого набора данных = 1269 блоков - - получается из размеров столбцов и количества записей Размер небольшого набора данных = 255 блоков - - получается из размеров столбцов и количества записей Размер кластера (спрогнозированный) = 8
Введение 367 - - предположительное значение, после события Размер операции ввода-вывода, используемый для расчетов = 6.588 - - настройка, как описано в главе 2 количество проходов по зондирующей таблице = 2 - - как показано в файле трассировки 10053 таким образом стоимость = (количество проходов по зондирующей таблице + 1) * round(1269/6.588) + round(255/6.588) = 3 * 193 + 39 = 579 + 39 = 618 -- что и требовалось (с небольшой разницей из-за ошибки округления) Обратите внимание, что, похоже, оптимизатор учитывает сохранение хэш- секций зондирующей таблицы на диск и получение их обратно с диска — он использует количество проходов по зондирующей таблице + 1 в расчетах — но только один раз учитывает стоимость хэш-таблицы. Я не знаю, почему (также обратите внимание, что я мог выполнить округление и в другом месте, чтобы цифры стали абсолютно точными — но всегда остается место для ошибки при выполнении округления и форматирования вывода в трассировке 10053). Чтобы убедиться, что гипотеза верна и что это не простая удача, мы можем выполнить расчеты для версий 8i и 10g. Со значением hash_multiblock_ i o_count, равным 9, оптимизатор использует в расчетах значение 7,12. Учтите, что мы должны получить итоговую стоимость, равную 1081 (для версии 10g) — это значит, что, когда мы вычтем стоимость табличного сканирования, стои- мость соединения хэширования составит 1081 - 510 = 570 (для версии 8i ре- зультат тот же, кроме того, что мы учитываем несколько меньшую стоимость табличного сканирования: 570 = 1079 - 508). Без сомнения, получается следующее: (количество проходов по зондирующей таблице + 1) * round(1269/7.12) + round(255/7.12) = 3 * 178 + 36 = 570 -- что и требовалось Если вы хотите проверить расчеты более тщательно, выполните hash_one.sql с разными значениями hash_multi block_i o_count и проверьте изменения стои- мости соединения хэширования. Обратите внимание, что параметр становится скрытым в 9i, так что синтаксис для установки значения параметра изменится: alter session set "_hash_multiblock_io_count" = 4: -- синтаксис в 91 alter session set hash_multiblock_io_count = 4; -- синтаксис в 8i Пишите код очень аккуратно при тестировании в версии 9i: синтаксис вер- сии 8i не приведет к ошибке в версии 9i, но новое значение будет просто проиг- норировано. Соединение хэширования в несколько проходов Давайте рассмотрим подробнее самый тяжелый случай, когда доступный объем памяти, указанный в hash_area_size, очень мал (или когда статистика доста-
368 Глава 12. Соединения хэширования точно неточна, чтобы оптимизатор сделал очень плохой выбор количества сек- ций хэш-таблицы). Представьте, что у вас 4 Мбайт данных в создающей таблице, которые долж- ны быть распределены по хэш-таблице, но в hash_a rea_si ze установлено такое значение, что есть только девять свободных блоков (72 Кбайт) для создания хэш-таблицы. Oracle, скорее всего, выберет размер кластера равным единице и создаст хэш-таблицу из четырех секций, по два кластера на хэш-секцию, с одним запас- ным блоком, оставленным для операций ввода-вывода. После заполнения всех блоков в хэш-таблице она будет сохранена на диск. В конце фазы создания хэш-таблицы на диске будет четыре хэш-секции создающей таблицы, каждая размером около 1 Мбайт, и Oracle перестроит хэш-таблицу в памяти, чтобы данные, которые остались в памяти, являлись несколькими блоками из первой хэш-секции создающей таблицы. В этот момент механизм времени выполнения уже знает, что ему нужно со- хранить на диск четыре хэш-секции из зондирующей таблицы. Поэтому он ре- зервирует по меньшей мере четыре блока в памяти, по одному на каждую хэш-секцию зондирующей таблицы, плюс по меньшей мере один запасной блок на получение данных из зондирующей таблицы. Таким образом, хэш-таблица в памяти будет ограничена максимум четырьмя блоками — для обработки хэш- секции размером 1 Мбайт! Начинается проход по зондирующей таблице с получением записей из зон- дирующей таблицы. Как и в случае с соединением хэширования в один проход, Oracle может исключить запись из рассмотрения, если битовая карта не пока- зывает никаких соответствий, проверить запись на соответствие данным в памя- ти (включая запись в результат или исключая из рассмотрения) или отложить обработку записи, если битовая карта показывает возможное соответствие, но нужная хэш-группа находится на диске. Когда первый проход по зондирующей таблице закончен, некоторые записи включаются в результат (те, для которых найдено соответствие в четырех бло- ках создающей таблицы, находящихся в памяти), а остальные включаются в че- тыре хэш-секции зондирующей таблицы, которые сохраняются на диск, по одной хэш-секции на каждую хэш-секцию создающей таблицы, ранее сохраненную на диск. Вот тут-то и начинаются проблемы. Oracle собирается выполнять соедине- ние хэширования между всеми парами хэш-секций создающей таблицы и зон- дирующей таблицы. Но каждая хэш-секция создающей таблицы имеет размер около 1 Мбайт, а памяти достаточно для получения обратно с диска только не- скольких блоков за один раз. Так как нам доступно девять блоков и мы знаем, что мы соединяем хэш-секции, соответствующие друг другу, у нас есть некото- рое пространство для маневра — мы можем использовать один блок для Хэши- рования секции создающей таблицы и восемь блоков для получения данных зондирующей таблицы, или два блока для хэш-секции создающей таблицы и семь блоков для хэш-секции зондирующей таблицы, и т. д. Чем больше бло- ков мы используем для хэширования, тем меньше доступный объем для полу- чения данных из хэш-секции зондирующей таблицы. И наоборот, чем меньше
Введение 369 блоков мы используем для хэширования, тем больше доступный объем для хэш-секции зондирующей таблицы, но тем большее количество раз мы должны повторить процесс. Предположим, что мы используем четыре блока для хэши- рования секции создающей таблицы. Oracle получает с диска четыре блока первой хэш-секции создающей табли- цы и сканирует всю первую хэш-секцию зондирующей таблицы, выполняя со- единение, когда это возможно. После этого Oracle получает с диска следующие четыре блока первой хэш-секции создающей таблицы и опять сканирует всю первую хэш-секцию зондирующей таблицы, выполняя соединение, когда это возможно. Если получение с диска всей первой хэш-секции создающей табли- цы будет выполнено за 32 прохода, то Oracle должен будет просканировать всю первую хэш-секцию зондирующей таблицы 32 раза. СОЕДИНЕНИЯ ХЭШИРОВАНИЯ В НЕСКОЛЬКО ПРОХОДОВ Текущая реализация соединения хэширования имеет слабые стороны, которые проявляются только тогда, когда нельзя выполнить соединение в один проход и приходится получать каждую хэш-сек- цию создающей таблицы в несколько проходов. Каждый раз, когда вы получаете обратно с диска несколько блоков хэш-секции создающей таблицы, вам приходится сканировать всю соответствую- щую хэш-секцию зондирующей таблицы. В идеальном случае можно было бы надеяться, что механизм времени выполнения обнаружит эту проблему в конце первого прохода. В конце концов, к этому моменту он собрал точную информацию об объеме данных в каждой паре хэш-секций (одна из создающей таблицы и одна из зондирующей) и знает, на сколько «подсекций» придется разбить каждую хэш-секцию, чтобы все пары подсекций были обработаны за один проход. В принципе, механизм времени выполнения мог бы использовать «рекурсивное» хэширование, ко- торое можно было бы применять к каждой хэш-секции. Можно было бы получить с диска целую хэш-секцию создающей таблицы и выполнить ее хэширование на разумное количество подсекций (например, равное такой степени двойки, чтобы это число превысило количество проходов, необхо- димое для обработки всей хэш-секции). Такое же разделение на подсекции необходимо было бы сделать и для хэш-секции зондирующей таблицы — но это может быть сделано при соединении с первой хэш-секцией создающей таблицы. При использовании такой стратегии общее количество операций ввода-вывода осталось бы в разумных пределах. Игнорируя некоторые граничные моменты, можно сказать следующее: мы получаем создающую таблицу, сохраняем ее на диск в виде хэш-секций, получаем ее обратно с диска и опять сохраняем на диск в виде подсекций, после чего получаем обратно с диска каждую подсекцию для выполнения соединения. Таким же образом мы получаем зондирующую таблицу, сохраняем ее на диск в виде хэш-секций, получаем ее обратно с диска и опять сохраняем на диск в виде подсекций, после чего получаем обратно с диска каждую подсекцию за один раз. Все равно это выглядит лучше, чем необ- ходимость получения обратно с диска хэш-секций зондирующей таблицы много раз, потому что хэш-секции создающей таблицы слишком велики, чтобы уместиться в памяти (ясно, что при этом от- правной точкой является количество проходов, равное двум, при котором нет необходимости раз- бивать хэш-таблицу на подсекции). Конечно, при этом придется беспокоиться о большей сложности кода и такой возможности, что «ре- курсивное соединение хэширования» начнет применяться к подсекциям, к их подсекциям, и т. д. И при переходе на очередной уровень рекурсии придется тратить память на битовую карту и другие накладные расходы, а поскольку соединение в несколько проходов и так выполняется при нехватке памяти, реализация рекурсивного соединения хэширования может оказаться бессмысленной. Если у вас выполняется соединение хэширования в несколько проходов — а обычно это происходит в редких специфических случаях при обработке больших объемов данных, — вам необходимо рас- смотреть варианты увеличения доступной памяти, изменения механизма соединения или измене- ния кода SQL.
370 Глава 12. Соединения хэширования Затем Oracle переходит ко второй паре хэш-секций и повторяет процесс. Та- кая операция хэширования известна как операция в несколько проходов (multi- pass operation), потому что после сохранения данных на диск они получаются обратно с диска много раз. Если значение hash_area_size слишком мало, то количество операций вво- да-вывода для выполнения соединения хэширования над большим объемом данных может стать очень большим. В моем гипотетическом случае мы можем ожидать превышение стоимости ввода-вывода в 34 раза по сравнению со стои- мостью оптимального соединения, так как мы получаем всю зондирующую таб- лицу, сохраняем ее практически всю на диск и затем получаем все хэш-секции зондирующей таблицы обратно с диска 32 раза. Хотя я и не создал тестовый пример, который полностью соответствует опи- санию соединения хэширования в несколько проходов, сценарий hash_multi.sql в онлайн-хранилище кода достаточно близок к нему. В нем повторяется объяв- ление таблиц из сценария hash_one.sql, но затем в нем устанавливается значе- ние hash_area_size равным 128 Кбайт — при размере создающей таблицы в 2 Мбайт. При этом план выпонения выглядит следующим образом: План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=13531 Card=2000 ' Bytes=4114000) 1 0 HASH JOIN (Cost=13531 Card=2000 Bytes=4114000) 2 1 TABLE ACCESS (FULL) OF 'BUILD_TAB' (Cost=255 Card=2000 Bytes=2060000) 3 1 TABLE ACCESS (FULL) OF 'PROBE_TAB' (Cost=255 Card=10000 Bytes=10270000) Обратите внимание, что стоимость табличных сканирований не изменилась, что и ожидалось, но стоимость соединения хэширования выросла с 1127 для со- единения в один проход до 13 531 для соединения в несколько проходов. В принципе, оптимизатор всегда может определить, сколько требуется хэш- секций для соединения хэширования в один проход. Но если значение hash_ area_size слишком мало, чтобы позволить создать множество хэш-секций — помните, что каждая хэш-секция требует наличия по меньшей мере одного бло- ка в памяти — то оптимизатор определит, что требуется соединение в несколько проходов, и рассчитает, сколько потребуется проходов на хэш-секцию зонди- рующей таблицы. Если количество проходов равно N, то дополнительная стои- мость соединения в несколько проходов будет состоять из стоимости разового сохранения на диск записей с возможным соответствием и получения их обрат- но с диска N раз. В трассировке 10104 можно оценить масштаб проблемы, посмотрев на раз- дел с заголовком Phase 2. Найдя соответствующую информацию в файле трас- сировки после выполнения сценария hash_mul.ti.sql, мы обнаружим следующее: *** HASH JOIN GET FLUSHED PARTITIONS (PHASE 2) *** Getting a pair of flushed partions. BUILD PARTION: nrows:256 size=(33 slots, 264K) PROBE PARTION: nrows:260 size=(34 slots, 272K)
Введение 371 * ** HASH JOIN BUILD HASH TABLE (PHASE 2) *** Number of blocks that may be used to build the hash hable 10 Из этих цифр можно увидеть, что нам необходимы 33 слота, что в данном случае также означает 33 блока, поскольку в начале файла трассировки указа- но, что размер слота (кластера) равен одному блоку. Мы можем сравнить эту цифру с количеством блоков (только 10), которые мы можем использовать для создания хэш-таблицы. (Да, в файле трассировки действительно указано «hash hable» и «PARTION» — файл трассировки не предназначен для конечных поль- зователей, поэтому никто в команде разработки Oracle не спешит исправить эти грамматические ошибки. Есть и более важные вещи, которые надо сделать.) Поэтому мы собираемся разделить каждую хэш-секцию создающей таблицы на четыре секции, получить их с диска по одной и просканировать каждую хэш- секцию зондирующей таблицы четыре раза. Это значит, что мы должны, по меньшей мере, увеличить значение hash_area_si ze в четыре раза (или, в край- нем случае, в 3,3), чтобы избежать соединения хэширования в несколько прохо- дов. Это заключение полностью подтверждается информацией из файла трасси- ровки, в котором Oracle указывает объем выполненной работы и объем остав- шейся. Получив важную информацию о первой паре хэш-секций (одна хэш- секция создающей таблицы и одна хэш-секция зондирующей таблицы), мы ви- дим следующие группы строк: Number of rows left to be iterated over (start of function): 256 Number of rows iterated over this function call: 78 Number of rows left to be iterated over (end of function): 178 Number of rows left to be iterated over (start of function): 178 Number of rows iterated over this function call: 78 Number of rows left to be iterated over (end of function): 100 Number of rows left to be iterated over (start of function): 100 Number of rows iterated over this function call: 78 Number of rows left to be iterated over (end of function): 22 Number of rows left to be iterated over (start of function): 22 Number of rows iterated over this function call: 22 Number of rows left to be Iterated over (end of function): 0 Каждый из этих фрагментов текста сообщает, что мы получили с диска оче- редную небольшую порцию первой хэш-секции создающей таблицы и проска- нировали всю соответствующую хэш-секцию зондирующей таблицы. Как и раньше, мы можем сказать, что стоимость соединения хэширования указывает на тот факт, что нам понадобилось очень много дополнительных опе- раций ввода-вывода и что оптимизатор смог примерно оценить количество этих операций ввода-вывода. Не так просто определить, откуда получились цифры, когда мы пытаемся это сделать уже после выполнения. В этом случае итоговая стоимость составила 13 531. Общая начальная стои- мость табличного сканирования составила 510. Поэтому инкрементная стои- мость соединения составила 13 021. Мы знаем, что пришлось сохранить на диск
372 Глава 12. Соединения хэширования около 2 Мбайт создающей таблицы (то есть большую ее часть) и 10 Мбайт зон- дирующей таблицы (тоже большую часть таблицы). После этого мы должны получить обратно с диска создающую таблицу один раз (хотя мы и получаем ее четырьмя частями, каждая часть получается только один раз) и зондирующую таблицу четыре раза. Поэтому общее количество дополнительных операций ввода-вывода в итоге составляет 2 Мбайт (сохранение на диск) + 2 Мбайт (по- лучение обратно с диска) + 10 Мбайт (сохранение на диск) + 4x10 Мбайт (по- лучение обратно с диска) = 54 Мбайт = 6912 блока. Хотя в этом случае размер «многоблочного» прямого чтения и равен одному блоку, инкрементная стоимость выше полученной цифры почти в два раза — откуда же получилось значение стоимости в 13 021 из общего количества опе- раций ввода-вывода, равного 6912? На самом деле, как и в случае с хэшированием в один проход, оптимизатор получил неверные цифры. Мы видели четыре прохода по каждой хэш-секции зондирующей таблицы в файле трассировки 10104, но расчеты в файле трасси- ровки 10053 показали оценку в 16 проходов (в этом случае мы можем предпо- ложить, что реальный размер кластера, равный 1, совпадает со спрогнозирован- ным размером кластера). Размер большого набора данных = 1269 блоков - - получается из размера столбцов и количества записей Размер небольшого набора данных = 255 блоков - - получается из размера столбцов и количества записей Размер кластера (спрогнозированный) = 1 - - предположение, после события Размер операции ввода-вывода, используемый для расчетов = 1.676 - - настроенное значение, как описано в главе 2 количество проходов по зондирующей таблице = 16 -- как показано в файлах трассировки 10053 Таким образом: стоимость = (количество проходов по зондирующей таблице + 1) * round(1269/1.676) + round(255/1.676) = 17 * 757 + 152 = 12,869 + 152 = 13,021 -- что и требовалось Файлы трассировки Два файла трассировки очень важны для исследования соединения хэширова- ния. Один из файлов является стандартной трассировкой стоимостного опти- мизатора — трассировкой события 10053; другой файл является трассировкой соединения хэширования — трассировкой события 10104. Также может быть достаточно полезным включить 10046 на уровне 8 для от- слеживания состояний ожидания ввода-вывода (I/O wait states) во время вы- полнения соединения — и, начиная с версии 9i, вы можете синхронизировать трассировку 10046 с операциями ввода-вывода, производимыми при выполне- нии соединения хэширования, установив для трассировки 10104 уровень 12.
Файлы трассировки 373 Это может быть особенно полезно, так как прямые операции чтения и записи, выполняемые соединениями хэширования при сохранении хэш-секций на диск или получении их обратно с диска, могут использовать форму асинхронного ввода-вывода, что приводит к появлению в файле трассировки 10046 очень не- большого количества состояний ожидания ввода-вывода. Событие 10104 Особенно удобным в файле трассировки 10104 является то, что данные в этом файле чрезвычайно информативны и могут дать вам очень хорошие подсказки о том, насколько большим должно быть значение hash_area_size, чтобы со- единение из соединения в несколько проходов превратилось в соединение в один проход или чтобы соединение в один проход превратилось в оптимальное со- единение. Мы уже видели разные части трассировки 10104, но ниже показаны четыре строки, которые находятся в самом начале файла трассировки версии 9i (то же самое с небольшими отличиями есть и в файлах версий 8i и 10g): Original memory: 131072 Memory after all overhead: 129554 Memory for slots: 122880 Estimated build size (KB): 2050 Значение Memory for slots показывает, сколько памяти максимально дос- тупно для создания хэш-таблицы в памяти. Значение Estimated build size показывает, сколько памяти оптимизатор считает нужным выделить под слоты. В этом случае, если вам нужно выполнить оптимальное соединение, вам пона- добится увеличить значение hash_a rea_si ze со 128 Кбайт (из которых доступ- но примерно 120 Кбайт) до значения, несколько превышающего 2 Мбайт. Если мы знаем, что у нас не хватает памяти, чтобы сделать значение hash_ area_size достаточно большим для оптимального соединения, то мы можем выделить достаточно памяти для соединения в один проход — как мы видели ранее, нам только необходимо проверить ту часть файла трассировки, в кото- рой описывается, как Oracle получает данные обратно с диска: Getting a pair of flushed partions. BUILD PARTION: nrows:256 size=(33 slots, 264K) PROBE PARTION: nrows:260 size=(34 slots, 272K) *** HASH JOIN BUILD HASH TABLE (PHASE 2) *** Number of blocks that may be used to build the hash hable 10 Number of rows 16ft to be Iterated over (start of function): 256 Number of rows iterated over this function call: 78 Number of rows left to be iterated over (end of function): 178 To количество раз, которое мы видим строку с end of function перед тем, как количество оставшихся записей дойдет до нуля, является числом, на кото- рое мы должны умножить hash_area_s i ze, чтобы превратить соединение в не- сколько проходов в соединение в один проход (эта последовательность строк повторяется для каждой пары сохраненных на диск хэш-секций — так что убе- дитесь, что вы проверили цифры на худшее значение).
374 Глава 12. Соединения хэширования Вы можете найти другую важную информацию об общей производительно- сти соединения хэширования в той части файла трассировки, в которой пред- ставлена информация уровня хэш-секции о распределении записей: ### Partition Distribution ### Partition:© rows:247 clusters:!! slots:1 kept=0 Partition:1 rows:244 clusters:32 slots: 1 kept=0 Partition:! rows:208 clusters:27 slots:1 kept=0 Partition:! rows:260 clusters:34 slots: 1 kept=0 Parti tion:4 rows:260 clusters:34 slots:1 kept=0 Partition:! rows:243 clusters:32 slots: 1 kept=0 Partition:! rows:282 clusters:37 slots:1 kept=0 Partition.7 rows;256 clusters:33 slots:7 kept=0 Эта секция взята из файла трассировки версии 9i для сценария hash_multi. sqL Как видите, оптимизатор решил разделить хэш-таблицу на восемь хэш-сек- ций. Также в этой части файла трассировки показано, сколько записей находит- ся в каждой хэш-секции, и, как видите, есть небольшой дисбаланс в распределе- нии записей — в одной хэш-секции находится только 208 записей, в то время как в другой — 282 записи. Это вполне нормально, и беспокоиться не о чем. Однако если вы видите большой дисбаланс в распределении записей по хэш-секциям, причиной может быть то, что ваши данные имеют необычное рас- пределение с несколькими часто повторяющимися значениями в столбцах со- единения. Это может привести к интенсивному использованию ресурсов про- цессора при выполнении соединения. Для многотабличного соединения это может подсказать вам, что нужно попробовать переписать запрос, чтобы изме- нить порядок соединения. Дисбаланс в распределении записей, скорее всего, не является результатом множественных хэш-коллизий в хэш-функции Oracle. Но я думаю, что могла бы быть вторая хэш-функция, которая могла бы использоваться в коде для по- вторного хэширования данных в памяти, если бы обнаружилось, что основная хэш-функция приводит к появлению слишком большого количества коллизий между несовпадающими записями. Другие столбцы в этой части файла трассировки — следующие. о Clusters. Количество кластеров (слотов), требующееся для размещения всех записей в этой хэш-секции. Как я уже указывал ранее, каждый кластер в этом примере состоит в точности из одного блока. о Slots. Количество слотов (кластеров) этой хэш-секции, находящихся в дан- ный момент в памяти. Эта часть файла трассировки выводится после окон- чания создания хэш-таблицы, но до того, как Oracle начнет перестройку хэш- таблицы. Как видите, при нехватке доступной памяти Oracle сохранил на диск хэш-секции с нулевой по шестую, хотя и оставил для каждой хэш-сек- ции по меньшей мере по одному слоту в памяти, а также решил оставить в памяти 7 слотов из 33 для седьмой хэш-секции. о Kept. Флаг устанавливается в нуль или единицу, чтобы показать, находится ли еще вся хэш-секция в памяти, или уже нет. Если все хэш-секции помечены как находящиеся в памяти (kept), то это соединение хэширования является оптимальным. Этот флаг не включен в трассировку версии 8i — вы должны
Файлы трассировки 375 определять эту информацию, проверяя, совпадает ли значение clusters (количество кластеров) со значением slots (количество слотов) — которые в версии 8i называются in-memory slots (слоты в памяти). Эта часть файла трассировки дает еще одну подсказку о том, насколько нуж- но увеличить значение hash_area_si ze, чтобы превратить соединение хэширо- вания в несколько проходов в соединение хэширования в один проход. Мы ви- дим, что Oracle достаточно для этого 7 слотов по сравнению с 37 в худшем случае (лучшей хэш-секцией является хэш-секция 7, которая требует только 33 слота и получила 7 слотов, а худшей хэш-секцией является хэш-секция 6, кото- рая требует 37 слотов). Нам понадобится умножить hash_area_si ze примерно на 37/7 = 5,3, чтобы получить хэширование в один проход. Событие 10053 В файле трассировки 10053 полезной для нас информации не очень много. На- пример, ниже показана часть файла трассировки версии 9i, относящаяся к со- единению хэширования, для выбранного порядка соединения из сценария hash_ opt.sql — трассировки версий 8i и 10g очень похожи на эту трассировку: НА Join Outer table: resc: 42 cdn: 500 rcz: 30 deg: 1 resp: 42 Inner table: PROBE_TAB resc: 60 cdn: 5000 rcz: 527 deg: 1 resp: 60 using join:8 distribution.^ #groups:l Hash join one ptn Resc: 4 Deg: 1 hash_area: 128 (max=128) buildfrag: 129 probefrag: 329 ppasses: 2 Hash join Resc: 106 Resp: 106 Ключевые понятия этой части файла трассировки описаны в табл. 12.2. Таблица 12.2. Описание элементов соединения хэширования в трассировке 10053 Элемент Описание Outer table Информация о данных, полученных на данный момент в порядке соединения. В нашем случае это просто указание стоимости получения 500 записей из build_tab Inner table Информация о данных, которые были необходимы из последней таблицы (или представления) в порядке соединения. В нашем случае это просто указание стоимости получения 5000 записей из probe_tab resc Стоимость последовательного выполнения шага deg Степень параллелизма в шаге resp Стоимость полного параллельного выполнения шага hash_area Номинальное минимальное значение hash_area_size в блоках. В версии 91 это является доступным для соединения минимумом, который устанавливается с помощью скрытого параметра _smm_min_size, указываемого в Кбайт hash_area Номинальное максимальное значение для hash_area_size в блоках. В версии 91 это (max=) является доступным для соединения максимумом при выполнении с автоматически настроенным параметром workarea_size_policy. В принципе, максимум устанавливается с помощью скрытого параметра _smm_max_size, который указывается в Кбайт. Однако, хотя в руководствах и указывается, что максимальное значение для отдельной рабочей области продолжение &
376 Глава 12. Соединения хэширования Таблица 12.2 (продолжение) Элемент Описание равняется 5 % от pga_aggregate_target, указанное здесь значение равняется 10 % от pga_aggregate_target (то есть 2 * _smm_max_slze). Похоже, здесь копируется поведение параметра hash_area_slze, значение которого в два раза больше значения sort_area_size. Часть расчетов оптимизатора, видимо, учитывает это ограничение, но включает граничное условие, которое переключает расчеты на использование минимального размера. Во время выполнения ограничение в 5 % выполняется — но и тут есть подозрения, что это достаточно гибкое ограничение. Еще больше путаницы вносит другой скрытый параметр, _pga_max_slze, со значением по умолчанию в 200 Мбайт, который является ограничением для всего PGA одного процесса. Значение _smm_max_slze не превышает половины значения _pga_max_slze, но вы не заметите этого, пока pga_aggregate_target не превысит 2 Гбайт — при этом значение _smm_max_slze достигнет 100 Мбайт и прекратит рост, если только вы не укажете новое значение _pga_max_slze или _smm_max_slze напрямую bulldfrag Размер набора данных создающей (первой) таблицы в блоках Oracle. Это значение абсолютно неверно для оптимального соединения хэширования в версии 81 (а также в версиях 91 и 10g, когда они эмулируют работу версии 81) — похоже, это значение показывается как hash_area + 1 для оптимальных соединений хэширования. В нашем примере правильное значение равно 3 probefrag Размер набора данных зондирующей таблицы в блоках Oracle ppasses Количество проходов по зондирующей таблице для выполнения соединения. Абсолютно неверно для нашего примера с оптимальным соединением, так как мы знаем, что в нашем случае сохранения на диск и получения данных обратно с диска не происходит. Но количество проходов никогда не указывается равным нулю. Похоже, это значение показано только для информационных целей, потому что оно уже включено в значение Hash join one ptn (см. следующий элемент) — и в большинстве моих тестовых примеров это значение не совпадало с тем, что реально происходило во время выполнения. Хотя в том случае, когда мы используем hash_areajslze и не включаем оценку стоимости использования ресурсов процессора (то есть при традиционном подходе оценки стоимости), значение ppasses, похоже, получается из bulldfrag / hash_area; во всех других условиях это значение равно 1 Hash join one Возможно, самая важная цифра. Примерно означает стоимость на одну ptn хэш-секцию создающей таблицы. Несмотря на стоимость, указанную в resc: (которая обычно называется «стоимостью последовательного выполнения»), это значение также используется в параллельных запросах, в которых оптимизатор, похоже, умножает resc: на deg перед включением его в итоговую стоимость соединения. Для соединений в несколько проходов эта цифра включает в себя компонент, который умножается на значение ppasses перед выводом в файл трассировки Hash join Для всех последовательных соединений итоговая стоимость соединения равна стоимости Hash join one ptn плюс стоимость получения данных из внешней и внутренней таблиц. В нашем примере мы имеем 106 = 4 + 60 + 42 Аномалии Достаточно легко найти какие-то аномалии в расчетах стоимости соединения хэширования, и перед тем, как прокомментировать различия между механизма- ми оценки стоимости, я бы хотел показать вам пару таких аномалий, чтобы вы
Аномалии 377 могли оценить трудность тщательной настройки значения hash_area_size (или pga_aggregate_target). Традиционная оценка стоимости Вернемся к сценарию hash_one.sql и выполним запрос с установленными вруч- ную значениями workarea_size_poUcy и hash_area_size в 1100 Кбайт. По- сле этого выполним запрос с установленным значением hash_area_si ze в 2200 Кбайт (см. сценарий hash_one_bad.sql в онлайн-хранилище кода). План выполнения (версия 9.2.0.6, автотрассировка, размер области хэширования = 1100 Кбайт) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=1081 Card=2000 Bytes=411400O) 1 0 HASH JOIN (Cost=1081 Card=2000 Bytes=4114000) 2 1 TABLE ACCESS (FULL) OF 'BUILD_TAB' (Cost=2SS Card=2000 Bytes=2060000) 3 1 TABLE ACCESS (FULL) OF 'PR0BE_TAB' (Cost=2SS Card=10000 Bytes=10270000) План выполнения (версия 9.2.0.6, автотрассировка, размер области хэширования = 2200 Кбайт) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=2769 Card=2000 Bytes=4114000) 1 0 HASH JOIN (Cost=2769 Card=2000 Bytes=4114000) 2 1 TABLE ACCESS (FULL) OF 'BUILD_TAB' (Cost=2SS Card=2000 Bytes=2060000) 3 1 TABLE ACCESS (FULL) OF 'PR0BE_TAB' (Cost=255 Card=10000 Bytes«10270000) Мы увеличили доступный объем памяти для соединения хэширования, но стоимость выполнения соединения хэширования выросла в 2,5 раза. Это не то, что вы ожидали, но это происходит, если выделенная память находится на гра- нице между оптимальным соединением хэширования и соединением хэширова- ния в один проход. Самым печальным в этом примере является то, что трассировка времени вы- полнения показывает, что Oracle без какой-то особой причины переключился с многоблочных операций ввода-вывода (с размером кластера в девять блоков) на одноблочные (с размером кластера в один блок), когда объем доступной па- мяти стал больше. Обычно, когда оптимизатор выдает странные результаты расчетов, вы обнаруживаете, что механизм времени выполнения все равно сде- лал что-то совсем другое. В этом же случае и механизм времени выполнения, и оптимизатор показали одно и то же странное поведение. Новая оценка стоимости С включенной оценкой стоимости использования ресурсов процессора и ис- пользованием динамического размера рабочей области в версии 9i вы можете подумать, что такие аномалии остались в прошлом. Однако сценарий hash_pat_ bad.sql в онлайн-хранилище кода показывает обратное. В нем используется тот
378 Глава 12. Соединения хэширования же запрос, что и в сценарии hash_one.sql, но также включается системная стати- стика и используются два разных значения pga_aggregate_target. С его по- мощью также можно получить непонятные результаты: План выполнения (версия 9.2.0.6, автотрассировка, pga_aggregate_target = 20 000 Кбайт) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=719 Card=2000 Bytes=4114000) 1 0 HASH JOIN (Cost=951 Card=2000 Bytes=4114000) 2 1 TABLE ACCESS (FULL) OF 'BUILDJTAB' (Cost=257 Card=2000 Bytes=2060000) 3 1 TABLE ACCESS (FULL) OF 'PROBEJFAB' (Cost=257 Card=10000 Bytes=10270000) План выполнения (версия 9.2.0.6, автотрассировка, pga_aggregate_target = 22 000 Кбайт) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=951 Card=2000 Bytes=4114000) 1 0 HASH JOIN (Cost=719 Card=2000 Bytes=4114000) 2 1 TABLE ACCESS (FULL) OF 1BUILD_TAB' (Cost=257 Card=2000 Bytes=2060000) 3 1 TABLE ACCESS (FULL) OF 'PROBEJFAB' (Cost=2S7 Card=10000 Bytes=10270000) Значение, которое я установил для pga_aggregate_target в этом примере, достаточно мало — даже значение по умолчанию больше, но и тестовый набор данных тоже достаточно мал. Эта аномалия может быть повторена с большими значениями параметра для больших наборов данных. Дополнительной странностью в этом случае является то, что различия мож- но найти только в файле трассировки 10053, в котором изменяется стоимость выполнения соединения хэш-секций. Файлы же трассировки 10104 идентичны. Это еще одно подтверждение того, что оптимизатор и механизм времени вы- полнения не всегда используют одну и ту же модель. Сравнения При оценке стоимости есть некоторые странности, особенно в граничных об- ластях, когда объем доступной памяти находится в пределах переключения с одного уровня соединения на другой. Существуют противоречия между моде- лью, используемой кодом оптимизатора, и действиями механизма времени вы- полнения. Но все равно можно примерно понять, что происходит, и узнать, как влияют объем данных и значения параметров. С помощью трассировок 10053 и 10104 можно определить, можете ли вы сделать что-либо для повышения про- изводительности выполнения важного запроса. Хотя сейчас я не думаю, что такая точная настройка настолько важна. На- стоящей проблемой, с которой сталкивается большинство людей, являются из- менения при миграции между версиями Oracle и принятие решения, какие па- раметры нужно изменить. Именно при этом и возникают проблемы. Поэтому
Сравнения 379 в этой секции мы выполним стандартный тест, чтобы мы могли сравнить рабо- ту оптимизатора в четырех различных рабочих средах. В онлайн-хранилище кода содержится четыре сценария, чтобы помочь нам сравнить поведение оптимизатора в разных средах. В этих сценариях рассмат- риваются четыре комбинации значений параметров. о Оценка стоимости использования ресурсов процессора выключена, ручная настройка hash_area_size — has_nocpu_harness.sql. о Оценка стоимости использования ресурсов процессора включена, ручная на- стройка hash_area_size — has_cpu_harness.sqL о Оценка стоимости использования ресурсов процессора выключена, автома- тическая настройка hash_area_si ze — pat_nocpu_harness.sql. о Оценка стоимости использования ресурсов процессора включена, автомати- ческая настройка hash_area_size — pat_cpu_harness.sql. Конечно, первым идет традиционный подход версии 8i, последний же, скорее всего, будет вашим выбором в версии 9i. К сожалению, результаты, получаемые из расчетов соединения хэширования, очень сильно расходятся с вашим выбором. HASH_AREA_SIZE И РАЗДЕЛЯЕМЫЕ СЕРВЕРЫ Если вы используете автоматическую настройку размера рабочей области для разделяемых серве- ров (называемых также многопоточными серверами — multithreaded servers, MTS), то многие воз- можности pga_aggregate_target будут недоступны до версии 10g. Исторически память для соедине- ния хэширования выделяется в UGA, но в случае использования разделяемых серверов память выделяется в SGA. В версии 9i ситуация не изменилась, и значение hash_area_size влияет на память, выделяемую разделяемыми серверами, хотя выделение памяти и отслеживается более новым ко- дом рабочей области. Что странно, оптимизатор не знает, на разделяемом или на выделенном сер- вере выполняется SQL-код, поэтому расчеты всегда основываются на использовании pga_aggre- gate_target — даже когда доступная память ограничена с помощью hash_area_size. В версии 10g область хэширования разделяемого сервера находится не в UGA, а в PGA, и поэтому ее размер зависит от pga_aggregate_target. Обратите внимание, в более ранних версиях Oracle давал- ся совет отказаться от разделяемых серверов для сессий, в которых выполняются операции хэширо- вания или сортировки, занимающие долгое время, — возможно, в версии 10g в таком предупрежде- нии больше нет нужды. Четыре сценария в основном делают одно и то же. Когда я включил оценку стоимости использования ресурсов процессора, я использовал следующие на- стройки для эмуляции расчетов, производимых в традиционной оценке стоимо- сти операций ввода-вывода: begi п dbms_stats.set_system_stats('MBRC,6.59); dbms_stats.set_system_stats('MREADTIM',10.001); dbms_stats.set_system_stats('SREADTIM',10.000); dbms_stats.set_system_stats('CPUSPEED',1000); end; I Я установил MBRC равным 6,59, потому что это соответствует значению, ко- торое использует оптимизатор для оценки стоимости табличных сканирований (и быстрых полных сканирований индексов), при значении db_f 1 le_multi -
380 Глава 12. Соединения хэширования block_read_count, равном 8 — моему обычному значению для тестов. Я также сделал mreadtim и sreadtim практически одинаковыми — считается, что так работают старые алгоритмы расчета стоимости операций ввода-вывода, исполь- зуемые оптимизатором. Затем сценарии генерируют SQL-сценарий, который вызывает третий SQL- сценарий (has_dump.sql или pat_dump.sql, в зависимости от вызывающего сцена- рия) много раз. ВНИМАНИЕ Механизм использования сценариев, создающих другие сценарии и вызывающих их, является очень опасным на рабочих системах, но очень удобен при тестировании. Сценарии, которые используют ручную настройку hash_area_size, содер- жат строку, похожую на показанную ниже: alter session set hash_area_size = &2; Сценарии, которые используют автоматическую настройку hash_area_ s i ze — то есть те, в которых для worka rea_s 1 ze_pol 1 су установлена автомати- ческая настройка и которые используют pga_aggregate_target для контроля hash_area_si ze — содержат строку, похожую на показанную ниже: alter system set pga_aggregate_target =• &2 scope = memory; Все вызываемые сценарии включают трассировки 10053 и 10104 и использу- ют tracefile_identifier, чтобы гарантировать, что каждый тест генерирует свой собственный файл трассировки с уникальным именем, например: alter session set events '10053 trace name context forever, level 2'; alter session set events '10104 trace name context forever'; alter session set tracefile_identifier = 'pat_&l._&2'; ПРИМЕЧАНИЕ В качестве менее ресурсоемкого, хотя и менее информативного подхода я написал четыре сцена- рия, которые просто проходят в цикле по значениям в hash_area_slze (или в pga_aggregate_target), генерируя планы выполнения для определенной команды; эти сценарии в онлайн-хранилище кода называются hash stream(a,b,c,d).sql. У меня была одна небольшая проблема — нужно было решить, как связать данные из тестов с hash_area_size с данными из тестов с pga_aggregate_ target. Согласно руководствам, любая отдельная рабочая область ограничена 5 % от значения pga_aggregate_target, поэтому очевидно, что при значении pga_ aggregate_target, равном 100 Мбайт, значение hash_area_size должно быть равно 5 Мбайт. С другой стороны, когда я установил значение pga_aggregate_target рав- ным 100 Мбайт, трассировка 10053 показала, что значение hash area (max=) равно 10 Мбайт, а не 5 Мбайт. Поэтому, в конце концов, я решил, что два набо- ра данных связаны между собой формулой hash_area_size = 0,1 * pga_ aggregate_target, основываясь на том, что это значение оптимизатор исполь- зует практически во всех своих расчетах.
Сравнения 381 Кроме всего этого, есть еще одна небольшая проблема: минимальное разре- шенное значение pga_aggregate_target равно 10 Мбайт, что соответствует 1 Мбайт для hash_area_size по моей формуле; однако минимальное разре- шенное значение для hash_area_si ze равно 32 Кбайт (хотя это значение и иг- норируется и реальный минимум, похоже, составляет 64 Кбайт). Поэтому мои наборы данных не начинаются с одних и тех же значений. Наконец, вместо того, чтобы показать действительную стоимость выполне- ния запроса, я отнял от итоговой стоимости стоимость двух табличных скани- рований, чтобы показать вам стоимость самого соединения. В табл. 12.3 показана стоимость выполнения базового запроса для различ- ных значений параметров при одинаковом объеме памяти, выделенном на хэш- таблицу — и действительно, видна разница; значения при включенной оценке стоимости использования ресурсов процессора действительно получились мень- ше значений при выключенной оценке. Таблица 12.3. Изменение стоимости при изменении выделения памяти и значений параметров Размер памяти для соединения хэширования Стоимость: ручная настройка Оценка стоимости использования ресурсов процессора выключена Стоимость: ручная настройка Оценка стоимости использования ресурсов процессора включена Стоимость: автоматическая настройка Оценка стоимости использования ресурсов процессора выключена Стоимость: автоматическая настройка Оценка стоимости использования ресурсов процессора включена 128 Кбайт 13 021 658 20 256 Кбайт 6965 408 3 384 Кбайт 3455 1242 **** 512 Кбайт 1911 765 768 Кбайт 1103 384 960 Кбайт 884 234 1024 Кбайт 617 194 506 441 1280 Кбайт 533 117 506 441 1408 Кбайт 501 90 506 441 1536 Кбайт 448 62 506 441 1792 Кбайт 408 28 306 209 • 2048 Кбайт 2416 **** 2 506 **** 441 **** 2560 Кбайт 789 2 506 441 3072 Кбайт 392 2 506 441 3584 Кбайт 255 2 306 209 4096 Кбайт 186 2 306 209 4608 Кбайт 306 209 5120 Кбайт 306 209 5632 Кбайт 190 104 6144 Кбайт 190 104
382 Глава 12. Соединения хэширования Первое, что вы замечаете: есть только два столбца, значения в которых хоть как-то похожи, и значения в обоих этих столбцах зависят от pga_aggre- gate_target. Но и при этом тенденция изменения значений совпадает, хотя, правда, не сами значения. Независимо от того, что вы меняете в вашей систе- ме — обновляете версию или изменяете значение какого-либо параметра, — вы можете столкнуться с проблемами, из-за которых стоимость соединений хэши- рования изменится очень сильно. Второе, что вы можете заметить, это то, что какие бы настройки вы ни уста- новили в вашей базе, существуют ошибки в коде или возможные недостатки в модели. Для каждого отдельного столбца есть случай (отмеченный знаками ****), когда увеличение доступной памяти для соединения хэширования при- водит к увеличению стоимости выполнения этого соединения хэширования. Похоже, что это происходит в граничных ситуациях, когда значение hash_ area_size (или значение hash area (max=)) примерно равно размеру первого набора данных — и прямым результатом этого является то, что для оптималь- ных соединений хэширования рассчитывается слишком большая стоимость. Стоимость соединения уменьшается при увеличении hash_area_size, но если значение hash_area_size очень большое, оно перестает влиять на стои- мость. ПРОБЛЕМЫ ПРИ ИЗМЕНЕНИИ РАЗМЕРА ХЭШ-СЕКЦИИ Мы уже видели, что данные, используемые оптимизатором для расчетов (например, значение ppasses, когда оно есть), и данные, используемые механизмом времени выполнения, — не одни и те же. Странные случаи увеличения стоимости, показанные в табл. 12.3, могут быть результатом того, что оптимизатор переключается на использование большего размера хэш-секции при увеличении доступной памяти — это требует большого количества проходов по зондирующей таблице. Есть случаи, когда такая странная ошибка появляется в трассировке времени выполнения. Напри- мер, когда значение hash_area_size равно 256 Кбайт, мое соединение разбивается на 16 хэш-секций и выполняется как соединение в один проход. Когда же значение hash_area_size увеличивается до 320 Кбайт, количество хэш-секций уменьшается до 8 (они становятся больше), что приводит к более эффективным операциям ввода-вывода, но соединение становится соединением в несколько про- ходов. Если вы включите оценку стоимости использования ресурсов процессора, но не установите автоматическую настройку размера рабочей области, то стои- мость оптимальных соединений и соединений в один проход кажется вполне разумной — даже тогда, когда стоимость соединений, полностью помещающих- ся в памяти, снижается чуть ли не до нуля. Если ваши самые важные запросы как раз такие, то, похоже, этот вариант является лучшим выбором для вашей системы (или, по крайней мере, для сессий, в которых выполняются соедине- ния хэширования). Однако проблема возникает, когда первый набор данных больше значения hash_area_size — такое впечатление, что в коде расчетов пе- рестает учитываться стоимость множества проходов по зондирующей таблице, в результате чего стоимость соединений в несколько проходов оказывается го- раздо ниже, чем вы могли бы ожидать. В результате соединения хэширования могут быть выбраны в случаях, когда есть и лучшие варианты. Поэтому следи-
Сравнения 383 те, не являются ли ваши соединения хэширования очень неэффективными, если у вас есть код SQL, в котором оба набора данных в соединении очень ве- лики. При переключении на автоматическую настройку размера рабочей области результат выглядит несколько странным. Код явно рассчитывает совершенно нереальную высокую стоимость оптимальных соединений хэширования. На са- мом деле при включенной оценке стоимости использования ресурсов процессо- ра стоимость соединения хэширования постепенно падала до нуля, но только когда я увеличил pga_aggregate_target практически до 1000 Мбайт, и я до сих пор не знаю, почему. Есть существенная разница между моделями, используемыми для ручной и автоматической настройки, которая делает новую модель гораздо более под- ходящей — хотя некоторая настройка все же требуется и в этом случае. Обрати- те внимание, что стоимость в старой модели меняется постепенно (в большин- стве случаев) при изменении доступной памяти, в то время как в новой модели стоимость какое-то время остается постоянной, после чего меняется резко. Это вполне логично — на самом деле есть две причины, почему изменение стоимости соединений хэширования должно выглядеть ступенчатым. Вспомните, что хэш-таблица разделена на хэш-секции, а хэш-секции состоят из кластеров. Кластер является единицей измерения при выполнении ввода-вывода. Самыми важными решениями, которые должен принять оптимизатор при выполнении соединения хэширования, являются следующие — сколько должно быть хэш- секций и насколько большими должны быть кластеры. Предположим, у вас достаточно памяти для N кластеров. Если вы увеличите память на N блоков, то каждый кластер станет на один блок больше. Это зна- чит, что операции ввода-вывода (многоблочные) при сохранении хэш-секций на диск и получении их обратно с диска станут немного более эффективными. Вот первая причина для ступенчатого изменения стоимости. Вторая причина ступенчатого изменения стоимости может быть объяснена процессом сохранения хэш-секции на диск и получением ее обратно с диска. Предположим, что во время выполнения есть возможность держать в памяти две хэш-секции создающей таблицы после окончания фазы создания хэш-таб- лицы. В этом случае только шесть из восьми хэш-секций зондирующей табли- цы необходимо сохранить на диск и получить обратно с диска. Увеличьте па- мять на 25 % — и, скорее всего, три из восьми хэш-секций создающей таблицы останутся в памяти после окончания фазы создания хэш-таблицы — поэтому только пять хэш-секций зондирующей таблицы придется сохранить на диск и получить обратно с диска. Конечно, при увеличении размера памяти оптимизатор может решить изме- нить размер кластера или изменить количество хэш-секций, чтобы сбалансиро- вать размер операций ввода-вывода и возможность выполнения оптимального соединения, но, в принципе, эти две причины должны четко проявиться в рас- четах стоимости. На самом деле, если внимательно посмотреть на результаты традиционной оценки стоимости, а затем сравнить с результатами в файлах трассировки 10104, то вы можете увидеть влияние изменения размера кластера. Ниже пока- зана часть набора данных из сценария hash_stream_a.sql:
384 Глава 12. Соединения хэширования Размер области хэширования, Кбайт Итоговая стоимость Стоимость хэширования 512 1277 765 520 1273 761 528 1269 757 536 1265 753 544 1261 749 552 1072 560 разница = -189 (см. ниже) 560 1069 557 568 1066 554 576 1063 551 584 1060 548 592 1057 545 600 1054 542 608 1051 539 616 1048 536 624 1045 533 632 1042 530 640 1039 527 648 1036 524 656 1033 521 664 927 415 разница = -106 (см. ниже) 672 925 413 680 923 411 688 920 408 696 918 406 704 915 403 При увеличении hash_area_size стоимость хэширования начинает посте- пенно уменьшаться по несколько единиц, после чего в определенной точке рез- ко уменьшается (это отмечено строками с указанием разницы между текущим и предыдущим значениями в списке выше). Эти резкие изменения вызваны тем, что оптимизатор увеличил размер кластера при очередном увеличении hash_area_si ze. Если вы сравните между собой трассировки 10104 и 10053, то вы, скорее всего, обнаружите, что каждое резкое изменение стоимости хэширо- вания действительно соответствует увеличению размера кластера на один блок (соответствие очень близкое, но не абсолютно точное — модель механизма вре- мени выполнения не повторяет в точности модель, используемую в расчетах). Итак, старая модель не поддерживает операции ввода-вывода разного разме- ра — и то же самое наблюдается, когда вы включаете оценку стоимости исполь- зования ресурсов процессора, даже если значение hash_area_size меняется вручную. Так как дело в этом, вы можете поинтересоваться, что же произойдет, если вы начнете изменять системную статистику — в конце концов, наибольшее влияние системной статистики заключается в том, что она указывает оптимиза- тору размер многоблочных операций чтения и относительное время на выпол- нение этих операций. Так какая же системная статистика влияет на стоимость соединения? Неудивительно, что, если вы измените скорость процессора, стоимость со- единения изменится — хотя, как вы и можете ожидать, изменение, скорее всего, не будет очень большим.
Сравнения 385 Стоимость также изменится, если вы измените относительные значения mreadtim и sreadtim. Соединения хэширования выполняют множество много- блочных чтений и записей (хотя и прямых), поэтому время выполнения опера- ций ввода-вывода должно влиять на стоимость, и, похоже, оптимизатор просто использует mreadtim в качестве времени выполнения операций ввода-вывода размером в кластер, вне зависимости от реального текущего размера кластера. Значение статистики MBRC также может иметь некоторое влияние — но явно не всегда. Я еще не рассматривал это влияние, но думаю, что эта статистика еще больше усложняет принятие решения о количестве хэш-секций и размере кла- стера, и, так как вы можете видеть для этой статистки только значения времени выполнения, а не спрогнозированные значения оптимизатора, очень трудно по- нять, почему стоимость изменилась при изменении значения MBRC, когда она действительно изменилась, и почему она не изменилась, когда осталась прежней. Итак, оценка стоимости использования ресурсов процессора имеет некото- рое влияние, и причина резкого изменения стоимости вполне понятна при ис- пользовании автоматической настройки размера рабочей области. Как бы то ни было, большие интервалы постоянной стоимости при использовании автомати- ческой настройки размера рабочей области все еще вызывают вопросы. Ответ становится ясен только после рассмотрения трассировки 10104, когда мы обна- руживаем, что оптимизатор принимает решения на основе объема достаточной памяти совсем по-другому. Как мы видели ранее, первыми значениями в трассировке 10104 являются следующие: Original memory (исходная память): 581 632 - - 568 Кбайт Memory after all overhead (память с учетом всех накладных расходов): 710 649 - - 694 Кбайт Memory for slots (память для слотов): 688 128 - - 672 Кбайт Я извлек эти три значения из файла трассировки, в котором значение pga_ aggregate_target было равным 11 440 Кбайт, чтобы подчеркнуть несколько особенностей. Во-первых, соединение хэширования действительно начинается с 5 % от pga_aggregate_target, но практически сразу принимается решение, что этого недостаточно. Поэтому похоже, что 5 % не является строгим лимитом для соединений хэширования. Во-вторых, когда я рассматриваю значение Memory for slots (память для слотов) в нескольких других файлах трассировки, в которых значение pga_ aggregate_target изменяется от 10 Мбайт до 40 Мбайт, вплоть до значения 33 200 Кбайт (32.4 Мбайт) значение Memory for slots остается равным 672 Кбайт, после чего Oracle сразу увеличивает использование памяти для своих слотов до 1 440 Кбайт. Эта стратегия отражает оценку стоимости при включенной авто- матической настройке размера рабочей области: оптимизатор получает одну и ту же стоимость при различных значениях размера памяти, потому что он планирует использование одного и того же объема памяти вне зависимости от того, сколько памяти ему доступно.
386 Глава 12. Соединения хэширования Когда вы переключаетесь на использование новой технологии, уже не важ- но, какое значение у pga_aggregate_target, потому что оптимизатор эффек- тивно определяет нужный объем памяти для выполнения соединения. Единст- венное, оптимизатор изменяет требуемый ему объем памяти большими шагами, потому что на самом деле только большое изменение приводит к заметному из- менению стоимости. В этом примере произошло большое изменение стоимости, потому что Oracle вместо 7 блоков на хэш-секцию стал использовать 15 бло- ков — это такое изменение, из-за которого дополнительное использование па- мяти приводит к заметному повышению производительности операций вво- да-вывода. Соединения с множеством таблиц Если описание изменений в стратегии недостаточно убедительно, чтобы вы со- гласились, что автоматическая настройка размера рабочей области эффективна, то давайте рассмотрим более реальные примеры SQL-кода. Очень редко встречаются серверы, на которых выполняются запросы только с двумя таблицами, и когда вы начинаете выполнять соединения хэширования с множеством таблиц, объемы требуемой памяти могут вырасти очень сильно. Рассмотрим следующий простой запрос (см. сценарий treble_hash_auto.sql в он- лайн-хранилище кода): select /*+ ordered full(tl) full(t2) full(t3) full(t4) use_hash(t2) use_hash(t3) use_hash(t4) swap_join_inputs(12) swap_join_i nputs(t3) »/ count(tl.small_vc). count(t2.small_vc). count(t3.small_vc). count(t4.small_vc) from tl. t4. t2, t3 where t4.idl = tl.id and t4.id2 = t2.id and t4.id3 = t3.id План выполнения (версия 9.2.0.6, автотрассировка). 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=927 Card=l Bytes=38) 1 0 SORT (AGGREGATE) 2 1 HASH JOIN (Cost=927 Card=343000 Bytes=13034000) 3 2 TABLE ACCESS (FULL) OF 'T3' (Cost=2 Card=70 Bytes=420) 4 2 HASH JOIN (Cost=923 Card=343000 Bytes=10976000) 5 4 TABLE ACCESS (FULL) OF 'T2' (Cost=2 Card=70 Bytes=420)
Соединения с множеством таблиц 387 6 4 HASH JOIN (Cost=919 Card=343000 Bytes=8918000) 7 6 TABLE ACCESS (FULL) OF 'Tl' (Cost=2 Card=70 Bytes=420) 8 6 TABLE ACCESS (FULL) OF 'T4' (Cost=915 Card=343000 Bytes=6860000) Подсказки на самом деле не нужны, но я поместил в запрос полный набор подсказок, чтобы показать вам подсказку swap_j oi n_i nputs (), которую вы мо- жете использовать в соединениях хэширования. Подсказки также подчеркива- ют тот факт, что порядок, в котором таблицы появляются в плане выполнения, не всегда понятен. В этом примере Oracle выполняет хэширование таблицы t3 (строка 3) в па- мяти, затем выполняет хэширование таблицы t2 (строка 5) в памяти, и, нако- нец, выполняет хэширование таблицы tl (строка 7) в памяти. После этого на- чинается сканирование таблицы t4. Oracle извлекает запись из t4 и ищет ей соответствие в tl; если такое соот- ветствие существует, Oracle ищет соответствие этой записи в 12; если такое со- ответствие существует, Oracle ищет соответствие этой записи в t3; если такое соответствие существует, эта запись передается операции сортировки (агрега- ции). Соединение хэширования с множеством таблиц может выдавать резуль- тат очень быстро — не всегда нужно завершать первое соединение хэширования перед тем, как начать второе. Несмотря на мое описание того, как Oracle выполняет хэширование 13, за- тем 12, затем tl, после чего ищет соответствие в t4, Oracle следует моей под- сказке порядка выполнения соединения. Посмотрите внимательно на порядок таблиц в выражении from (tl, t4, t2, t3) — ведь Oracle действительно вы- полнил указанное в подсказке. Это было сделано следующим образом. О Join order [1] (первый порядок соединения): 11, t4, t2, t3. О Now joining t4 (соединение c t4): (tl, t4). О Now joining t2 (соединение c t2): ((tl, t4), t2). О Но мы используем подсказку swap_join-inputs(t2), что означает, что t2 является второй таблицей соединения хэширования, то есть мы изменяем порядок соединения: (t2, (tl, t4)). О Now joining t3 (соединение с t3): ((t2, (tl, t4)), t3). О Но мы используем подсказку swap_join_inputs(t3), что означает, что t3 является второй таблицей соединения хэширования, то есть мы изменяем порядок соединения: (t3, (t2, (tl, t4))), что мы и получили в плане вы- полнения. Но, как бы то ни было, целью этого раздела является описание преимуществ использования автоматической настройки workarea_size_policy. Поэтому вы- полним запрос дважды: первый раз со значением hash_area_size, равным 10 Мбайт, и второй раз со значением pga_aggregate_target, равным 200 Мбайт (что, согласно руководствам, является эквивалентом значения hash_area_size, равного 10 Мбайт, хотя, судя по моим наблюдениям, оптими- затор может использовать в расчетах значение 20 Мбайт). Перед тем как мы выполним тест, вспомните, что этому запросу нужны три рабочие области — по одной на каждое выполняемое соединение хэширования —
388 Глава 12. Соединения хэширования и эти рабочие области будут выделяться одновременно. Если у вас производит- ся соединение 11 таблиц, вам нужны десять рабочих областей, если все соеди- нения окажутся соединениями хэширования (и еще одна или две, если после соединения вы делаете группировку или сортировку). Если я начну новую сессию и проверю статистику сессии после выполнения запроса, я получу следующие результаты в версии 9.2.0.6 со значением hash_ area_si ze, установленным вручную: Name session logical reads session uga memory max session pga memory max consistent gets physical reads Value 6037 12 948 124 14 791 008 6037 6015 А вот что я получу при использовании автоматической настройки значения workarea_si ze_poli су: Name Value session logical reads 6037 session uga memory max 3 058 432 session pga memory max 3 256 672 consistent gets 6037 physical reads 6018 Количество логических и физических операций ввода-вывода показывает, что оба запроса делали одно и то же. Важными цифрами являются значения memory max. Обратите внимание, насколько больше памяти использовал запрос со значением hash_area_si ze, установленным вручную. Учитывая, что три хэ- шируемые таблицы содержали только по 70 коротких записей каждая, для хэ- ширования было использовано очень много памяти (в версии 10g были получе- ны лучшие результаты при автоматической настройке размера, при этом было использовано не более 1,5 Мбайт на соединение четырех таблиц). Создается впечатление, что при выполнении запроса со значением hash_ area_si ze, установленным вручную, Oracle тут же выделяет по меньшей мере половину hash_area_size в начале выполнения первого соединения хэширо- вания — и не освобождает эту память, даже если каждая хэшируемая таблица требует совсем немного памяти. НАСКОЛЬКО ТРЕБОВАТЕЛЬНЫ К РЕСУРСАМ СОЕДИНЕНИЯ ХЭШИРОВАНИЯ? Различные детали механизма хэширования могут отличаться от версии к версии, но объем памяти, требуемый для соединения хэширования с несколькими таблицами, может стать очень большим при увеличении hash_area_slze — в ранних выпусках версии 81 даже были случаи, когда выделялось по 100 % доступной памяти на каждое соединение. При выполнении запроса с большим значением pga_aggregate_target Ora- cle, похоже, вначале выделяет достаточно большой объем памяти на слоты хэш-таблицы, но потом освобождает ее, если такой объем не требуется. При вы- полнении соединения хэширования вы можете видеть ссылки на операции из-
Заключение 389 менения объема используемой памяти в файле трассировки 10104 — уменьше- ние объема показано в следующем примере: Slot table resized: old=23 wanted=12 got=12 unload=0 В других случаях Oracle выделяет слишком мало памяти, после чего увели- чивает ее объем — вот почему в предыдущем разделе мы видели файл трасси- ровки 10104 с 672 Кбайт памяти, хотя лимит равнялся 1,5 Мбайт или даже 3 Мбайт: если Oracle требуется больше памяти, чтобы избежать выполнения со- единения с высокой стоимостью, он увеличит размер хэш-таблицы, когда дой- дет до конца первой фазы — фазы создания хэш-таблицы. Заключение Алгоритмы определения стоимости соединений хэширования сильно зависят от того, включены ли оценка стоимости использования ресурсов процессора и (или) автоматическая настройка размера рабочей области. Это значит, что пе- реход от версии 8i к версии 9i или любое изменение этих настроек могут при- вести к значительным изменениям стоимости, что, в свою очередь, может при- вести к значительным изменениям планов выполнения. Какие бы настройки вы ни установили для оценки стоимости, в расчетах су- ществуют «катастрофические» точки — в этих точках изменение hash_ area_size (или pga_aggregate_target), которое явно должно привести к умень- шению стоимости, приводит к ее увеличению, причем такое поведение зависит от установленных настроек. Это значит, что вы не можете производить точную оп- тимизацию соединений хэширования, изменяя эти настройки и ожидая ста- бильный конечный результат — результат может оказаться непредсказуемым. На данный момент похоже, что самое стабильное поведение у настройки hash_area_size, установленной вручную, с включенной оценкой стоимости использования ресурсов процессора. Эта настройка не приводит к хорошей стоимости для соединений в несколько проходов, но дает возможность полу- чить хорошую стоимость для оптимальных соединений и соединений в один проход. Несмотря на явную стабильность поведения настройки hash_area_si ze, ус- тановленной вручную, с включенной оценкой стоимости использования ре- сурсов процессора, лучше все-таки устанавливать автоматическую настройку workarea_size_policy и использовать pga_aggregate_target с включенной оценкой стоимости использования ресурсов процессора. Чтобы избежать про- блем с запросами, вы можете изменить объем выделяемой памяти на уровне сессии и изменить неправильный порядок соединения с помощью очень осто- рожного применения подсказок на уровне команды. Помните, что автоматическая настройка workarea_size_policy выделяет память для соединений с несколькими таблицами гораздо эффективнее, чем на- стройка с установленным вручную значением. Это само по себе является очень хорошей причиной начать использовать новый механизм.
390 Глава 12. Соединения хэширования Тестовые сценарии Файлы к этой главе, доступные для загрузки, перечислены в табл. 12.4. Таблица 12.4. Тестовые сценарии к главе 12 Сценарий Комментарии Hash_opt.sql Простой пример соединения хэширования в памяти (оптимизация соединения) Hash_one.sql Hash_multi.sql Hash_one_bad.sql Соединение хэширования в один проход Соединение хэширования в несколько проходов Аномалия при оценке стоимости соединения хэширования в один проход с помощью традиционных методов Hash_pat_bad.sql Аномалия при оценке стоимости соединения хэширования в один проход с помощью новых возможностей has_nocpu_hamess.sql Генерация трассировок со значением hash_area_size, установленным вручную, без включенной оценки стоимости использования ресурсов процессора has_cpu_harness.sql Генерация трассировок со значением hash_area_slze, установленным вручную, с включенной оценкой стоимости использования ресурсов процессора pat_nocpu_hamess.sql Генерация трассировок с автоматически настраиваемым значением hash_area_slze, без включенной оценки стоимости использования ресурсов процессора pat_cpu_harness.sql Генерация трассировок с автоматически настраиваемым значением hash_area_size, с включенной оценкой стоимости использования ресурсов процессора has_dump.sql Сценарий, который используется сценариями has_nocpu_harness.sql и has_cpu_harness.sql pat_dump.sql Сценарий, который используется сценариями pat_nocpu_harness.sql и pat_cpu_harness.sql hash_stream_a.sql Стоимость соединения хэширования со значением hash_area_size, установленным вручную, без включенной оценки стоимости использования ресурсов процессора hash_stream_b.sql Стоимость соединения хэширования со значением hash_area_size, установленным вручную, с включенной оценкой стоимости использования ресурсов процессора hash_stream_c.sql Стоимость соединения хэширования с автоматически настраиваемым значением hash_area_size, без включенной оценки стоимости использования ресурсов процессора hash_stream_d.sql Стоимость соединения хэширования с автоматически настраиваемым значением hash_area_size, с включенной оценкой стоимости использования ресурсов процессора treble_hash.sql treble_hash_auto.sql Генерация таблиц для соединения хэширования с четырьмя таблицами Выполнение соединения хэширования с четырьмя таблицами с workarea_size_policy = auto treble_hash_manual.sql Выполнение соединения хэширования с четырьмя таблицами с workarea_size_policy = manual snapjnyst.sql cjnystats.sql Показывает изменения в статистике текущей сессии Создает представление, используемое сценарием snapjnyst.sql — должно быть выполнено как SYS setenv.sql Установка стандартизированной тестовой среды для SQL*Plus
*1 О Соединения I сортировки и слияния Для соединения слияния требуется, чтобы оба входных потока были отсорти- рованы, и достаточно часто сортировка потребляет больше ресурсов, чем остав- шаяся часть операции. Поэтому первый раздел этой главы полностью посвящен сортировке, а описание механизмов и стоимости операции слияния дается в ее остальных разделах. Сортировка — это специфическая операция, которая заслуживает особого рассмотрения, потому что это распространенная операция, а механизмы ее вы- полнения и требования к ресурсам все еще не очень хорошо известны. Oracle может потребоваться сортировка для выражений order by, group by, оператора di stinet, соединений слияния, запросов connect by, конвертации индексов co структурой бинарного дерева в битовые индексы, аналитических функций, опе- раций над множествами, создания индексов и, возможно, еще для пары случаев, которые я мог пропустить. Хотя пользователи, начиная с версии 9i, могут воспользоваться возможно- стью автоматической настройки workarea_size_poliсу для управления памя- тью и использовать pga_aggregate_target для настройки памяти при выпол- нении операций массовой обработки данных в памяти, таких как сортировка, в начале этой главы эти настройки отключены. Вспомните, что автоматическая настройка workarea_size_policy влияет на то, какой объем памяти получает процесс и как он его освобождает; но поскольку я сначала хочу продемонстри- ровать, как Oracle использует эту память, на самом деле не важно, использует ли Oracle динамическое управление выделением памяти, или я делаю это вруч- ную. Как мы уже делали в главе 12, мы рассмотрим четыре возможные комбинации настройки оценки стоимости использования ресурсов процессора {CPU costing) и настройки workarea_si ze_poli су. И, как и в случае с соединениями хэширо- вания, мы обнаружим, что изменения, происходящие при включении этих на- строек, более важны, чем любые абсолютные цифры, получаемые в результате расчетов. Одной из ключевых проблем в понимании расчета стоимости сортировки является то, что формула расчета стоимости из раздела 9.2 «Performance Guide
392 Глава 13. Соединения сортировки и слияния and Reference», которую я привел в главе 1, не включает компонент, отражаю- щий те операции сохранения данных, относящихся к операциям сортировки, на диск и получения их обратно с диска, которые во время выполнения сохраняют часть своих данных на диске. Еще одна проблема появляется, когда мы анализируем файлы трассировки 10053 и обнаруживаем, что есть несколько расхождений между моделью, подра- зумеваемой рассчитанными значениями, и реальной деятельностью. Заметные различия между версиями также никак не упрощают анализ файлов трассировки. Введение Как и в случае с соединениями хэширования, существуют три уровня эффек- тивности сортировки: оптимальная, в один проход и в несколько проходов. о Оптимальная сортировка полностью выполняется в памяти. Мы просто по- лучаем и сортируем поток данных, сохраняя все в памяти. Данные заканчива- ются до того, как заканчивается память, поэтому нам не нужно использовать дисковое пространство в качестве временной рабочей области. Несмотря на распространенное мнение, память выделяется постепенно при получении данных; сразу весь размер sort_a re a_s i ze (или тот лимит, который зависит от pga_aggregate_target) не выделяется в начале сортировки. о Сортировка в один проход выполняется, когда наборы данных слишком большие, чтобы поместиться в памяти. Мы получаем столько данных, сколь- ко может уместиться в доступной памяти, сортируем их во время получения и, достигнув лимита памяти, сохраняем отсортированный набор данных на диск. Затем мы получаем следующую часть данных, сортируя данные во вре- мя получения, и, снова достигнув лимита памяти, сохраняем следующий от- сортированный набор данных на диск, и так далее, пока мы не обработаем весь входной поток данных. В этот момент у нас есть несколько отсортиро- ванных наборов данных на диске и мы должны выполнить их слияние в еди- ный набор данных. Сортировка является сортировкой в один проход, если у нас достаточно памяти, чтобы получить часть из каждого отсортированно- го набора за один раз. Если вы хотите понять, как выглядит шаг слияния, за- пишите на видео раздачу колоды карт, а затем посмотрите запись в обратном направлении. о Сортировка в несколько проходов сначала выполняется так же, как сорти- ровка в один проход. Отличия начинаются, когда после обработки и сохра- нения всех данных на диск обнаруживается, что памяти недостаточно для обработки части из каждого отсортированного набора данных на диске за один раз. В этом случае вам необходимо выполнить слияние нескольких по- токов, сохранить один поток большего размера обратно на диск, выполнить слияние еще нескольких потоков и сохранить еще один поток большего раз- мера обратно на диск. После того как вы обработаете все начальные потоки, вы можете начать получать обратно с диска и выполнять слияние несколь- ких потоков большего размера — и, возможно, сохраняя потоки еще больше-
Введение 393 го размера обратно на диск. Наконец, вы сможете выполнить слияние всех (возможно, очень больших) потоков, оставшихся на диске. Количество раз, которое вы должны получать данные обратно с диска, называется количест- вом проходов при выполнении слияния. Давайте создадим пример, чтобы понять, что происходит при выполнении простой операции сортировки. Мы начнем с создания таблицы в один миллион записей (чтобы быть точным, 1 048 576 записей — ближайшая степень двойки) с псевдослучайными данными фиксированной длины и выполним простой за- прос, который отсортирует содержимое этой таблицы. Как обычно, в моей демонстрационной среде используются блоки размером 8 Кбайт, локально управляемые табличные пространства, однородные экстенты размером в 1 Мбайт, ручное управление размером сегмента, выключенная сис- темная статистика (CPU costing) и, как я уже указал в начале главы, ручная на- стройка workarea_ size_policy. Кроме того, по причинам, которые станут по- нятными позднее, я замечу, что я использую 32-разрядную операционную систе- му для этой серии тестов (сценарий sort_demo_01.sql в онлайн-хранилище кода). Моя стратегия рассмотрения сортировки будет абсолютно отличаться от подхода, который я использую в остальной части этой книги. Вместо анализа автотрассировки или деталей плана выполнения я собираюсь использовать раз- личные события трассировки и динамические представления производительно- сти, чтобы показать, что происходит. execute dbms_random.seed(0) create table tl as with generator as ( select --+ materialize substr(dbms_random.string('U',4),1,4) sortcode from all_objects where rownum <= 5000 ) select /*+ ordered use_nl(v2) */ substr(v2,sortcode,1,4) || substr(vl.sortcode,1,2) sortcode, substr(vl.sortcode,2,2) v2, substr(v2.sortcode,2,3) v3 from generator vl, generator v2 where rownum <= 1048576 -- Сбор статистики с помощью dbms_stats alter session set workarea_size_policy = manual; alter session set sort_area_size = 1048576; alter session set events '10032 trace name context forever'; alter session set events '10033 trace name context forever';
394 Глава 13. Соединения сортировки и слияния alter session set events '10046 trace name context forever, level 8'; alter session set events '10053 trace name context forever': select sortcode from tl order by sortcode Кроме настроек трассировки, которые я указал в предыдущем фрагменте кода, моя тестовая программа также получает статистику, относящуюся к фай- ловым операциям ввода-вывода, активности сессии и состояниям ожидания сессии в течение выполнения запроса, а также некоторую статистику о таблице tl. С этими данными мы можем выполнить перекрестное сравнение различной информации для получения некоторых интересных выводов. После выполнения процедуры dbms_stats.gather_table_stats я могу проверить представление user_tables, чтобы убедиться, что таблица занимает 2573 блока и содержит 1 048 576 записей со средней длиной записи из avg_ row_len, равной 14 байтов, и что значение avg_col_len для столбца sortcode равно 7 байт (столбцы v2 и v3 нужны для того, чтобы избежать появления странных граничных явлений). Эти 7 байтов включают 1 байт, в котором хра- нится сама длина столбца, поэтому мы можем определить, что итоговый объем отсортированных данных должен быть равен 6 Мбайт и что выделенная мною память в 1 Мбайт недостаточна для выполнения сортировки в памяти (опти- мальной сортировки). Кроме этого, есть подтверждающая это статистика, получаемая из отчетов на основе vjmystat и vjtempstat, соответственно, при выполнении тестового примера в версии 9.2.0.6 (сценарии snap_myst.sql и snap_ts.sql создают пакеты для получения и форматирования этих данных): Name Value consistent gets 2758 physical reads direct 1565 physical writes direct 1565 table scans (long tables) 1 table scan rows gotten 1 048 576 table scan blocks gotten 2753 sorts (disk) 1 sorts (rows) 1 048 576 T/S Reads Blocks Avg Csecs Writes Blocks Avg Csecs TEMP 993 1565 2.420 269 1565 .710 В статистике сессии можно видеть, что мы выполняем одно большое таблич- ное сканирование и одну сортировку на диске. Количество записей в таблич- ном сканировании равно 1 048 576, как мы и ожидали. Значение consistent gets (2758) вполне согласуется как с размером таблицы (2753 блока), так и с количеством блоков таблицы, по которым было выполнено табличное сканиро- вание. Наконец, количество прямых физических операций чтения и прямых физических операций записи согласуется с количеством прочитанных и сохра- ненных блоков, полученным из v$tempstat.
Введение 395 ПРИМЕЧАНИЕ Файловая статистика показывает количество запросов на чтение и количество прочитанных блоков как две разные цифры, аналогично количеству запросов на запись и количеству сохраненных бло- ков, в то время как статистика сессии показывает количество сохраненных и прочитанных блоков с названиями, из-за которых можно подумать, что это количество запросов ввода-вывода. СТАТИСТИКА СЕССИИ Многие используют представление v$sessstat для проверки статистики определенной сессии. Одна- ко если сессия, за которой вы хотите понаблюдать — ваша собственная, то для этого существует бо- лее точное представление — v$mystat, Которое выводит статистику только для текущей сессии. Для удобства я обычно создаю представление, называемое v$my_stats, в схеме sys, в котором про- изводится соединение v$mystat с v$statname (см. сценарии c_mystats.sqi и snapjnyst.sqi в он- лайн-хранилище кода). Событие 10032 показывает статистику активности сортировки, событие 10033 — подробную информацию о вводе-выводе, событие 10046 является рас- ширенной трассировкой, которую я включаю на уровне 8 для сохранения со- стояний ожидания (wait states, «eight» для «wait», чтобы лучше запомнить), а событие 10053 является трассировкой стоимостного оптимизатора. Давайте посмотрим, как результаты некоторых из этих событий согласуются с получен- ной нами ранее статистикой. Событие 10033 показывает следующее: *** 2005-01-20 09:35:02.666 Recording run at 406189 for 62 blocks Recording run at 4061c8 for 62 blocks Recording run at 406206 for 62 blocks 19 similar lines deleted Recording run at 4066de for 62 blocks Recording run at 40671c for 62 blocks Recording run at 40675a for 62 blocks Recording run at 406798 for 15 blocks Merging run at 406798 for 15 blocks Merging run at 406189 for 62 blocks Merging run at 4061c8 for 62 blocks 19 similar lines deleted Merging run at 4066a0 for 62 blocks Merging run at 4066de for 62 blocks Merging run at 40671c for 62 blocks Merging run at 40675a for 62 blocks Total number of blocks to read: 1565 blocks По мере того, как Oracle сохраняет данные на диск в виде отсортированных наборов данных, он записывает объем сохраненных на диск данных и адреса блоков базы данных во временном табличном пространстве, с которых начина- ются эти отсортированные наборы данных. Мы могли бы просуммировать ко- личества сохраненных и прочитанных блоков, но для нашего удобства в конце списка выводится общее количество блоков для этой сортировки в один про- ход — и это количество согласуется с количеством прямых операций чтения
396 Глава 13. Соединения сортировки и слияния и записи, а также с количеством сохраненных и прочитанных блоков в vjmystat и v$tempstat, соответственно. Шестнадцатеричные числа, такие как 4061с8, являются адресами блоков, с которых начинаются отсортированные наборы данных. Поэтому мы можем вывести содержимое этих блоков, чтобы посмотреть, что в них. Сконвертируем шестнадцатеричное число в десятичное (а шестнадцатеричное число 0х4061с8 — это десятичное 4 219 336), а затем используем функции data_ block_address_file и data_block_address_block из пакета dbms_utility для получения номеров файла и блока (запомните, что в системах с множест- вом табличных пространств и файлов номер файла является абсолютным номе- ром файла, а не номером файла в табличном пространстве). В данном случае блок 4 219 336 соответствует блоку 25 032 временного файла 1. Теперь мы мо- жем выполнить команду вывода содержимого блока и посмотреть файл трасси- ровки, как показано ниже: alter system dump tempfile 1 block min 25032 block max 25032; Start dump data blocks tsn: 2 file#: 1 minblk 25032 maxblk 25032 buffer tsn: 2 rdba: 0X004061C8 (1/25032) sen: 0x0000.031652d5 seq: 0x01 fig: 0x0c tail: 0x52d50801 frmt: 0x02 chkval: 0x65b6 type: 0x08=unknown Dump of memory from 0x10593014 to 0X10594FFC 10593010 004061C8 0000003F 004061C9 [.a@.?....a@.] 10593020 00000000 00000041 00001FE8 00000000 [....A.............] 10593030 000002A6 00000008 41410006 45425242 [.............AABRBE] 10593040 00000008 41410006 584A5242 00000008 [........AABRJX....] 10593050 41410006 484F5242 00000008 41410006 [..AABROH.........AA] 10593060 45535242 00000008 41410006 42555242 [BRSE.........AABRUB] Файл трассировки не очень информативен, но можно заметить одну деталь: в секции справа от дампа данных, в пятой строке сверху, вы можете видеть шесть точек между AABROH и AABRSE. При сортировке 6-байтных строк сущест- вуют дополнительные расходы в 6 байт на строку в дампе данных. В этом примере мы можем объяснить дополнительные расходы, посмотрев на AABRBE на две строки выше. После изменения порядка байтов становится по- нятно, что это соответствует числу 00000008 41410006 45425242 на той же строке. В результате: 6 байт на длину строки 00000008 2 байта на длину столбца 0006 6 байт данных 414142524245 -- после изменения порядка байтов Хотя это и не видно в этом примере, строки также дополнены нулями, чтобы быть кратными 4 байтам. Форматирование данных, сохраняемых на диск во время выполнения сортировки, означает, что общий сохраняемый объем может быть несколько больше, чем вы ожидаете, — мы сохранили 1565 блоков, что со- ставляет более 12 Мбайт, в то время как сортировалось только 6 Мбайт данных. Дополнительные расходы достаточно большие в данном случае, потому что мы сортируем очень короткие записи. Но возникает еще один вопрос об отсортированных наборах данных, сохра- няемых на диск. Почему они имеют длину 62 блока, когда размер блока — 8 Кбайт, а значение sort_area_si ze равно 1 Мбайт (128 блоков) — ведь Oracle
Введение 397 может отсортировать больше 62 блоков перед тем, как кончится доступная па- мять и придется сохранить данные на диск? ВЫБОР РАЗМЕРА ВРЕМЕННЫХ ЭКСТЕНТОВ Наверное, вы долгие годы помните этот совет: размер экстента во временном табличном простран- стве нужно устанавливать кратным значению sort_area_size (иногда советуется добавить еще один блок или два), чтобы Oracle сохранял на диск данные, кратные значению sort_area_size. Я всегда ду- мал, что это попытка быть слишком точным, и только когда я узнал о событии 10033, я понял, на- сколько искусственным был этот совет. Если вы используете правильные временные табличные пространства и глобальные временные таблицы (global temporary tables, GTTs), то, возможно, характерное использование глобальных вре- менных таблиц должно диктовать однородный размер экстента временных табличных пространств. Помните, что каждый пользователь глобальной временной таблицы получает один экстент на свою копию таблицы и один экстент на каждый из ее индексов. Вы можете захотеть, чтобы экстенты были небольшими, если у вас большое количество пользователей, одновременно работающих с глобаль- ными временными таблицами. Перед решением проблемы «пропавшего» пространства используем метод проб и ошибок для определения, насколько большим должно быть значение sort_area_si ze, чтобы получить оптимальную сортировку — сортировку в па- мяти (см. сценарий sort_demo_01a.sql в онлайн-хранилище кода). В табл. 13.1 показаны настройки памяти, размеры отсортированных наборов данных на дис- ке и (по причинам, которые далее станут понятными) время процессора, потра- ченное при различных значениях sort_area_size. Таблица 13.1. Сравнение изменения значения sort_area_size с использованием ресурсов sort_area_size Размеры отсортированных наборов данных, кроме последнего Количество отсортированных наборов данных Количество : записанных блоков Время работы процессора в секундах 1 Мбайт / 128 блоков 62 блока 26 1565 4,30 2 Мбайт / 256 блоков 122-123 блока 13 1552 4,44 4 Мбайт / 512 блока 241-245 блоков 7 1550 5,09 8 Мбайт / 1024 блока 484-492 блока 4 1549 5,71 12 Мбайт/ 1540 блоков 725-748 блоков 3 1549 6,00 16 Мбайт / 2048 блоков 970 и 577 блоков 2 1548 6,02 25,5 Мбайт/ 3264 блока 0 6,21 Итак, для сортировки набора данных в 6 Мбайт нам требуется выделить 25,5 Мбайт памяти для того, чтобы выполнить сортировку в памяти. Кроме
398 Глава 13. Соединения сортировки и слияния того, используемые нами ресурсы процессора растут при увеличении использо- вания большего объема памяти. Говорит ли нам это что-нибудь о том, как Oracle выполняет сортировку? Использование памяти Похоже, механизм сортировки в Oracle работает на основе бинарного дерева вставки {binary insertion tree). Это значит, что Oracle эффективно загружает ваши данные в простой список в памяти, создавая индекс в памяти по этим дан- ным. Значимость слова «бинарный» состоит в том, что, по моему мнению, индекс является бинарным деревом. Это значит, что каждый узел в дереве имеет не бо- лее двух дочерних элементов. Когда читается новая запись данных, Oracle про- ходит по дереву, используя простой механизм двоичного поиска (binary chop mechanism) для определения, какому узлу принадлежит запись. В нижней части дерева есть только два варианта для этой ситуации: или есть свободное место в узле, которому принадлежат данные, или узел заполнен. Если узел заполнен, Oracle должен добавить новый узел, возможно, разделив текущий узел и произведя разделения выше по дереву настолько, насколько это необходимо. Если эта гипотеза верна, то дерево в результате всегда будет сба- лансированным по высоте — это значит, что не будет участков, ответвляющих- ся от основного тела дерева — с высотой, которая всегда примерно равна log2 (количество записей для сортировки). Например, с восемью записями, за- гружаемыми в том порядке, в результате которого появляется идеальное дере- во, мы получим бинарное дерево с высотой, равной трем (log2 8 = 3). АЛГОРИТМ СОРТИРОВКИ В ORACLE Я бы хотел особо упомянуть Ричмонда Ши (Richmond Shee), одного из авторов книги «Oracle Wait Interface: A Practical Guide to Oracle Performance Diagnostics and Tuning», за его очень важные заме- чания (см. его статью с конференции IOUG-A 2004 года, «If your memory serves you right», о том, как Oracle выполняет сортировку). Пока я не увидел его статью, я думал, что Oracle загружает данные в память и затем использует один из стандартных алгоритмов для выполнения сортировки. Я не догадался, что Oracle может ис- пользовать механизм дерева вставки для выполнения сортировки, и чтение его статьи стало для меня просто откровением. На рис. 13.1 показана простая схема того, как Oracle использует память для выполнения сортировки. Обратите внимание, что я нарисовал дерево и отдель- ный стек данных. Я не думаю, что Oracle хранит значения записей прямо в де- реве, так как код и структуры памяти будут намного яснее, если данные будут храниться отдельно, а листовые узлы дерева будут хранить только указатели на соответствующие записи. Я также упростил диаграмму, показывая все дерево в одном блоке памяти, а весь стек данных — в другом. На самом деле Oracle выделяет память для сор- тировки понемногу по мере чтения данных, так что дерево и данные будут раз- биты на множество небольших блоков, распределенных по всей доступной па- мяти.
Введение 399 Рис. 13.1. Использование памяти во время сортировки Когда Oracle закончил сортировку или когда он должен сохранить отсорти- рованные данные на диск, он просто проходит по дереву, перемещаясь на соот- ветствующие данные, чтобы выдать данные в нужном порядке. Эта стратегия сортировки объясняет следующие неожиданные требования к памяти. о Я думаю, что каждый узел дерева является набором трех указателей (роди- тельский элемент, левый дочерний элемент, правый дочерний элемент). Для 32-разрядной операционной системы указатель будет состоять из 32 бит, то есть из 4 байт, так что каждый узел потребует 12 байт. о Хотя мои элементы данных в своем исходном виде имеют длину только 6 байт, когда они форматируются таким образом, как мы видели в содержи- мом блока, размер каждого вырастает до 12 байт. Кроме этого мы должны выделить по 12 байт на узел, а общее количество узлов в полностью запол- ненном бинарном дереве только на единицу меньше количества элементов — обратите внимание, что на диаграмме показаны восемь элементов данных и семь узлов дерева. о В результате наш исходный элемент данных увеличивается в размере с 6 байт до 24 — неудивительно, что мы не сможем выполнить сортировку полностью в памяти, не выделив на нее как минимум 25,5 Мбайт памяти плюс дополни- тельная память на буферы ввода-вывода. Конечно, при этом существует важный побочный эффект. Я работаю на 32-разрядной системе. Что произойдет, если я буду работать на 64-разрядной системе? Указатели удвоятся в размере. Дополнительные 12 байт на запись из-за узлов превратятся в дополнительные 24 байта на запись. Если вы работае- те на 64-разрядной системе, то сортировка в коде примера не выполнится пол- ностью в памяти, если только вы не увеличите значение sort_area_size при- мерно до 38 Мбайт. Использование ресурсов процессора Исторически всегда существовало утверждение, что вы можете повысить про- изводительность сортировки, увеличив значение sort_area_size, чтобы сор- тировка могла выполниться в памяти без сохранения части данных на диск. Но я только что продемонстрировал, что увеличение sort_area_si ze увеличивает использование ресурсов процессора — что, похоже, противоречит устной тради- ции. Это также может быть объяснено с помощью алгоритма бинарного дерева вставки.
400 Глава 13. Соединения сортировки и слияния Перед тем, как показать это противоречие, давайте вернемся к другой из включенных мною трассировок — событию 10032 — и посмотрим на информа- цию из этого события. После выполнения того же теста с начальным значением в 1 Мбайт для sort_area_size полный файл трассировки 10032 будет выгля- деть следующим образом: Sort Parameters sort_area_size 1048576 sort area retained size 1048576 sort multiblock read count 2 max intermediate merge width 29 Sort Statistics Initial runs 26 Number of merges 1 Input records 1048576 Output records 1048576 Disk blocks 1st pass 1565 Total disk blocks used 1567 Total number of comparisons performed 19863631 Comparisons performed by in-memory sort 14869919 Comparisons performed during merge 4993712 Temp segments allocated 1 Extents allocated 13 Run Directory Statistics Run directory block reads (buffer cache) 27 Block pins (for run directory) 1 Block repins (for run directory) 26 Ml 1 v. к* L 1 ILL jLuLIjLILj Write slot size 49152 Write slots used during in-memory sort 2 Number of direct writes 268 Num blocks written (with direct write) 1565 Block pins (for sort records) 1565 Cached block repins (for sort records) 25 -— Direct Read Statistics Size of read slots for output 16384 Number of read slots for output 64 Number of direct sync reads 435 Number of blocks read synchronously 461 Number of direct async reads 558 Number of blocks read asynchronously 1104 Видно, что файл трассировки начинается с перечисления важных парамет- ров, относящихся к сортировке (sort_area_size, sort_area_retained_s1ze и sort_multi block_read_count), а также с производного значения максималь- ной ширины промежуточного слияния (max Intermediate merge width). Это значение является количеством отсортированных наборов данных, которые Ora- cle может получить обратно с диска и по которым может выполнить слияние одновременно (после чего сохранить данные обратно на диск в виде одного на- бора данных большего размера), если объем данных, который нужно отсортиро- вать, приводит к сортировке в несколько проходов. Вторая часть файла трассировки, секция Sort Statistics, на самом деле выводится дважды — возможно, первый раз, когда Oracle заканчивает создание
Введение 401 результирующего набора данных, и второй раз при закрытии курсора. Я не хочу подробно рассматривать весь отчет, но обратите внимание, что значение Initi- al runs (26) соответствует количеству строк Recording run at mmmmm for nn blocks в трассировке 10033, а значение Disk blocks 1st pass figure (1565) также соответствует строке Total number of blocks to read: 1565 blocks в трассировке 10033. Количество слияний равно единице — это хорошее значе- ние и идеал для очень больших сортировок, которые не могут полностью вы- полниться в памяти. Количество слияний всегда равно единице, если значение Initial runs не превышает максимальной ширины промежуточного слияния, и статистика сессии сохраняет эту операцию как операцию рабочей области в один проход (на самом деле есть вероятность, что количество слияний будет равно единице и в том случае, когда количество начальных отсортированных наборов данных — initial runs — превышает максимальную ширину проме- жуточного слияния, что мы и увидим в следующем разделе, посвященном pga_aggregate_target). Мы видим количество входных записей (1 048 576), но более интересной цифрой является количество сравнений: около 20 000 000 в общей сложности, 15 000 000 во время сортировки в памяти и 5 000 000 во время выполнения слияния с диска. Эти цифры меняются в зависимости от входных данных и раз- мера памяти, но для фиксированного набора данных имеет смысл выполнить несколько примеров, чтобы увидеть, есть ли какая-либо зависимость между цифрами (здесь поможет сценарий sort_demo_01a.sql в онлайн-хранилище кода). В табл. 13.2 показаны несколько результатов — в последних двух столбцах показаны результаты, полученные из предыдущих столбцов. Таблица 13.2. Как значение sort_area_size влияет на количество требуемых сравнений sort_area_size Количество сравнений в памяти Количество начальных отсортирован- ных наборов данных Количество сравнений при выполнении слияния Количество записей * 1од2 (количество начальных отсортированных наборов данных) Общее количество сравнений 1 Мбайт 14 869 919 26 4 993 712 4 929 000 19 863 631 2 Мбайт 15 969 150 13 3 946 598 3 880 000 19 915 748 4 Мбайт 16 984 029 7 2 981 434 2 944 000 19 965 463 8 Мбайт 17 967 788 4 2 097 141 2 097 152 20 064 929 12 Мбайт 18 564 704 3 1 604 693 1 662 000 20 169 397 16 Мбайт 18 884 122 2 1 048 573 1 048 576 19 932 695 24 Мбайт 19 622 960 2 1 048 555 1 048 576 20 671 515 25,5 Мбайт 19 906 600 0 0 Нет данных 19 906 600 Из таблицы видно, что количество выполняемых сравнений во время слия- ния с диска достаточно близко к количеству записей * log2 (количество отсорти- рованных наборов данных). Это говорит о том, что Oracle, скорее всего, хранит
402 Глава 13. Соединения сортировки и слияния небольшую структуру дерева, которая указывает на начало каждого отсортиро- ванного набора данных, который был загружен обратно с диска, и проходит вниз по дереву, чтобы найти, в каком отсортированном наборе данных находит- ся следующий самый маленький элемент данных (с другой стороны, вы можете представить это дерево в виде списка из первых элементов отсортированных наборов данных, из которого Oracle с помощью двоичного поиска выбирает наименьшее значение). Если вы вспомните, что в нашем тестовом примере 1 048 576 записей и что log2 1 048 576 = 20, то заметите, что количество сравнений для сортировки в па- мяти достаточно близко к произведению количество записей * log2 (количество за- писей). Полученная цифра не очень точна (точная цифра составляет 20 971 520), и в некоторых особых случаях результат может достаточно сильно отличать- ся — похоже, в коде используется оптимизация для отсортированных наборов данных. Именно коэффициент log2 (...), вместе с тремя указателями на одно значение данных, привел меня к мысли, что дерево вставки является бинарным деревом, но проявляющиеся иногда серьезные отклонения могут означать, что мое пред- положение неверно. СОРТИРОВКА «КУЧЕЙ» Возможно, что Oracle использует какую-либо форму сортировки «кучей» (heapsort), которая начи- нается с загрузки данных в частично упорядоченное дерево, называемое «кучей» (heap). Слово «куча» иногда означает нечто гораздо более структурированное, чем то, каким образом подростки хранят свои вещи. Затем сортировка «кучей» использует механизм перетасовки данных (shuffle- down mechanism) для реструктуризации «кучи» в полностью отсортированную структуру. Я исключил сортировку «кучей» как механизм сортировки в Oracle, потому что даже хотя количест- во используемых сравнений примерно ей соответствует, оптимальная реализация алгоритма сорти- ровки «кучей» требует только один указатель на запись, а не три, которые, судя по объему занятой памяти, использует Oracle. Что самое интересное, я слышал мнения, что второй выпуск 10д гораздо быстрее выполняет сорти- ровку, чем предыдущие версии и выпуски — возможно, что разработчики из Oracle все-таки измени- ли алгоритм и код теперь использует сортировку «кучей». В последнем столбце таблицы показано итоговое количество выполненных сравнений. Как видите, оно меняется незначительно при смещении от сорти- ровки в памяти к сортировке слиянием. Но в предыдущей таблице было пока- зано, что использование ресурсов процессора возросло при смещении от сорти- ровки с диска к сортировке в памяти. Как же такое может быть? Вряд ли затрачиваемое процессором время на сравнение элементов данных отличается в этих двух типов операции — но ресурсы процессора могут расхо- доваться на движение вверх и вниз по дереву, чтобы найти элементы для срав- нения! Когда мы делаем сортировку полностью в памяти, наше дерево получа- ется достаточно высоким (высота дерева равна log2 (количество записей)). Но когда сортируется небольшая часть данных (4 % в нашем самом маленьком тес- товом примере ранее) перед ее сохранением на диск, по каждому отдельному отсортированному набору данных создается гораздо меньшее дерево, так что время процессора, затрачиваемое на движение по дереву, становится гораздо
Введение 403 меньше. А итоговое слияние тоже является движением по очень короткому де- реву. Так что ресурсы процессора расходуются не на сравнения, а на движение по указателям, из которых состоит дерево. Важно понять, что если вашим узким местом является процессор, а подсис- тема ввода-вывода работает не с полной нагрузкой, то вы можете повысить про- изводительность сортировки больших наборов данных, переключившись с сор- тировки в памяти на сортировку в один проход, используя при этом минимально возможный объем памяти, достаточный для выполнения сортировок в один про- ход. Также важно помнить, что если вы не можете полностью выполнить сорти- ровку в памяти, то весь набор данных будет сохранен на диск в любом случае — у вас нет «небольшого дополнительного» преимущества от наличия «небольшо- го дополнительного» объема памяти, здесь все или ничего — так что указывайте для sort_area_s1ze наименьшее возможное значение, чтобы только сортиров- ка не превратилась в сортировку в несколько проходов (при указании автома- тической настройки workarea_size_poliсу эта стратегия, похоже, встроена в код времени выполнения). ПРИМЕЧАНИЕ В отличие от традиционных советов есть случаи, когда вы можете выполнять операции сортировки быстрее, уменьшив объем памяти и выполняя сортировку с диска. Самым ярким примером примене- ния этой стратегии была серия операций создания больших индексов, в результате чего время соз- дания индекса упало со 150 с до 85 с (со 140 с процессора до 75 с процессора), потому что я умень- шил значение sort_area_size с 500 Мбайт до 5 Мбайт. Однако это была система с очень быстрыми дисками и огромным кэшем между Oracle и дисками, поэтому подсистема ввода-вывода абсолютно не была нагружена. Параметр sort_area_retained_size Значение sort_area_retained_size также можно изменить, чтобы повлиять на выполнение сортировки. Значение по умолчанию равно 0. Это значит, что значение sort_area_retained_size должно само автоматически настраивать- ся для соответствия текущему значению sort_area_size. Установка значения sort_area_reta1ned_s1ze значения, отличного от 0, приводит к двум последствиям. Во-первых, это влияет на то, как Oracle выделя- ет и освобождает память. Во-вторых, это влияет на то, как Oracle использует временное табличное пространство. РАЗДЕЛЯЕМЫЕ СЕРВЕРЫ И Р_А_Т Для разделяемых серверов (shared servers, или многопоточных серверов — multithreaded servers, MTS, как их еще называют) в версии 91 память для операций массовой обработки данных в памяти, таких как сортировка, соединения хэширования и др., все еще учитывает параметры, относящиеся к ручной настройке workarea_size_policy, а не ограничения на основе значения pga_aggregate_target. Память, выделяемая в глобальной области пользователей (user global area, UGA) для этих операций, выделяется из разделяемой или системной глобальной области (shared or system global area, SGA), а не из глобальной области процесса (process global area, PGA). Этот подход меняется только в вер- сии 10g.
404 Глава 13. Соединения сортировки и слияния Вернемся к сценарию sort_demo_01.sql и выполним два теста, перезапуская сессию SQL*Plus для каждого теста. Для первого теста установим значение sort_area_size равным 30 Мбайт и не будем менять значение sort_ area_retai ned_si ze; для второго теста оставим значение sort_area_si ze рав- ным 30 Мбайт и значение sort_area_retained_size равным 8 Мбайт (этот тест реализован в сценарии sort_demo_01a.sql в онлайн-хранилище кода). После каждого теста проверяйте количество операций ввода-вывода для временного файла и изменения в статистике сессии, относящейся к сортировке и использо- ванию памяти. В табл. 13.3 показаны результаты, которые я получил в версии 9.2.O.6. Таблица 13.3. Влияние значения sort_area_retained_size на ввод-вывод Статистика sort_area_retamed_size = 0 sort_area_retamed_size = 8 Мбайт session UGA memory max Session PGA memory max Physical writes direct sorts (disk) 25,5 Мбайт 8,3 Мбайт 25,8 Мбайт 28,5 Мбайт 0 1547 0 0 Наиболее важным значением в этой статистике является physical writes di rect — при ненулевом значении sort_area_retai ned_si ze Oracle сохраня- ет весь набор данных на диск перед возвратом результатов клиенту (что также может объяснить дополнительно использованный объем PGA-памяти — она нужна для буферов ввода-вывода, которые иначе не были бы использованы). Я включил в таблицу количество сортировок на диск, выведенных в v$sesstat, чтобы показать, что Oracle не считает это значение сортировкой на диск — сортировка была закончена перед тем, как данные были сохранены на диск. Вот что происходит, когда устанавливается значение sort_area_retai ned_ size. Первое выделение памяти происходит в UGA, но когда выделение памяти достигает ограничения, установленного в sort_area_retained_size, память начинает выделяться в PGA. Даже если сортировка полностью выполняется в памяти, полный отсортированный набор данных будет сохранен на диск, если память выделялась и в UGA, и в PGA. ТАК КОГДА ЖЕ ВЫДЕЛЯЕТСЯ ПАМЯТЬ? Важно помнить, что sort_area_size и sort_area_retained_size являются ограничениями на выделение памяти. Вы периодически можете слышать мнение, что сессия получает объем памяти, указанный в sort_area_size, в самом начале. Это не так. Память выделяется при необходимости для сортировки, и выделяется понемногу, а не сразу вся. Такая же ошибка делается и в отношении pga_aggregate_target. Была пара статей, в которых утвер- ждалось, что весь объем памяти, указанный в pga_aggregate_target, на самом деле выделяется эк- земпляру в самом начале. Это абсолютно не так. Во-первых, параметр просто является числом, ко- торое используется для целей учета (чтобы вы не превысили бюджет); во-вторых, память никогда не выделяется всему «экземпляру» — она выделяется отдельным процессам и высвобождается ими только по мере необходимости.
Введение 405 Как я уже указывал ранее в этой главе, при работе с разделяемыми серверами (MTS) UGA выделяется в SGA, поэтому память до объема, равного sort_area_ retained_size, будет выделяться из SGA — обычно из большого пула (large pool), хотя, если вы забыли указать значение для параметра large_pool_si ze, она будет выделена из разделяемого пула (shared pool). Дополнительный объем до значения (sort_area_size — sort_area_retained_size) будет выделен в PGA. Это значит, что он будет выделен в памяти процесса разделяемого сер- вера (то есть процесса, называемого ora_{SID}_sNNN — этот объем будет учтен кодом, который выполняет расчеты на основе pga_aggregate_target). Если значение sort_area_retained_size меньше значения sort_area_ size, то отсортированные данные будут сохранены на диск по окончании сор- тировки, дополнительная память, выделенная в PGA, станет доступной для других целей, а память в размере sort_area_retained_size (возможно, огра- ниченная 2 Мбайт, согласно недавним экспериментам) будет использована для получения сохраненных данных обратно с диска для дальнейшей обработки. Параметр pga_aggregate_target В начале этой главы я упомянул, что механизм сортировки не меняется, если вы используете ручную настройку workarea_size_policy и sort_area_size, а не новую автоматическую настройку workarea_size_policy и pga_aggre- gate_target. Пришло время продемонстрировать это. Используя автоматическую настройку workarea_size_poli су, администра- тор баз данных устанавливает для Oracle ограничение общего объема памяти, которое может быть использовано всеми процессами для операций массовой обработки данных в памяти, таких как сортировка, соединения хэширования, создание индексов и операции с битовыми индексами. Различные части кода ядра Oracle работают совместно для того, чтобы проверить текущее выделение памяти, избежать выделения избыточного объема памяти и высвободить па- мять, которая больше не требуется. Конечно, память, выделенная на операции PL/SQL (такие, как обработка массивом, array processing), находятся вне контроля этого механизма, хотя и та- кое выделение все-таки отслеживается для целей учета и влияет на объем па- мяти, доступный другим рабочим областям (у вас могут быть проблемы с сис- темами, в которых сессии выполняют соединение и отсоединяются очень быстро, потому что проверка ограничения происходит только раз в три секунды). Есть множество параметров (большинство из них скрытые), влияющие на детали операции — но есть только две важные детали, которые оказывают наи- большее влияние: по умолчанию любая рабочая область для последовательного выполнения ограничена максимум 5 % от значения pga_aggregate_target, а рабочая область для параллельного выполнения ограничена таким образом, что общая сумма всех подчиненных сессий (slaves), вовлеченных в эту опера- цию, составляет не более 30 % от значения pga_aggregate_target — с ограни- чением в 5 % для каждой подчиненной сессии. Это значит, что вы можете уви- деть действие ограничения в 30 % только в том случае, если у вас есть запросы со степенью параллелизма выше шести (так как 5x6 = 30).
406 Глава 13. Соединения сортировки и слияния Скрытый параметр _smm_max_size является ограничением последователь- ного выполнения, a _smm_px_max_size — параллельного. Еще один параметр, n_si ze, определяет наименьший объем выделяемой памяти, который сессия может получить для рабочей области. В версии 9.2.0.6 этот параметр, по- хоже, по умолчанию равен 0,1 % от значения pga_aggregate_target, с мини- мальным значением 128 Кбайт и максимальным 1 Мбайт. Так как в сессии мо- жет одновременно выполняться несколько операций массовой обработки дан- ных в памяти, есть еще один параметр, ограничивающий объем памяти для сессии — _pga_max_size со значением по умолчанию 200 Мбайт; вторым огра- ничением значения по умолчанию параметра _smm_max_size является то, что оно не может превышать половины значения параметра _pga_max_size (хотя мы и видели в главе 12, что соединения хэширования выполняют свои расчеты на основе удвоенного значения параметра _smm_max_si ze и их значения могут несколько превышать значение _smm_max_si ze во время выполнения). Мы можем понять, как Oracle наилучшим образом использует память, вы- полнив тот же код, как и раньше (см. сценарий sort_demo_01b.sql в онлайн-хра- нилище кода), но установив параметр workarea_size_policy в его значение по умолчанию, равное auto, со значением pga_aggregate_target, равным 200 Мбайт. В результате наш процесс имеет ограничение в 10 Мбайт (5 % от 200 Мбайт) на операцию сортировки, а мы знаем, что код тестового примера требует около 25,5 Мбайт на выполнение сортировки в памяти. После выполнения запроса мы видим в v$mystat, что значение UGA memory max увеличилось до 9,7 Мбайт, а значение PGA memory max увеличилось до 10,7 Мбайт. Такое впечатление, что сессия получила возможность выделить все 10 Мбайт на выполнение этой сортировки. Однако если мы посмотрим файлы трассировки 10032 и 10033, то увидим странную вещь (ниже показана важная часть этих файлов): Recording run at 404489 for 598 blocks Recording run at 4042e0 for 127 blocks Recording run at 40425f for 128 blocks Recording run at 4041df for 162 blocks Recording run at 404181 for 198 blocks Recording run at 403947 for 226 blocks Recording run at 403a29 for 111 blocks -— Sort Parameters --------------------------------- sort_area_size 3555328 sort_area_retai ned_si ze 3555328 sort_multiblock_read_count 31 max intermediate merge width 5 -— Sort Statistics ------------------------------ Initial runs 7 Number of merges 1 Самой очевидной аномалией является то, что Oracle показал значение s о г t_ area_si ze равным 3,5 Мбайт, хотя мы знаем, что потребность в памяти увели- чилась до 10 Мбайт. Также нужно обратить внимание на размеры отсортированных наборов дан- ных: размер первого составляет 598 блоков (около 4,5 Мбайт), размер осталь-
Введение 407 ных меньше — от 1 Мбайт до 1,7 Мбайт. Зная, сколько «дополнительной» памя- ти требуется Oracle для выполнения сортировки, мы понимаем, что первый отсортированный набор данных соответствует значению sort_area_si ze, равно- му 10 Мбайт, а остальные соответствуют значению sort_area_size от 2 Мбайт до примерно 3,5 Мбайт. Также обратите внимание, что размер sort_multiblock_read_count, рав- ный 31, значительно превышает значение 2, которое мы видели ранее в трасси- ровке 10032, с соответствующим уменьшением максимальной ширины проме- жуточного слияния (max intermediate merge width). Есть даже аномалия в максимальной ширине промежуточного слияния — хотя ее значение равно 5, в следующих нескольких строках трассировки 10032 говорится, что мы создали семь отсортированных наборов данных, но выполни- ли их слияние в один проход. В этом случае ключевым является слово «проме- жуточный». Механизм (или, возможно, только отчет) меняет свое поведение в зависимости от ручной или автоматической настройки workarea_si ze_poli су, но механизм времени выполнения имеет определенную степень свободы в фи- нальном проходе слияния операции сортировки — он может определить, что можно прочитать и выполнить слияние всех потоков за один проход, если это поможет избежать резервирования памяти для сохранения потоков обратно на диск. Вспомните мое замечание ранее, что количество проходов слияния может быть равно единице, даже если количество начальных отсортированных набо- ров данных превышает значение максимальной ширины промежуточного слия- ния. ПАРАМЕТР SORT_MULTIBLOCK_READ_COUNT Параметр sort_multiblock_read_count определяет количество блоков, которые процесс может прочи- тать из отсортированного набора данных за одну операцию чтения. Больший объем для единовре- менного чтения может привести к тому, что ваше аппаратное обеспечение будет выполнять слия- ние с лучшей производительностью, но, так как есть строгое ограничение на общий объем памяти для выполнения слияния, больший объем для единовременного чтения уменьшит количество отсор- тированных наборов данных, которые могут быть прочтены и по которым может быть выполнено слияние одновременно. Если вы не можете выполнить слияние всех отсортированных наборов дан- ных за один проход, вам придется выполнять слияние только нескольких отсортированных наборов данных за один раз, затем сохранять результаты обратно на диск, после чего выполнять слияние больших отсортированных наборов данных дополнительными проходами. Начиная с версии 9i, это называется в системной статистике «workarea executions — multipass» (выполнение слияния в не- сколько проходов), и это то, чего вы обычно стараетесь избежать. Исторически корпорация Oracle советовала не менять значение sort_multiblock_read_count, и значе- ние параметра обычно равнялось двум (или одному) блокам — даже когда объем доступной памяти очень большой и достигнуто верхнее ограничение максимальной ширины промежуточного слияния (678). Если вы работаете в версии 81 (или в версии 91 с ручной настройкой размера рабочей облас- ти), то вы можете обнаружить, что получили небольшое преимущество в производительности для очень больших сортировок, проверяя трассировку 10032 и настраивая значение параметра sort_ multiblock read count для определенной сессии или определенной команды. Странные противоречия в отчетах трассировок могут быть объяснены опти- мистическими, но экономичными алгоритмами, используемыми механизмом времени выполнения. Так как для параметра pga_aggregate_target было ус- тановлено значение 200 Мбайт в этом тесте, мой процесс имеет ограничение
408 Глава 13. Соединения сортировки и слияния в 10 Мбайт для выполнения сортировки. Так как во время выполнения моего теста больше ничего не выполняется, то код ядра позволяет моей сессии ис- пользовать весь объем вплоть до этого ограничения — проверяя представление v$pgastat (и, возможно, суммируя значения v$sql_workarea_acti ve) для срав- нения глобального значения с общим объемом выделенной на данный момент памяти в PGA. Поэтому операция сортировки начинается с sort_area_size, равного 10 Мбайт; но при первом проходе — когда 10 Мбайт полностью заполняются данными и их деревом вставки — становится ясно, что сортировка не может быть полно- стью выполнена в памяти. Сессия сохраняет первый отсортированный набор данных на диск и высвобождает память для учетного пула (accounting pool). Я думаю, что при этом выполняется некий расчет для определения того неболь- шого, но достаточного объема памяти, который позволяет выполнить сортиров- ку в один проход. В идеале требуется такой объем памяти, который достаточен для выполнения операции в один проход, но не настолько большой, чтобы, во-первых, стоимость используемых ресурсов процессора выросла без необхо- димости и, во-вторых, другие процессы, нуждающиеся в памяти, пострада- ли в результате использования нами излишнего объема памяти. Учитывая постоянный рост размеров отсортированных наборов данных, воз- можно, что сессия проверяет состояние глобального выделения памяти PGA при каждом их сохранении на диск. Также возможно, что в расчетах проверяет- ся количество сгенерированных отсортированных наборов данных и определя- ется объем памяти, необходимый для выполнения слияния за один раз с мини- мальными затратами на ввод-вывод. Есть одна важная деталь в трассировке 10032, которую можно не заметить. В версии 8i секция Sort Parameters файла трассировки выводится до начала генерации отсортированных наборов данных, а в версии 9i эта секция выводит- ся после того, как закончено первое сохранение данных на диск. Данные в фай- ле трассировки отражают состояние PGA сразу после того, как последний из отсортированных наборов данных был сохранен на диск; это делается для того, чтобы показать, что новый код имеет возможность воспользоваться преимуще- ством выделения больших объемов памяти, используя гораздо больший размер операций ввода-вывода во время выполнения слияния (минимальный и макси- мальный размеры операций ввода-вывода теперь устанавливаются скрытыми параметрами _smm_auto_mi n_io_si ze и _smm_auto_max_io_size). Чтобы оп- тимизатор ни делал в расчетах в версии 9i, он не использует значение времени выполнения параметра sort mulitblock read count, указанное в трассиров- ке 10032). Реальный ввод-вывод Последнее, что нужно рассмотреть перед обсуждением того, как оптимизатор получает оценку стоимости сортировки, является реальное влияние ввода-вы- вода. Вернувшись к трассировке 10032 исходного теста со значением sort_ area_size, равным 1 Мбайт, видно, что на диск были сохранены 1565 блоков
Введение 409 в отсортированных наборах данных примерно по 62 блока в каждом, а затем они были получены обратно с диска для выполнения слияния. С другой сторо- ны, хотя результаты в v$tempstat показывают то же количество сохраненных на диск и полученных обратно с диска блоков, количество запросов на сохране- ние на диск равно 269, а количество запросов на получение обратно с диска рав- но 993. Интересно проверить, какой на самом деле тип у операций записи и чте- ния, насколько они большие и можно ли произвести их настройку. В коде из исходного демонстрационного сценария одним из событий, кото- рые я включил, было событие 10046 (событие «расширенной трассировки»), включенное на уровне 8, чтобы я мог отследить появляющиеся состояния ожи- дания (wait states). Запустив tkprof на этом файле трассировки, я получил следующие результаты для основного запроса: Event waited on Times Max. Wait Total Waited ----------------------- waited --------------- --------------- direct path write 2 0.00 0.00 direct path read 435 0.59 15.00 Это не очень соответствует количеству операций чтений и записи для вре- менных файлов. Поэтому я внимательно исследовал сам файл трассировки. Следующие строки являются восемью последовательными строками из файла трассировки для типичного выполнения теста. Я добавил пару пустых строк, чтобы указать изменение функции: WAIT #1: nam='db file scattered read' ela= 33985 pl=5 p2=1847 p3=6 WAIT #1: nam='db file sequential read' ela= 246 pl=5 p2=1875 p3=l WAIT #1: nam='direct path write' WAIT #1: nam='direct path write' ela= 6 pl‘=201 p2=16799 p3=6 ela= 22 pl=201 p2=16805 p3=2 WAIT #1: WAIT #1: WAIT #1: WAIT #1: nam=’di rect nam='di rect nam='di rect nam='di rect path read' path read' path read' path read' ela= 293 pl=201 p2=16792 p3=2 ela= 20087 pl=201 p2=19206 p3=2 ela= 6655 pl=201 p2=19081 p3=2 ela= 3685 pl=201 p2=19144 p3=2 Первые две строки показывают чтение данных из таблицы — типичные мно- гоблочные чтения из файла данных 5 (pl = 5) при сканировании таблицы, с од- ной операцией чтения размером в один блок, чтобы прочитать последний блок таблицы. Следующие две строки показывают только состояния ожидания для опера- ций записи во временное табличное пространство (файл 201 в моей тестовой системе). В нескольких тестах сценария количество операций записи менялось очень незначительно — только в одном выполнении теста оно достигло макси- мума в четыре операции записи. Последняя операция записи в каждом тесте со- стояла из двух блоков, все предыдущие состояли из шести блоков. НУМЕРАЦИЯ ВРЕМЕННЫХ ФАЙЛОВ Нумерация временных файлов может показаться несколько странной. Если вы уберете из номера в файле трассировки значение параметра dbjiles, вы получите номер файла, указанный в v$tempfile. И наоборот, вы можете выполнить запрос к x$kcctf, чтобы в столбце tfafn увидеть абсолютные номе- ра временных файлов (Temporary File Absolute File Numbers).
410 Глава 13. Соединения сортировки и слияния Oracle может использовать форму асинхронного механизма записи для пря- мых операций записи (если операционная система поддерживает такую воз- можность), отправляя по несколько пакетов блоков подсистеме ввода-вывода, а затем проверяя, были ли эти пакеты сохранены на диск. Некоторые данные об этих операциях видны в трассировке 10032 для этого теста, в которой можно видеть статистику прямых операций записи и прямых операций чтения (ниже показана соответствующая часть трассировки). Я думаю, что цифры в Write slots used и Number of read slots показывают, сколько асинхронных вызовов может сделать Oracle перед тем, как проверить, что пакет был действительно сохранен на диск или прочтен с диска. Я добавил пару строк в этот листинг, ко- торых не было в первом тесте, потому что они имеют значение только во время операций в несколько проходов: -— Direct Write Statistics Write slot size 49152 Write slots used during in-memory sort 2 Write slots used during merge -- Только для операций в несколько проходов Number of direct writes 268 Num blocks written (with direct write) 1565 Waits for async i wri tes -- Нет в версии 9i -— Direct Read Statistics---------- Size of read slots for merge phase Number of read slots for merge phase Size of read slots for output Number of read slots for output Number of direct sync reads Number of blocks read synchronously Number of direct async reads Number of blocks read asynchronously -- Только для операций в несколько проходов -- Только для операций в несколько проходов 16384 64 435 461 558 1104 Обратите внимание, что количество прямых синхронных операций чтения (435) в статистике 10032 соответствует количеству состояний ожидания пря- мых операций чтения, показанному в файле трассировки 10046. Более того, об- щее количество операций чтения (435 + 558), показанное в трассировке 10032, соответствует количеству операций чтения, показанному в статистике файла v$tempstat (993). Жаль, что количество direct writes (прямых операций записи) в трасси- ровке 10032 (268) не совсем соответствует количеству операций записи, пока- занных в v$tempstat (269), но в трассировке в версии 9i отсутствует строка об асинхронных операциях записи, которая есть в трассировках версий 8i and 10g, так что, возможно, эта разница просто является следствием ошибки подсчетов где-то в коде, относящемся к асинхронным операциям. Вернемся к трассировке 10046: четыре последние строки трассировки пока- зывают, что Oracle получает блоки обратно с диска из сегмента сортировки, чтобы выполнить их слияние. Значение р2, показанное в этих четырех строках, является номером блока в файле, с которого происходит чтение, и показывает, что Oracle перемещается с места на место в файле; но также есть 26 последова-
Стоимость сортировки 411 тельных состояний ожидания операций чтения в начале списка, что соответст- вует 26 строкам с текстом Merging run at mmmm for nn blocks в трассировке 10033. Значение рЗ является количеством прочитанных блоков. После операций чтения в начале, состоящих из двух блоков (ограничение, задаваемое парамет- ром sort_multi block_read_count), все последующие операции чтения в фай- ле трассировки состоят из одного блока — и они оставляют пустые места в от- сортированных наборах данных, которые были перед этим сохранены на диск. Это, скорее всего, является результатом наличия множества слотов для чтения, доступных для запросов на асинхронные операции чтения. Учитывая асинхронные операции записи и чтения в трассировке 10032, не- удивительно, что наши цифры не соответствуют друг другу. На самом деле мы даже можем обнаружить, что, как показывает количество состояний ожидания операций чтения в трассировке 10046, иногда все заканчивается состоянием ожидания как асинхронных операций чтения, так и синхронных. Стоимость сортировки Зная, как работает сортировка, можно взглянуть на цифры, которые выдает оп- тимизатор для оценки стоимости сортировки, и попытаться получить формулу, объясняющую, как эти цифры были сгенерированы. Конечно, нужно учесть все следующие четыре варианта работы, если вы хотите получить полное представ- ление о работе оптимизатора: workarea_size_policy = manual Оценка стоимости использования ресурсов процессора выключена (стиль версии 81) workarea_size_policy = auto Оценка стоимости использования ресурсов процессора выключена (стиль версии 91 по умолчанию) workarea_size_policy = manual Оценка стоимости использования ресурсов процессора включена (общий стиль версии 91) workarea_size_policy = auto Оценка стоимости использования ресурсов процессора включена (стратегический стиль версии 91 и стиль версии 10g по умолчанию) Также нужно решить и общую проблему: есть ли какие-либо различия для sort (aggregate), что происходит при сортировке во время создания индекса; отличается ли как-то стоимость сортировки в соединении сортировки и слия- ния от стоимости простой сортировки? И наконец, есть еще особенности опера- ционных систем — учитывает ли механизм оценки стоимости каким-либо обра- зом разницу между 32- и 64-разрядными платформами, а также доступность асинхронного ввода-вывода? Прежде чем я расскажу, как оптимизатор рассчитывает стоимость сортиров- ки, я бы хотел привлечь ваше внимание к скрытому параметру _new_sort_ cost_estimate, который появился в версии 9i. Значение по умолчанию для этого параметра — true, в этом случае используется новый механизм оценки стоимости сортировки.
412 Глава 13. Соединения сортировки и слияния Определение алгоритмов оптимизатора через проверку результатов контро- лируемых экспериментов практически невозможно — особенно когда так много изменений между различными версиями и так много отклонений, которые мо- гут относиться к изменениям в версиях, могут быть ошибками или обработкой особых случаев. Я не утверждаю, что полностью понимаю механизм оценки стоимости сортировок и соединений слияния — но следующая информация мо- жет помочь вам в решении проблем. Трассировка 10053 Мы начнем с события трассировки, которое я включил в самом первом тесте — трассировки стоимостного оптимизатора, или трассировки 10053. С ручной на- стройкой workarea_size_policy, значением параметра sort_area_size, рав- ным 1 Мбайт, и выключенной оценкой стоимости ресурсов процессора важные секции файла трассировки выглядят следующим образом: SINGLE TABLE ACCESS PATH TABLE: Tl ORIG CDN: 1048576 ROUNDED CDN: 1048576 CMPTD CDN: 1048576 Access path: tsc Resc: 266 Resp: 266 BEST_CST: 266.00 PATH: 2 Degree: 1 Join order[1]: T1[T1]#0 ORDER BY sort SORT resource Sort statistics Sort width: 29 Area size: 712704 Max Area size: 712704 Degree: 1 Blocks to Sort: 2311 Row size: 18 Rows: 1048576 Initial runs: 27 Merge passes: 1 10 Cost / pass: 2711 Total 10 sort cost: 2511 Total CPU sort cost: 0 Total Temp space used: 33711000 Best so far: TABLE#: 0 CST: 2777 CDN: 1048576 BYTES: 7340032 Я включил секцию single-table access path в этот фрагмент, чтобы пока- зать стоимость полного табличного сканирования (266), выполненного Oracle для получения данных, которые нужно было отсортировать. В последней строке показана лучшая на тот момент стоимость, равная 2777, которая является суммой 266 (стоимость табличного сканирования) и 2511 (Total lOsortcost — стоимость общего ввода-вывода сортировки). Нам необ- ходимо понять, как оптимизатор получил значение 2511 для Total 10 sort cost. Перед тем, как углубиться в сложные вопросы, я бы хотел объяснить другие цифры, показанные во второй секции трассировки, по порядку. О Sort wi dth (ширина сортировки) — это то же самое, что и max intermediate sort width в трассировке 10032. О Area size (размер области) — это объем памяти, доступный для обработки данных. Значение параметра sort_area_si ze у нас было равно 1 Мбайт, так что указанный здесь объем кажется несколько маленьким. В расчетах в сти-
Стоимость сортировки 413 ле версии 8i оптимизатор выделяет до 10 % (значение, округляемое до раз- мера блока) от значения sort_area_si ze для буферов записи. Все равно ос- тается немалый объем, который непонятно куда делся — согласно моей тео- рии, этот объем выделяется под бинарное дерево вставки. Отнимите 10 % (ок- ругленные до следующего полного блока) от начального значения, затем от- нимите 25 % от оставшегося значения на дерево сортировки: 1 048 576 байт = = 128 блоков (по 8 Кбайт); отнимаем 13 блоков (10 %) и получаем 115 бло- ков; отнимаем 25 % и получаем 86,25 блоков, что при округлении до следую- щего целого блока дает 87 блоков — 712 704 байт, что мы и получили. Но об- ратите внимание, что эти 25 % не изменяются ни при изменении размера записи (что должно быть, если моя теория верна), ни при переходе от 32- разрядного Oracle к 64-разрядному. О Max Area size (максимальный размер области) — так как это файл трасси- ровки в версии 9i, то в Area size показан минимальный доступный для вы- деления объем, зависящий от установленного значения pga_aggregate_tar- get, а в Max area size показана максимальная область, которая может быть выделена сессии. Так как этот тестовый пример выполняется в стиле версии 8i, используя ручную настройку размера рабочей области, то размеры Area size и Max area size одинаковы. Когда два этих значения отличаются друг от друга, то, похоже, расчеты стоимости производятся на основе Max area size, а не на основе Area size. о Degree (степень параллелизма) — если вы включаете параллельное выпол- нение, то вы получите две отдельные, идентично структурированные секции файла трассировки, показывающие расчеты стоимости сортировки. В одной секции будет показана степень параллелизма, равная 1, во второй — степень параллелизма, установленная для запроса (которая может быть установлена с помощью подсказки, степени параллелизма, установленной для таблицы, или степени параллелизма, автоматически определяемой Oracle, если вы вклю- чили автоматическую настройку параллельного выполнения). О Blocks toSort (количество блоков для сортировки) — размер сортируемых данных в блоках. Получается как Row size х Rows / размер блока. Если вы включили параллелизм, то значение Blocks to Sort во второй секции (сек- ции параллелизма) будет равно значению из первой (последовательной) сек- ции, разделенному на степень параллелизма. О Row size (размер записи) — рассчитанный оптимизатором средний размер сортируемых записей. Учитывая пару небольших настроек и вариаций, этот размер получается из столбца avg_col_length представления user_tab_ columns и обычно равен (12 + sum(avg_col__length) + (п - 1)) (где п — ко- личество столбцов, которые нужно отсортировать). Фиксированное значе- ние 12 достаточно точно отражает стоимость узла дерева, но остальная часть формулы, похоже, неверна, потому что мы видели, что наши данные по 6 байт на столбец дополняются еще 2 байтами на столбец и 4 байтами на запись. Это еще одна ситуация, когда модель расчетов стоимости не совсем согласу- ется с работой времени выполнения.
414 Глава 13. Соединения сортировки и слияния РАЗЛИЧИЯ МЕЖДУ DBMS_STATS.GATHER_TABLE_STATS() И ANALYZE Различие между двумя этими методами сбора статистики состоит в том, что выдаваемое пакетом dbms_stats значение avg_col_len включает байт длины, а выдаваемое командой analyze — нет. Так как размер записи, используемый в расчете стоимости сортировки, использует sum(avg_col_len) из представления user_tab_columns, это значит, что стоимость сортировки будет несколько выше после вызова dbms_stats, чем если продолжать использовать команду analyze (эта разница также относится и к соединениям хэширования). Есть множество вариантов получения неверной оценки размера записи. Например, если вы исполь- зуете простую функцию по двум столбцам (скажем, substr(coll 11 со12,5,2)), то оптимизатор в расче- тах будет использовать сумму длин обоих столбцов, хотя «очевидно», что нужно использовать кон- станту 2. о Rows (количество записей) — это количество равно 1 048 576, что является рассчитанной (отфильтрованной) кардинальностью таблицы. Опять же, если вы включили параллелизм, то значение во второй секции (секции паралле- лизма) будет равно значению из первой (последовательной) секции, разде- ленному на степень параллелизма. Так как количество записей уменьшается, а значение Area si ze может оставаться неизменным, стоимость параллельной сортировки легко может оказаться гораздо меньше стоимости последовательной сортировки — и даже может оказаться меньше стоимости последовательной сортировки, деленной на степень параллелизма. О Initial runs (количество начальных отсортированных наборов данных) — рассчитанное оптимизатором количество отсортированных наборов данных, которые будут сохранены на диск. Вы можете получить это значение как ко- личество блоков для сортировки х размер блока / максимальный размер об- ласти (2311 х 8192/712 704 = 26,56). В этом примере оптимизатор получил правильный результат, используя странное значение размера области и не- правильное значение размера записи. Оба значения (и Blocks to Sort, и Area si ze) больше тех, которые на самом деле были получены во время выполне- ния. О Merge passes (количество проходов слияния) — к счастью, значение Initial runs меньше значения Sort width, поэтому оптимизатор решил, что можно выполнить слияние всех отсортированных наборов данных в один проход. Это определяет важность значения Merge passes. К сожалению, похоже, что это значение всегда равно как минимум единице — даже для сортировки, полностью выполняемой в памяти. Эта цифра отражает количество раз, ко- торое весь набор данных будет сохранен на диск и получен обратно с диска, если будет выполняться сортировка в несколько проходов. Эта цифра не рав- на количеству слияний, показываемых в трассировке 10032 — один проход слияния (из трассировки 10053) может потребовать выполнения множества слияний (из трассировки 10032). Представьте, что у вас 15 отсортированных наборов данных на диске, но вы можете получать обратно с диска только по три набора. В этом случае первый проход слияния потребует выполнения пяти (15/3) слияний и приведет к сохранению пяти отсортированных набо- ров данных большего размера обратно на диск.
Стоимость сортировки 415 О 10 Cost / pass (стоимость ввода-вывода на один проход) — стоимость вы- полнения одного прохода слияния (в дальнейшем я буду ссылаться на это значение как на 10 cost per pass, чтобы не возникало путаницы при указа- нии знака деления). Это стоимость, которая возникает при необходимости сохранения данных на диск и получения данных обратно с диска, когда сор- тировка не может быть полностью выполнена в памяти. Если производится операция рабочей области в один проход, то мы должны сохранить весь на- бор данных на диск один раз, а затем получить его обратно с диска один раз, чтобы выполнить его слияние. Если же мы не можем выполнить слияние всех данных за один проход, то мы должны сохранить их обратно на диск в виде отсортированных наборов данных большего размера в конце первого прохода слияния, а затем получить их обратно с диска для выполнения слия- ния этих больших наборов данных. Хотя и можно выполнить некоторую оп- тимизацию слияний в несколько проходов, мы вынуждены сохранять весь набор данных на диск каждый раз — поэтому мы должны знать, какова стои- мость дополнительных операций ввода-вывода на разовое сохранение дан- ных на диск, а затем умножить ее на количество проходов, которое требуется для выполнения полного слияния. Эта цифра — одна из тех, происхождение которых нам очень важно понять, и как раз на нее большое влияние оказы- вают изменения в версиях и изменения значений различных параметров. Параметр sort_multi block_read_count оказывает на нее влияние в версии 8i, но не в версии 9i, в которой, похоже, значение этого параметра игнориру- ется. О Total lOsortcost (общая стоимость ввода-вывода при выполнении сорти- ровки) — предположительно, это значение должно объединять стоимость ввода-вывода на один проход с количеством проходов — но до появления оценки стоимости использования ресурсов процессора это значение всегда было меньше, чем стоимость на один проход при выполнении слияния в один проход. Типичный результат равен (Blocks to Sort + 10 cost per pass x x Merge passes) / 2, как и в этом примере (и фиксированное значение 2 в этой формуле указано не из-за уменьшенного значения параметра _sort_ multi block_read_count). Когда включена оценка стоимости использования ресурсов процессора, стоимость обычно равна (Blocks to Sort + 10 cost per pass x Merge passes) — хотя есть особые граничные условия, при кото- рых опять появляется деление на 2. О Total CPU sort cost (общая стоимость использования ресурсов процессора для выполнения сортировки) — компонент стоимости, относящийся к ресур- сам процессора, который оценивается количеством операций процессора. Эти операции конвертируются в эквивалентные операции ввода-вывода относи- тельно небольшой стоимости далее в файле трассировки. О Total Temp space used (общий использованный объем временного про- странства) — в принципе, это объем временного пространства, которое тре- буется нам для выполнения сортировки. На самом деле мы использовали 1565 блоков (12,5 Мбайт), а не 32 Мбайт, как здесь указано. Хотя имейте
416 Глава 13. Соединения сортировки и слияния в виду, что по расчету оптимизатора требуется отсортировать 2311 блоков (18 Мбайт), что несколько уменьшает ошибку. Часть трассировки, показанная выше, получена в версии 9i с эмуляцией мо- дели версии 8i, но если мы переключимся в стратегическую конфигурацию вер- сии 9i (с включенной оценкой стоимости использования ресурсов процессора и автоматической настройкой workarea_size_policy), то увидим несколько значительных изменений в файле трассировки. В показанном ниже фрагменте трассировки значение параметра pga_ aggregate_target равно 200 Мбайт, что приводит к значению параметра _smm_ min_size, равному 204 Кбайт, и значению параметра _smm_max_size, равному 10 Мбайт (0,1 % и 5 % от pga_aggregate_target, соответственно). Я также ус- тановил системную статистику (используя процедуру dbms_stats. set_sys- tem_stats()) таким образом, чтобы расчет стоимости табличного сканирова- ния выполнялся так, как будто оценка стоимости использования ресурсов про- цессора выключена и значение параметра db_file_multiblock_read_count равно 8 (см. сценарий sort_demo_01b.sql в онлайн-хранилище кода). SINGLE TABLE ACCESS PATH TABLE: Tl ORIG CDN: 1048576 ROUNDED CDN: 1048576 CMPTD CDN: 1048576 Access path: tsc Resc: 283 Resp: 283 BEST.CST: 283.00 PATH: 2 Degree: 1 Join orderfl]: T1[T1]#0 ORDER BY sort SORT resource 5ort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 2311 Row size: 18 Rows: 1048576 Initial runs: 2 Merge passes: 1 10 Cost / pass: 580 Total 10 sort cost: 2891 Total CPU sort cost: 1011773433 Total Temp space used: 33711000 Best so far: TABLE#: 0 C5T: 3276 CDN: 1048576 BYTE5: 7340032 Final - All Rows Plan: JOIN ORDER: 1 C5T: 3276 CDN: 1048576 R5C: 3275 R5P: 3275 BYTE5: 7340032 I0-R5C: 3157 IO-RSP: 3157 CPU-RSC: 1181444018 CPU-RSP: 1181444018 Стоимость табличного сканирования в секции single-table access path увеличилась с 266 до 283. Хотя это явно и не показано в данном файле трасси- ровки, мы можем определить (посмотрев отдельную трассировку 10053 выпол- нения простой выборки из таблицы), что разница в 17 единиц — это рассчитан- ная стоимость ресурсов процессора на сканирование таблицы. Теперь взгляните на вторую секцию показанного выше фрагмента трасси- ровки: во-первых, видно, что значение Area size упало до 204 Кбайт, а значе- ние Max area size — до 10 Мбайт, как и ожидалось. Но подождите — когда мы установили значение sort_area_size, мы видели в этих столбцах около 67,5 % (75 % от 90 %) от этого значения, а теперь видим все 100 %. Не значит ли это, что буферы записи и дерево вставки теперь располагаются в другом месте, или что отчет просто теперь используется по-другому?
Сравнения 417 На самом деле если мы начнем изменять количество записей, используемое в тесте, то обнаружим, что количество начальных отсортированных наборов данных падает до одного, а количество проходов слияния до нуля, только когда Rows х Row size уменьшается до значения Area size (а не Max area size). Это говорит о том, что существует формула обработки особых случаев, которая про- гнозирует, что сортировка может быть полностью выполнена в памяти. Это так- же показывает, что оптимизатор ведет себя так, будто различные дополнитель- ные затраты памяти и буферы ввода-вывода не включаются в память, выделен- ную для сортировки. Также обратите внимание, что ширина сортировки равна 58; это значение при использовании ручной настройки workarea_si ze_policy вы бы получили при значении sort_area_s 1 ze, равном примерно 2 Мбайт. Со значением sort_ area_size, равным 204 Кбайт (скорее, 320 Кбайт, которое выводится как раз- мер области, равный примерно 204 Кбайт), ширина сортировки была бы равна 9, а начальное количество отсортированных наборов данных было бы равно 86, что привело бы к трем проходам слияния. Со значением sort_area_si ze, рав- ным 10 Мбайт (скорее, 15,1 Мбайт, которое выводится как размер области, рав- ный примерно 10 Мбайт), ширина сортировки была бы равна 426 с начальным количеством отсортированных наборов данных, равным двум. Несмотря на наши попытки подобрать такое значение pga_aggregate_ target, которое бы позволило эмулировать значение sort_area_size, равное 10 Мбайт, мы не можем получить цифры, которые бы дали возможность опреде- лить явную зависимость между двумя параметрами. В конце концов, вне зависи- мости от того, что мы видим в трассировке 10053, помните, что в исходной трас- сировке 10032 для этого примера значение sort_multiblock_read_count равно 31, а максимальная ширина промежуточного слияния равна 5. Предположения оптимизатора не очень точно моделируют реальную работу во время выполнения. Ясно, что не так просто смоделировать алгоритм, который использует стои- мостный оптимизатор для определения стоимости сортировки. Поэтому, преж- де чем начать выяснять, как оптимизатор получает 10 cost per pass (стои- мость операций ввода-вывода на один проход), я бы хотел выполнить еще несколько тестовых примеров и прокомментировать их результаты. Сравнения Сценарии, которые я использовал для сравнения стоимости сортировки в четы- рех различных средах, очень похожи на сценарии, которые я использовал в гла- ве 12. У меня есть пара сценариев в онлайн-хранилище кода — pat_dump.sql и sas_dump.sql, которые выполняют следующий запрос после установки pga_ aggregate_target или sort_area_size, соответственно: select sortcode from (select * from tl where rownum <= 300000) order by sortcode
418 Глава 13. Соединения сортировки и слияния Эти сценарии вызываются четырьмя другими сценариями: два вызывают pat_dump.sql и два — sas_dump.sql. Вызывающими сценариями являются pat_ cpujiarness.sql, pat_nocpu_harness.sql, sas_cpu_harness.sql и sas_nocpu_harness.sql. Как вы можете понять из названий, в двух из этих сценариев включена оценка стоимости использования ресурсов процессора и в двух — выключена. Сценарии, в которых включена оценка стоимости использования ресурсов процессора, используют следующий PL/SQL-код для имитации расчетов в сис- теме, в которой не включена оценка стоимости ресурсов процессора, когда зна- чение db_f i le_multi lblock_read_count равно восьми блокам: begin dbms_stats.set_system_stats('MBRC.6.59); dbms_stats.set_system_stats('MREADTIM'.10.001); dbms_stats.set_system_stats('SREADTIM'.10.000); dbms_stats.set_system_stats('CPUSPEED',1000); end; / Чтобы провести корректное сравнение традиционного механизма оценки стоимости на основе sort_area_size и оценки стоимости на основе pga_ aggregate_target, я предположил, что для этого подходит правило 5 % — на- пример, значение sort_area_size, равное 1 Мбайт, соответствует значению pga_aggregate_target, равному 20 Мбайт. Базовой моделью, которая проявилась во всех файлах трассировки 10053, была следующая. О Оценка стоимости использования ресурсов процессора выключена: Total 10 sort cost = (Blocks to sort + 10 cost per pass x Merge passes) / 2. О Оценка стоимости использования ресурсов процессора включена: Total 10 sort cost = (Blocks to sort + 10 cost per pass x Merge passes). Так как значение Blocks to sort (588) не менялось во всех тестах, общую стоимость легко будет получить, если мы поймем, что происходит со значением 10 cost per pass. В табл. 13.4 показаны некоторые интересные результаты; четыре цифры в каждом столбце соответствуют 10 cost per pass / Initial runs / Merge passes и Total 10 sort cost. Таблица 13.4. Сравнение стоимости в разных средах Общий объем памяти для сортировки S_A_S Оценка стоимости использования ресурсов процессора выключена S_A_S Оценка стоимости использования ресурсов процессора включена Р_А_Т Предполагается ограничение в 5 % Оценка стоимости использования ресурсов процессора выключена Р_А_Т Предполагается ограничение в 5 % Оценка стоимости использования ресурсов процессора включена 64 Кбайт 784/131/5 => 2254 148 / 131/ 5 => 1328 Нет данных Нет данных 128 Кбайт 784/56/4 => 1862 148 / 56 / 4 => 1180 Нет данных Нет данных 256 Кбайт 756 / 27 / 2 => 1050 148 / 27 / 2 =>884 Нет данных Нетданных 320 Кбайт 720/22/2 => 1014 148 / 22 / 2 =>884 Нет данных Нет данных
Сравнения 419 Общий объем памяти для сортировки S_A_S Оценка стоимости использования ресурсов процессора выключена S_A_S Оценка стоимости использования ресурсов процессора включена Р_А_Т Предполагается ограничение в 5% Оценка стоимости использования ресурсов процессора выключена Р_А_Т Предполагается ограничение в 5 % Оценка стоимости использования ресурсов процессора включена 384 Кбайт 696/18 / 2 =>990 148 / 18 / 2 => 884 Нет данных Нет данных 448 Кбайт 726/16 / 2 => 1020 148 / 16 / 2 => 884 Нет данных Нет данных 512 Кбайт 714/14/1 =>651 148/14/1 =>736 882 / 10 / 4 => 2058 148/10/4=> 1180 640 Кбайт 687/11/1 => 638 148/11/ 1 => 736 882/8/3 => 1617 148/8/3 => 1032 1024 Кбайт 693/8/1 => 643 148/8/1 => 736 735 / 5 / 2 => 1029 148/5/2 =>884 1536 Кбайт 686 / 5 / 1 => 637 148/5/1 => 736 756 / 4/1 => 672 148/4/1 =>736 2048 Кбайт 687/4/1 => 638 148/4/1 => 736 706/3/1 =>647 148/3/1 => 736 3072 Кбайт 686/3/1 =>637 148/3/1 =>736 699/2/ 1 =>644 148 / 2 / 1 => 736 4096 Кбайт 696 / 2/1 =>642 148/3/ 1 => 736 696 / 2 / 1 => 642 148/2/1 =>736 6976 Кбайт 678/1/1 => 633 0/1/0=>0 700 / 2/1 =>644 148/2/1 =>736 10 240 Кбайт 720/1/1 => 654 0/1/0=>0 687 / 2/1 =>638 148/2/1 =>736 15 360 Кбайт 686/ 1/1 =>637 0/1/0=>0 686/2/1 =>637 148/2/1 =>736 20 480 Кбайт 764/1/1 =>676 0/1/0=>0 683 / 2 / 1 => 636 148/2/1 =>736 30 720 Кбайт 692 / 1/ 1 => 640 0/1/0=>0 700/2/1 =>644 148/2/1 =>736 Существуют несколько интересных аномалий, и, как мы уже видели при рассмотрении оценки стоимости соединений хэширования, изменения в стои- мости при попытке изменения стратегии, возможно, даже более важны, чем точные знания о том, как рассчитывается определенная стоимость. Во-первых, я бы хотел обратить внимание на то, что когда вы хотите вместо sort_area_size использовать pga_aggregate_target, значения стоимости не совсем соответствуют друг другу, если вы ориентируетесь на значение 5 %, ко- торое считается ограничением размера рабочей области. На самом деле, если вы сравните первые несколько значений стоимости для этой сортировки в столбцах 2 и 4 (два столбца «Оценка стоимости использования ресурсов про- цессора выключена»), вы увидите, что значения pga_aggregate_target, ско- рее, будут соответствовать значениям sort_area_size, в предположении, что ограничение равно 1 %, а не 5 %. Во-вторых, я бы хотел обратить внимание на то, что, когда вы работаете с ручной настройкой рабочих областей и решаете включить оценку стоимости использования ресурсов процессора без переключения на автоматическую на- стройку рабочих областей, стоимость выполнения некоторых сортировок мо- жет значительно снизиться. На самом деле такое поведение кажется мне кор- рекцией в модели оценки стоимости — падение стоимости до нуля в модели происходит примерно в той точке, когда сортировка может быть полностью вы- полнена в памяти (к сожалению, модель не всегда реально показывает потреб- ность в памяти — но по крайней мере модель становится самосогласованной, если такое падение стоимости действительно происходит). В-третьих, есть странности в оценке стоимости, когда объем доступной па- мяти большой: у меня было около 2 Мбайт данных для сортировки (300 000 запи- сей по 6 байт каждая, если не учитывать дополнительные затраты на сортировку),
420 Глава 13. Соединения сортировки и слияния которые превращались примерно в 7 Мбайт, если учесть все дополнительные объемы и указатели. Но даже когда объем доступной памяти был равен 30 Мбайт, оптимизатор рассчитал большую стоимость для сортировки, кото- рая, как я знал, должна была полностью выполниться в памяти. Есть немало странностей в трех моделях из четырех, приводящих к тому, что рассчитывает- ся беспричинно высокая стоимость сортировок, которые во время выполнения полностью выполняются в памяти. Есть также еще пара вопросов, относящихся к оценке стоимости использова- ния ресурсов процессора, которые не показаны в этой таблице. Когда вы изме- няете значение MBRC (multiblock read count — количество многоблочных операций чтения), стоимость сортировки меняется — на стоимость операций ввода- выво- да влияет предполагаемый размер запросов на ввод-вывод, хотя и не линейно, насколько я могу судить. Когда вы изменяете mreadtim (время на типичную многоблочную операцию чтения в миллисекундах), стоимость сортировки меняется — в этом тестовом примере я установил минимальное значение mreadtim, то есть стоимость сортировки (особенно сортировки, полностью вы- полняемой в памяти) будет даже выше, если в вашей системе будет установле- но более реальное значение времени на операцию чтения. Также есть пара особых случаев — граничных условий — в системной стати- стике, из-за которых оптимизатор меняет свою стратегию. Например, если mreadtim/ sreadtim больше значения MBRC, то оптимизатор изменит формулу расчета стоимости сортировки. В одном тестовом примере я получил стоимость сортировки, равную 5236, когда значение mreadtim было равно 65 мс, и стои- мость сортировки, равную 2058, когда значение mreadtim стало равным 66 мс — не думаю, что вы ожидаете резкого падения стоимости сортировки при увели- чении времени выполнения дисковых операций (и это поведение не согласует- ся с тем, как рассчитывается стоимость табличного сканирования — в этом слу- чае слишком большое значение mreadtim не считается аномалией). Еще одной проявившейся аномалией оказалась проблематичность получе- ния оптимизатором правильной стоимости сортировки, полностью выполняе- мой в памяти, при использовании автоматической настройки workarea_size_ policy и pga_aggregate_target. Ниже показана часть файла трассировки 10053, когда я выполнил тестовый пример со значением pga_aggregate_ target, равным 2 Гбайт: ORDER BY sort SORT resource Sort statistics Sort width: 598 Area size: 1048576 Max Area size: 104857600 Degree: 1 Blocks to Sort: 588 Row size: 16 Rows: 300000 Initial runs: 2 Merge passes: 1 10 Cost / pass: 148 Total 10 sort cost: 736 Total CPU sort cost: 270405074 Total Temp space used: 7250000 Обратите внимание, что значение Area size равно всего лишь 1 Мбайт. По- хоже, это жесткое ограничение установлено параметром _smm_min_size, даже если механизм времени выполнения выделит гораздо больше памяти. Но это ограничение, приводящее к граничному условию при оценке стоимости сорти-
Сравнения 421 ровки, поэтому оптимизатор не рассчитывает стоимость этой сортировки как сортировки, которая будет полностью выполнена в памяти, потому что ей тре- буется как минимум 300 000 х 16 байт = 4,6 Мбайт, что превышает значение _smm_min_size. В этом примере я был вынужден ограничить количество записей в моем за- просе значением 65 536, чтобы оптимизатор рассчитал стоимость сортировки как сортировки, которая будет полностью выполнена в памяти (при этом стои- мость операций ввода-вывода на выполнение сортировки падает до нуля). Тот факт, что компонент стоимости сортировки, относящийся к вводу-выводу, мо- жет упасть до нуля при этой конфигурации pga_aggregate_target и оценке стоимости использования ресурсов процессора, конечно, является хорошей но- востью, потому что отражает реальность. При этом лучше включить оценку стоимости использования ресурсов процессора, если у вас OLTP-система, пото- му что ваш SQL-код, выполняющий операции над небольшими объемами дан- ных, скорее всего, будет приводить к рассчитанным и действительным опти- мальным сортировкам. То, что компонент стоимости, относящийся к вводу- выводу, падает до нуля только в том случае, когда рассчитанный объем данных становится ниже такого низкого ограничения, не очень хорошо. Это может за- ставить нас изменять значения скрытых параметров, чтобы оптимизатор начал давать более разумные оценки. Я считаю, что стратегически нужно включать оценку стоимости использова- ния ресурсов процессора и пользоваться преимуществом автоматической на- стройки workarea_size_policy. Но я уверен, что у вас могут возникнуть про- блемы с переходом на использование нового механизма и что вам потребуется провести тщательное регрессионное тестирование. Чтобы оказать минимальное влияние на текущие планы выполнения, в качестве начального значения для pga_aggregate_target лучше установить значение, примерно в 100 раз превы- шающее текущее значение sort_area_size (а не в 20 раз, как это подразумева- ется известным ограничением в 5 %, которое используется для определения размера рабочих областей во время выполнения). Но я бы посоветовал вам в коде сессии переключаться на ручную настройку размера рабочей области и указывать явные значения sort_area_si ze для важных пакетных задач. Во многих случаях изменение стоимости сортировки не приводит к измене- нию планов выполнения, потому что большая часть операций сортировки отно- сится к операциям ordeг by, group by или distinct, которые должны быть вы- полнены вне зависимости от стоимости (хотя и нужно следить за параметрами хэширования в выражениях group by и di stinet, которые появились в версии 10gR2). Хотя есть случаи, когда планы выполнения очень сильно меняются, на- пример, когда соединение слияния становится соединением хэширования или наоборот, из-за резкого (и беспричинного) изменения стоимости. Также вы можете обнаружить случаи, когда использование уменьшения уровней вло- женности запроса {subquery unnesting') или слияния комплексных представлений {complex view merging) внезапно меняется (или не меняется) из-за изменения стоимости операции distinct, которой даже нет в вашем SQL-коде.
422 Глава 13. Соединения сортировки и слияния Соединения слияния Наконец мы можем перейти к рассмотрению механизма и оценки стоимости соединения слияния и оценки стоимости этого соединения — обычно называе- мого соединением сортировки/слияния (sort/merge join), потому что это соедине- ние требует, чтобы оба входных потока были отсортированы по столбцам со- единения. Концепция механизма очень проста, и, как и в случае с соединением хэши- рования, все начинается с разложения соединения на два независимых запроса перед объединением отдельных результирующих наборов данных. о Получение первого набора данных с использованием любых необходимых предикатов доступа и фильтрации и сортировка этого набора данных по столб- цам соединения. о Получение второго набора данных с использованием любых необходимых предикатов доступа и фильтрации и сортировка этого набора данных по столб- цам соединения. о Для каждой записи в первом наборе данных находится стартовая точка во втором наборе данных, после чего второй набор данных сканируется, пока не будет найдена запись, для которой не выполняется соединение (в этой точке можно остановиться, потому что второй набор данных отсортирован). В главе 12 я указал, что соединение хэширования является обратным вло- женным циклом (back-to-front nested loop) в хэш-кластере одной таблицы. Про- чтите описание соединения слияния еще раз, и вы поймете, что это очень похо- же на соединение с использованием вложенных циклов между одной таблицей с индексом и другой таблицей с индексом (хотя отсортированные наборы дан- ных на самом деле не индексированы и Oracle может использовать двоичный поиск стартовой точки при каждом прохождении по второму набору данных). Как и в случае с соединением хэширования, соединение слияния разделяет за- прос на два отдельных запроса, которые выполняются для получения данных; в отличие от соединения хэширования, первый запрос не должен обязательно быть завершен перед тем, как начнет выполняться второй запрос. Механизм слияния В зависимости от вашей точки зрения, вы можете решить, что количество вари- антов механизма соединения слияния находится в пределах от одного до пяти. Определившись с количеством вариантов, вы должны увеличить их в два раза, чтобы учесть случаи, когда первый набор данных может быть получен уже в от- сортированном виде, например при наличии соответствующего индекса, и его не требуется получить полностью перед получением второго набора данных (см. сценарий no_sort.sql в онлайн-хранилище кода). Если вы считаете, что есть множество вариантов, которые нужно рассмот- реть, то, скорее всего, вы получите следующий список этих вариантов. о Один-к-одному. О Один-ко-многим с равенством (например, соединение «родитель — потомок»).
Соединения слияния 423 О Один-ко-многим с диапазоном (например, t2. dt between tl. dt - 3 and tl. dt + 3). о Многие-ко-многим с равенством. о Многие-ко-многим с диапазоном. Графически эти варианты после того, как наборы данных были отсортирова- ны, можно представить так, как показано на рис. 13.2. Один-к-многим с равенством Один-к-одному с равенством Один-к-многим с диапазоном Многие-ко-многим с равенством Многие-ко-многим с диапазоном Рис. 13.2. Варианты соединений слияния Но если вы хотите рассматривать соединение слияния как единую страте- гию, то можно сказать, что механизм всегда один и тот же: для каждой записи в первом потоке находится первая соответствующая запись во втором потоке и далее происходит сканирование второго потока по порядку, пока будут нахо- диться записи, соответствующие записям из первого потока. По рисункам видно, что первые два варианта проще, чем последние три — и эти последние три варианта не очень отличаются друг от друга. В первых двух вариантах механизм времени выполнения просто проходит по двум набо- рам данных в нужном порядке, не возвращаясь назад. В более сложных вариан- тах механизм времени выполнения должен возвращаться назад во втором набо- ре данных каждый раз, когда он перемещается на новую запись в первом наборе данных. Побочный эффект состоит в том, что соединение слияния не является абсо- лютно симметричным — несмотря на комментарии в руководстве, второй набор данных всегда полностью получается и сортируется; он не может быть получен по частям (например, с использованием индекса), как первый набор данных. Учитывая возрастающую сложность работы, которую необходимо выполнить, особенно в последних трех соединениях, можно ожидать, что стоимость соеди- нения слияния зависит от типа этого соединения.
424 Глава 13. Соединения сортировки и слияния Сценарий merge_sampl.es.sql в онлайн-хранилище кода демонстрирует за- просы, соответствующие рисункам, и генерирует результаты планов выпол- нения: alter session set hash_join_enabled = false; create table tl as with generator as ( select --+ materialize rownum id from all_objects where rownum <= 3000 ) select rownum rownum id, nl, trunc((rownum - l)/2) n2. Ipadfrownum,10,'0') small_vc, rpad('x',100,' x') from generator vl, generator v2 where rownum <= 10000 padding alter table tl add constraint tl_pk primary key(id); Создание таблицы t2 таким же образом Сбор статистики с помощью dbms_stats В следующую команду включены различные предикаты, соответствующие пяти рисункам на диаграмме select count(distinct tl_vc ||t2_vc) from ( select /*+ nojnerge ordered use_merge(t2) */ tl.small_vc tl_vc, t2.small_vc t2_vc from tl, t2 where tl.nl <= 1000 and t2.id = tl.id tl.nl <= 1000 and t2.n2 = tl.id tl.nl <= 1000 and t2.id between tl.id - 1 and tl.id + 1 tl.nl <= 1000 and t2.n2 = tl.n2 tl.nl <= 1000 and t2.n2 between tl.n2 - 1 and tl.n2 + 1 )
Соединения слияния 425 ВСЕ МЕНЯЕТСЯ Выполняя тесты в версиях 9i и 81, я устанавливал для hash_join_enabled значение false, чтобы опти- мизатор не выбирал в качестве соединения соединение хэширования для простых запросов с равен- ством. К сожалению, в версии 10g эта настройка игнорируется, поэтому я вынужден добавить под- сказки ordered и use_merge(t2), чтобы оптимизатор использовал соединение слияния. Все меняется, и это действительно неудобно, когда какой-либо код вдруг становится устаревшим и перестает работать. Следите, например, за параметром _optimizer_sortmerge_join_enabled, добав- ленным в версии 91; когда-нибудь его значение по умолчанию может без предупреждения стать рав- ным false — и все ваши соединения слияния превратятся в соединения с использованием вложен- ных циклов или соединения хэширования! Неожиданно то, что итоговая стоимость пяти разных планов выполнения одинакова, хотя и есть небольшие вариации в планах и отдельные изменения в кардинальности соединений. Например: План выполнения (версия 9.2.0.6, автотрассировка, без системной статистики, один-к-одному с равенством) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=105 Card=l Bytes=14) 1 0 SORT (GROUP BY) 2 1 VIEW (Cost=105 Card=1000 Bytes=14000) 3 2 MERGE JOIN (Cost=105 Card=1000 Bytes=34000) 4 3 SORT (JOIN) (Cost=38 Card=1000 Bytes=19000) 5 4 TABLE ACCESS (FULL) OF 'Tl' (Cost=29 Card=1000 Bytes=19000) 6 3 SORT (JOIN) (Cost=68 Card=10000 Bytes=150000) 7 6 TABLE ACCESS (FULL) OF 'T2' (Cost=29 Card=10000 Bytes=150000) План выполнения (версия 9.2.0.6, автотрассировка, без системной статистики, один-к-одному с диапазоном) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=105 Card=l Bytes=14) 1 0 SORT (GROUP BY) 2 1 VIEW (Cost=105 Card=25002 Bytes=350028) 3 2 MERGE JOIN (Cost=105 Card=25002 Bytes=850068) 4 3 SORT (JOIN) (Cost=38 Card=1000 Bytes=19000) 5 4 TABLE ACCESS (FULL) OF ’Tl' (Cost=29 Card=1000 Bytes=19000) 6 3 FILTER 7 6 SORT (JOIN) 8 7 TABLE ACCESS (FULL) OF 'T2' (Cost=29 Card=10000 Bytes=150000) Хотя кардинальность соединения (строка 3) изменяется от рассчитанного значения 1000 до рассчитанного значения 25 002, стоимость запроса остается одинаковой и равна 105. И эта стоимость в основном состоит из стоимости ге- нерации двух отсортированных наборов данных — строки 4 и 6 в первом плане выполнения: 38 + 68 = 106 (разница между этой суммой и итоговой стоимостью в плане выполнения вызвана тем, что план выполнения округляет каждый шаг до ближайшего целого числа, а расчеты ведутся с точными значениями). В главе 10 был показан тестовый пример с соединением слияния с диапазо- ном для демонстрации некоторых странностей в расчете кардинальности соеди- нения. В тот момент я не дал объяснения странной строки filter, появившейся
426 Глава 13. Соединения сортировки и слияния в плане выполнения. Та же строка фильтрации появилась и в показанном выше плане, и теперь пришло время рассказать о ней. Если вы переключитесь с autotrace на dbms_xplan. display О, чтобы вы могли увидеть предикаты доступа и предикаты фильтрации, то предикатами, относящимися к показанному выше плану (для них указаны номера строк), бу- дут 5 - filter("Tl"."Nl"<=1000) 6 - filter("T2"."N2"<="Tl"."N2"+l) 7 - access("T2"."N2">="T1"."N2"-1) filter("T2"."N2">="T1"."N2"-l) Вы также можете выполнить запрос, а затем проверить представление v$sql_ plan_stati sti cs, чтобы определить, сколько раз Oracle выполнил строки 6, 7 и 8, а также чтобы определить количество записей, которые вернул каждый ис- точник записей. Операция фильтрации (строка 6) была выполнена 1000 раз и вернула 5996 записей (что верно — кроме самой первой и самой последней записей в выборке из tl, я ожидал по шесть записей из t2 на каждую запись в tl). Полный доступ к таблице t2 (строка 8) был выполнен один раз и вернул 10 000 записей (что верно — мы просканировали таблицу только один раз и получили из нее все записи, потому что у нее отсутствуют простые ограни- чения). Странной строкой является строка 7 — строка сортировки — которая (со- гласно v$sql_plan_stati sti cs) выполняется 1000 раз (по одному разу на ка- ждую запись в таблице tl) и возвращает 9 502 996 записей! Ясно, что что-то не совсем верно в том, что показывает нам план выполнения. Мы не выполняли 1000 отдельных сортировок, мы выполнили одну сортировку для генерации данных — по крайней мере это показывает нам файл трассиров- ки 10032. Проблема появляется потому, что мы выполнили 1000 отдельных об- ращений к отсортированным данным, и наше выражение between превратилось в два отдельных предиката. Предикат доступа в строке 7 ("Т2”. "N2">="T1". "N2"-l) указывает Oracle, с какого места нужно начинать сканирование отсортированного набора данных; ио предикат фильтрациии в той же строке заставляет Oracle проверять каждую запись, чтобы убедиться, что условие предиката (”Т2". "N2">="T1". ”N2"-1) еще выполняется — несмотря на тот факт, что это условие и так должно выпол- няться, потому что набор данных отсортирован. Для каждой записи в 11 Oracle определил первую соответствующую запись во втором отсортированном наборе данных и просканировал его, начиная с этого места и до конца отсортированно- го набора данных. Вот почему 10 000 записей проверяются на соответствие пер- вой записи в tl, 9998 записей проверяются на соответствие второй записи в tl, 9 996 записей проверяются на соответствие третьей записи в 11, и так далее, что дает в общей сложности более 9,5 млн записей. В строке 6, строке с операцией фильтрации, Oracle использовал предикат ("Т2". "N2"<="T1". "N2"+l) для ис- ключения всех записей, кроме шести, на каждую запись из набора данных tl. Это выглядит так, как будто оптимизатор превратил исходный предикат be-
Соединения слияния 427 tween в два отдельных предиката, а затем забыл о специальном назначении этих двух предикатов (так же, как и при расчете стоимости простого выраже- ния between в одной таблице). Оптимизатор должен иметь возможность использовать предикат ("Т2". "N2">="T1". "N2" -1) в качестве предиката доступа, чтобы начать ранжи- рование во втором наборе данных, и использовать предикат ("T2"."N2"<= < =" Т1"." N 2 "+1) в качестве предиката фильтрации в той же строке. Этот преди- кат должен быть проверен для каждой записи во время выполнения ранжиро- вания, и процесс ранжирования должен прекратиться, как только условие про- верки не выполнится, то есть не доходя до конца всего набора данных. Чтобы закончить построение целостной картины (хотя на ней полный беспорядок и что-то увидеть практически невозможно), на рис. 13.3 сравнивается, что Oracle должен делать и что Oracle реально делает в этом случае: Что Oracle должен делать Что Oracle реально делает Рис. 13.3. Соединение слияния на основе диапазона Стоимость использования ресурсов процессора из-за этой ошибки может быть огромной, и в этом примере на самом деле проще выполнить запрос в виде вложенного цикла с 1000 сканирований таблицы t2, чем выполнить соединение слияния, которое должно было быть более эффективным. Соединение слияния без первой сортировки Как я уже указывал, в руководствах действительно предполагается, что если оба набора данных могут быть получены в отсортированном виде, то ни один из этих наборов не потребуется сортировать. На самом деле трассировка 10053 по- казывает, что единственный вариант «без сортировки», который используется оптимизатором, применяется только к внешней (первой) таблице. В результате для соединения слияния указываются две секции с оценкой стоимости, что мы и рассмотрим далее. Один из примеров из сценария merge_samples.sql в онлайн-хранилище кода может получить данные из первой таблицы с помощью индексного доступа с достаточно высокой стоимостью, поэтому выдается следующая трассировка при оценке стоимости соединения: SM Join Outer table: resc: 29 cdn: 9001 rcz: 15 deg: 1 resp: 29 Inner table: Tl
428 Глава 13. Соединения сортировки и слияния resc: 29 cdn: 9001 rcz: 15 deg: 1 resp: 29 using join:l distribution^ #grpups:l SORT resource Sort statistics - - Оценка стоимости внешней таблицы SORT resource Sort statistics - - Оценка стоимости внутренней таблицы Merge join Cost: 127 Resp: 127 SM Join (with index on outer) Access path: index (scan) -- Указывает используемый индексный доступ Index: Т2_РК TABLE: Т2 RSC_CPU: 0 RSC.IO: 182 IX_SEL: 9.0009e-001 TB_SEL: 9.0009e-001 Outer table: resc: 182 cdn: 9001 rcz: 15 deg: 1 resp: 182 Inner table: Tl resc: 29 cdn: 9001 rcz: 15 deg: 1 resp: 29 using join:l distribution^ #groups: SORT resource Sort statistics - - Оценка стоимости только внутренней таблицы Merge join Cost: 246 Resp: 246 Обратите внимание, что второй расчет SM Join (соединения сортировки/ слияния) появляется только в том случае, если индексный доступ еще не вы- бран в качестве метода получения первого набора данных с наименьшей стои- мостью — измените пример для выборки только 1000 записей из первой таблицы вместо 9000 записей, и вариант без сортировки будет выбран автоматически; также обратите внимание, что план выполнения показывает строку сортировки только для второй таблицы. План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=53 Card=l Bytes=14) 1 0 SORT (GROUP BY) 2 1 VIEW (Cost=53 Card=999 Bytes=13986) 3 2 MERGE JOIN (Cost=53 Card=999 Bytes=29970) 4 3 TABLE ACCESS (BY INDEX ROWID) OF 'Tl' (Cost=22 Card=1000 Bytes=15000) 5 4 INDEX (RANGE SCAN) OF *T1_PK' (UNIQUE) (Cost=4 Card=1000) 6 3 SORT (JOIN) (Cost=31 Card=1000 Bytes=15000) 7 6 TABLE ACCESS (BY INDEX ROWID) OF 'T2' (Cost=22 Card=1000 Bytes=15000) 8 7 INDEX (RANGE SCAN) OF 'T2_PK' (UNIQUE) (Cost=4 Card=l) Даже когда оптимизатор выбирает слияние без сортировки, вы можете обна- ружить, что стоимость соединения слияния все равно равняется (очень близка) сумме двух отдельных обращений к таблице. В этом случае видно, что стои- мость в строке 3 является суммой значений стоимости в строках 4 и 6 (53 = = 22 + 31).
Соединения слияния 429 Соединение слияния с декартовым произведением В главе 6 мы рассматривали пример, в котором оптимизатор использовал пере- ходное замкнутое выражение (transitive closure), чтобы превратить очевидное соединение в соединение с декартовым произведением. Я также тогда упомя- нул, что стоимость соединения оказалась несколько неожиданной. Пришло вре- мя разобраться, что произошло в том примере (см. сценарий cartesian.sql в он- лайн-хранилище кода): select count(tl.small_vc), count(t2.small_vc) from tl, t2 where tl.nl = 5 and t2.nl = 5 В таблице tl находится 800 записей, в таблице t2 — 1000 записей. В обоих случаях одна десятая часть записей имеет значение nl = 5. Ниже показан план выполнения для предыдущего запроса из представления v$sql_plan_stati s- tics после выполнения команды alter session, чтобы установить значение параметра_rowsource_execution_statisties равным true. Вы можете полу- чить то же содержимое представления, установив значение параметра sta- tist! cs_level равным all или значение параметра sql_trace равным true. Скрытый параметр более точен, но, конечно, его не следует использовать на ра- бочих системах. Id Starts Rows Plan (9.2.0.6 - v$sql_plan_statisties) 0 SELECT STATEMENT (all_rows) Cost (324,,) 111 SORT (aggregate) 2 1 8,000 MERGE JOIN (cartesian) Cost (324,8000,112000) 3 1 80 TABLE ACCESS (analyzed) Tl (full) Cost (4,80,560) 4 80 8,000 BUFFER (sort) Cost (320,100,700) 5 1 100 TABLE ACCESS T2 (full) Cost (4,100,700) Три цифры в скобках в конце каждой строки показывают стоимость, карди- нальность и количество байт, соответственно — и как видите, стоимость соеди- нения слияния (строка 2) равна 324, что, похоже, является стоимостью таблич- ного сканирования из строки 3 плюс стоимость буферной сортировки из строки 4. А стоимость буферной сортировки, похоже, равна стоимости табличного ска- нирования из строки 5, умноженного на кардинальность из строки 3 — в ре- зультате стоимость соединения слияния с декартовым произведением кажется очень похожей на стоимость соединения с использованием вложенных циклов, то есть на стоимость выборки из первой таблицы + кардинальность первой таб- лицы х стоимость выборки из второй таблицы. Во втором и третьем столбцах показаны значения I as t_s tarts и last_ output_rows из динамического представления v$sql_plan_statisties с дан- ными о производительности: эти цифры показывают, что буферная сортировка
430 Глава 13. Соединения сортировки и слияния выполнилась 80 раз (один раз на каждую запись из таблицы tl) и что 8000 за- писей действительно были отсортированы. Это то же самое, что мы видели и в случае нормального соединения слияния многие-ко-многим на основе диа- пазона — невероятно. Мы могли обратиться к отсортированным данным 80 раз, но неужели мы действительно отсортировали их 80 раз? Надеюсь, что нет. SORT И BUFFER (SORT) Я еще не определил, есть ли какая-либо значительная разница между этими двумя вариантами — похоже, оптимизатор не учитывает объем данных при выборе того или иного варианта. Операция buffer (sort), которая была выполнена в этом соединении слияния с декартовым произведением, яв- ляется обычной операцией сортировки при работе в версии 8г, и не похоже, что в следующих верси- ях произошли какие-то изменения. Какую бы информацию мы ни получали из статистики плана выполнения, сгенерированного оптимизатором, огромная стоимость выполнения этого за- проса, без сомнения, является ошибкой. Oracle получил очень маленький объем данных из второй таблицы, обработал его в памяти и указал стоимость полного табличного сканирования этому набору данных, как будто он собирался ис- пользовать эти данные. Мы можем увидеть причину этой аномалии, посмотрев в файл трассировки 10053. Важная часть этого файла выглядит следующим об- разом: Joi п order [1] : Т1[Т1]#0 Т2[Т2]#1 Now joining: Т2 [Т2]#1 ******* NL Join Outer table: cost: 4 cdn: 80 rcz: 7 resp: 4 Inner table: T2 Access path: tsc Resc: 4 Join: Resc: 324 Resp: 324 Best NL cost: 324 resp: 324 Join cardinality: 8000 = outer (80) * inner (100) * sei (1.0000e+000) [flag=0] Join result: cost: 324 cdn: 8000 rcz: 14 Best so far: TABLE#: 0 CST: 4 CDN: 80 BYTES: 560 Best so far: TABLE#: 1 CST: 324 CDN: 8000 BYTES: 112000 Посмотрите внимательно на приведенную выше часть файла трассировки — я не убрал из нее ни одной строки. Трассировка оптимизатора показывает толь- ко расчет стоимости соединения с использованием вложенных циклов. Я гово- рил, что расчет стоимости соединения слияния очень похож на расчет стоимо- сти соединения с использованием вложенных циклов, и это действительно так — оптимизатор использует стоимость соединения с использованием вло- женных циклов в качестве стоимости соединения слияния. Результатом использования этого механизма является то, что соединения с декартовым произведением приводят к очень большим значениям стоимо- сти — жаль, потому что иногда соединение с декартовым произведением явля- ется эффективной стартовой точкой при выполнении сложного запроса, и оп- тимизатор может проигнорировать такое соединение. Поэтому это именно тот случай, когда вы должны использовать подсказку в вашем SQL-коде. Что странно, этот пример полностью аналогичен показанному ранее запросу с очень интенсивным использованием ресурсов процессора, который действи- тельно обработал 9,5 млн записей. Разница состоит в том, что в этом примере
Агрегирование и другие операции 431 соединение превратилось в соединение с декартовым произведением, потому что предикат соединения исчез при использовании переходного замкнутого вы- ражения. Показанный ранее запрос не потерял свои предикаты соединения при использовании переходного замкнутого выражения, поэтому и расчеты у него выглядят совсем по-другому. В одном случае у нас есть стоимость, которая слишком велика для запроса, который не выполняет много работы; в другом случае у нас есть стоимость, которая слишком низка для запроса, который ин- тенсивно использует ресурсы процессора. Агрегирование и другие операции После рассмотрения базового соединения сортировки и слияния пришло время рассказать о других причинах, по которым Oracle может использовать сорти- ровку. Например, какова будет стоимость у следующих запросов (см. сценарий agg_sort.sql в онлайн-хранилище кода)? select coll, col2 from tl order by coll, col2; select distinct coll, col2 from tl; select coll, col2, count(*) from tl group by coll, col2; select coll, col2, count(*) from tl group by coll, col2 order by col2, coll; select max(coll), max(col2) from tl; Типичный план выполнения для любого из этих пяти запросов будет иметь следующую структуру: План выполнения (версия 9.2.0.6, автотрассировка, системная статистика выключена) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=18 Card=5000 Bytes=20000) 1 0 SORT (ORDER BY) (Cost=18 Card=5000 Bytes=20000) 2 1 TABLE ACCESS (FULL) OF 'Tl' (Cost=4 Card=5000 Bytes=20000) Единственное значительное различие между этими запросами находится в строке 1 плана, и это различие показано в табл. 13.5. Таблица 13.5. Сравнение значений стоимости для различных выражений сортировки Выражение Параметр оптимизатора Итоговая стоимость order by sort (order by) 18 distinct sort (unique) 18 group by sort (group by) 18 group by ... order by sort (group by) 18 max() sort (aggregate) 3 Есть две аномалии, на которые стоит обратить внимание. Первой очевидной аномалией является то, что итоговая стоимость запроса с выражением тах() гораздо меньше, чем итоговая стоимость всех остальных запросов, которые имеют одинаковую стоимость (на самом деле, когда вы включите оценку стои- мости использования ресурсов процессора, вы также обнаружите, что осталь-
432 Глава 13. Соединения сортировки и слияния ные четыре запроса также имеют в точности одну и ту же стоимость использо- вания ресурсов процессора, несмотря на очевидно различные выражения для обработки). В теории можно ожидать, что Oracle выполняет сортировку данных на пер- вом шаге каждой из этих операций. Однако пример с max О может быть опти- мизирован без сортировки, несмотря на строку с указанием сортировки в плане выполнения. Код времени выполнения просто должен отслеживать значения во входных данных, сохраняя самое большое на этот момент значение. Расчеты оптимизатора в точности отражают это поведение. Если мы посмотрим на другие операции, то становится ясно, что выражение order by требует сортировки. Таким же образом, выражение distinct может быть реализовано с помощью сортировки данных, сканирования отсортирован- ного набора данных и вывода записи только в том случае, если она ртличается от предыдущей записи. Оптимизатор не пытается получить небольшое значе- ние стоимости использования ресурсов процессора в последнем шаге этой опе- рации. Тот же подход используется и для выражения group by, за исключением того, что мы сохраняем получаемые итоговые значения (в данном случае — ко- личества) и выдаем эти итоговые значения вместе с записью, если запись отли- чается от предыдущей записи. Вторая аномалия проявляется, когда вы посмотрите на запрос, который вы- полняет группировку (group by) с последующим упорядочением (order by) — посмотрите внимательно на код, и вы увидите, что для выражения group by указаны столбцы coll, со!2, а для выражения order by — col2, coll, то есть в обратном порядке. Легко предположить, что Oracle должен отсортировать данные один раз, чтобы выполнить group by, и второй раз, чтобы выполнить order by. Но опти- мизатор в состоянии определить, что и для выражения group by, и для выраже- ния order by достаточно выполнить одну операцию сортировки. SQL-КОД ДОЛЖЕН ИСПОЛЬЗОВАТЬ ВЫРАЖЕНИЕ ORDER BY Нам постоянно говорится, что мы всегда должны включать в запрос выражение order by, если мы хотим получить данные в определенном порядке, и не полагаться на сортировку, которая в опреде- ленных случаях может быть выполнена выражением group by. Хотя это правило, которому мы долж- ны неукоснительно следовать, это не значит, что оптимизатор на самом деле выполнит обе эти опе- рации, если найдет альтернативу с более низкой стоимостью. Существует интересное развитие этого подхода. В версии 9i в Oracle поя- вился новый алгоритм агрегирования по множеству столбцов. Сценарий gby_ onekey.sql в онлайн-хранилище кода демонстрирует этот принцип. Я сгенериро- вал небольшой набор данных и выполнил к нему запрос с помощью следующей команды SQL: select coll, col2, count(*) from tl group by
Агрегирование и другие операции 433 coll, со!2 Результат выглядит следующим образом: COL1 COL2 COUNT(*) A Y 1 В V 1 В Y 1 Y ТО 1 Z DU 1 AD К 1 СС К 1 СЕ DZ 1 Результат не выведен в упорядоченном виде — явная и простая демонстра- ция того, что вы никогда не должны полагаться на выражение group by, чтобы неявно выполнить операцию order by. Это поведение контролируется скрытым параметром _gby_onekey_enabled; укажите для него значение false, и Oracle вернется к старому поведению и начнет выдавать данные в «правильном» по- рядке. ОШИБКА ПРИ ИСПОЛЬЗОВАНИИ ВЫРАЖЕНИЯ GROUP BY Существует не совсем обычная ошибка, связанная с этим новым механизмом группировки, которая проявляется в некоторых специфических случаях при выполнении соединения сортировки/слия- ния. На MetaLink эта ошибка имеет номер 3 487 660. Проблема состоит в том, что есть случаи, когда оптимизатор считает, что реализация выражения group by подразумевает реализацию выражения order by, и не выполняет сортировку первого входного потока данных для соединения слияния, ко- гда она действительно требуется. Итак, если в версии 9i появился новый механизм реализации выражения group by, что произойдет, если мы изменим этот запрос, включив выражение order by после выражения group by? Будет ли выполнена вторая сортировка после окончания группировки, чтобы выдать данные в нужном порядке? Если мы посмотрим на план выполнения или на трассировку 10053, то уви- дим, что ничего не изменилось по сравнению с тем, что мы уже видели в верси- ях 8i и 9i; план по-прежнему показывает, что будет выполнена только операция sort (group by), так что ответ отрицательный — но мы знаем, что механизм времени выполнения не всегда делает то, что показывает оптимизатор. Посмот- рев на статистику сессии, мы обнаружим в ней одну сортировку с правильным количеством отсортированных записей, что, похоже, является убедительным подтверждением плана выполнения. Но как может Oracle сделать работу, вы- полнив только одну сортировку? Ответ можно найти в трассировке 10032. Сравните два фрагмента набора статистики: первый фрагмент получен при вы- полнении запроса без выражения order by, второй — при выполнении запроса с выражением order by: -— Sort Statistics ------------------------ Input records 50
434 Глава 13. Соединения сортировки и слияния Output records 50 Total number of comparisons performed 245 Comparisons performed by in-memory sort 245 -— Sort Statistics -------------------------- Input records 50 Output records 50 Total number of comparisons performed 222 Comparisons performed by in-memory sort 222 В обоих этих фрагментах сортировка только одна, но количество выполнен- ных сравнений изменяется при добавлении выражения order by. Если вы по- вторите тесты с отключенной новой функциональностью, то обнаружите, что Oracle просто возвращается к старому механизму группировки, когда добавля- ется выражение order by, поэтому ему не нужно выполнять дополнительную сортировку, чтобы получить данные в нужном порядке. НОВАЯ РЕАЛИЗАЦИЯ ВЫРАЖЕНИЯ GROUP BY В ВЕРСИИ 10GR2 В версии 10gR2 появилась новая функциональность — операция hash group by (с соответствующей операцией hash unique), которая, в принципе, во многих случаях может выполняться гораздо быст- рее, чем sort group by. Вам лучше проверить, остался ли у вас еще SQL-код, в котором вы полагае- тесь на неявную сортировку выражения group by, чтобы получить данные в отсортированном виде. Было бы интересно проверить, существует ли такой набор данных, при котором оптимизатор в вер- сии 10gR2 решит использовать hash group by с последующей операцией sort order by — или комби- нированные операции group by/order by всегда будут превращаться в одну операцию sort group by, когда столбцы группировки и сортировки совпадают. Еще одно изменение появляется при обновлении версии 8i до 9i в расчетах оптимизатора для операций group by и distinct. Ниже показаны запрос из сценария agg_sort_2.sql в онлайн-хранилище кода и фрагмент файла трассиров- ки 10053 в версии 9.2.0.6 для этого запроса (файл трассировки 10053 для про- стой операции select distinct очень похож на этот): select coll, col2, count(*) from tl group by coll, col2 SINGLE TABLE ACCESS PATH TABLE: Tl ORIG CDN: 5000 ROUNDED CDN: 5000 CMPTD CDN: 5000 Access path: tsc Resc: 3 Resp: 3 BEST.CST: 3.00 PATH: 2 Degree: 1 Grouping column cardinality [ COL1] 25 Grouping column cardinality [ COL2] 71 *************************************** GENERAL PLANS *********************** Join orderfl]: T1[T1]#0 GROUP BY sort GROUP BY cardinality: 1256, TABLE cardinality: 5000
Агрегирование и другие операции 435 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 10 Row size: 15 Rows: 5000 Initial runs: 1 Merge passes: 1 10 Cost / pass: 19 Total 10 sort cost: 11 Total CPU sort cost: 0 Total Temp space used: 0 Best so far: TABLE#: 0 CST: 14 CDN: 5000 BYTES: 20000 Total Temp space used: 0 Первое, на что надо обратить внимание, — это информация в GROUP BY cardinality. Оптимизатор получил отдельно значения num_distinct для столбцов со11исо12с помощью обращения к одной таблице и использовал их для получения кардинальности группировки в секции general plans. Но как получается значение 1256 из значений 25 и 71? Ответ следующий: эти два зна- чения умножаются и делятся на квадратный корень из 2 (1256 = 24 х 71/ /1,4142). В общем случае оптимизатор получает количество уникальных ком- бинаций W столбцов, перемножая отдельные значения num_di sti net и (N - 1) раз деля их на квадратный корень из 2. Для выполнения контроля ошибок оптимизатор сравнивает это значение с количеством записей в таблице и выводит меньшее из этих двух значений в качестве кардинальности результирующего набора данных. В версии 9i (со значением по умолчанию параметра _new_sort_cost_ estimate) это имеет прямое влияние на стоимость. Ранее в этой главе я указы- вал, что часто стоимость Total 10 sort cost равна (Blocks to Sort + 10 Cost per pass) / 2: в этом примере стоимость должна быть равна (10 + 19) / 2, то есть выведено было бы значение 15. Но стоимость Total 10 sort cost показана равной И. Что-то изменилось (в версии 8i вы бы увидели в качестве общей ста- тистики операций ввода-вывода эквивалент значения 15, с небольшим отличи- ем из-за ошибок округления и пр.). Разница в стоимости интересна: начиная с версии 9i, стоимость корректиру- ется с учетом того, что количество записей, возвращаемых запросом, меньше общего количества записей в таблице. Похоже, что в общем случае стоимость 10 cost per pass равна общий объем входных данных + общий объем выходных данных. Значение 19 получается из следующих расчетов: Объем входных данных в блоках + Объем выходных данных в блоках = Количество входных записей * Размер записи / Размер блока + Количество выходных записей * Размер записи / Размер блока = 5000 * 15/8192 + 5000 * 15/8192 = 18.3105 что округляется до 19 Но в случае нашей группировки оптимизатор решил, что кардинальность группировки равна 1256, поэтому результат изменился: 5000 * 15/8192 + 1256 * 15/8192 = 11.455 -- что округляется до 12 Поместим полученное значение 12 в нормальную формулу (формулу сорти- ровки со слиянием в один проход):
436 Глава 13. Соединения сортировки и слияния (blocks to sort + 10 Cost per pass)/2 = (10 + 12)/2 = 11 Поэтому в этом примере трассировка 10053 показывает одно значение для 10 Cost per pass (19), а для расчета Total 10 sort cost использует другое зна- чение (12). Похоже, что файл трассировки не учитывает появившуюся новую функциональность. Существует ловушка, которой следует опасаться, если вы собираетесь об- новлять версию. Если у вас есть сложные запросы, которые используют агре- гатные функции для получения из большого количества записей небольшие ре- зультирующие наборы данных, то это изменение стоимости может привести к значительному изменению стоимости некоторых частей этих запросов — что может полностью изменить весь план выполнения. В этом простом примере стоимость агрегирования по одной таблице снизилась с 18 до 14 (из-за того, что стоимость группировки снизилась с 15 до И) после обновления версии. Это уменьшение произошло при выполнении запроса, который возвратил 1256 вы- ходных записей из 5000 входных. Разница при выполнении запроса, возвра- щающего 100 выходных записей из 100 000 входных, может быть гораздо боль- шей. Индексы Об индексах мало что можно сказать — если вы хотите создать индекс с помо- щью чтения данных из таблицы, то большая часть работы уйдет на сортировку данных, в то время как Oracle оценивает только стоимость получения данных (то есть стоимость табличного сканирования или полного индексного сканиро- вания, которое будет использовано для получения нужных столбцов и иденти- фикаторов записей). Если вы хотите определить требования к памяти для создания индекса со структурой бинарного дерева, то помните, что в основном при этом происходит простая сортировка. Но также надо помнить, что выполняемая сортировка включает кроме ожидаемых вами столбцов еще один — rowid, размер которого составляет 6 байт для нормальной таблицы, 8 байт для кластерного индекса и 10 байт для глобального индекса на секционированной таблице (но, опять же, только 6 байт для локального индекса на секционированной таблице). Как вы можете догадаться, требования к памяти для создания битового ин- декса несколько выше, потому что размер битового индекса может очень силь- но различаться. На данный момент я не знаю, как можно получить общую оцен- ку требований к памяти для создания битового индекса. Необходимо учесть еще один момент, если вы все еще используете ручную настройку workarea_size_policy или работаете в версии 8i: в версиях 8i и 91 вся память выделяется из PGA (а не из UGA), вне зависимости от того, устано- вили вы или нет значение sort_area_retai ned_si ze. В версии 10g память вы- деляется из UGA, что несколько неожиданно и выглядит рискованным, но это не имеет значения, если у вас установлена автоматическая настройка work-
Агрегирование и другие операции 437 area_si ze_poliсу, потому что память будет корректно высвобождена, как толь- ко завершится создание индекса. Операции над множествами Oracle предлагает три операции над множествами (set operations) — union, intersect и minus, как показано в табл. 13.6. Вы можете захотеть добавить в список операций над множествами и операцию union all, но технически она не является операцией над множествами, потому что множества не позволяют хранить дублированные записи, а результат операции union all может вклю- чать дублированные записи (это также очень простая операция — Oracle просто обрабатывает отдельно два запроса и выполняет их один за другим). Таблица 13.6. Операции над множествами в SQL Операция Результирующий набор данных включает union По одному экземпляру каждой записи из двух входных потоков intersect По одному экземпляру каждой записи, встречающейся в обоих входных потоках minus По одному экземпляру каждой записи из первого входного потока, которой нет во втором входном потоке Из-за необходимости устранения «дублированных записей» вы можете по- думать, что используются сортировка и оператор distinct — так что давайте посмотрим, что делает Oracle с этими операциями над множествами. В сцена- рии set_ops.sql в онлайн-хранилище кода я создал две очень простые таблицы: create table tl as select rownum id, ao.* from all_ob]'ects ao where rownum <= 2500 create table t2 as select rownum id, ao. * from all_ob]'ects ao where rownum <= 2000 Согласно этому определению, таблица t2 является строгим подмножеством таблицы tl и содержит 2000 записей, которые также существуют в таблице tl. Чтобы продемонстрировать результаты использования всех трех операций над множествами, я выполнил три запроса (плюс один дополнительный для демон- страции несимметричной сущности оператора minus) с результатами, приве- денными в табл. 13.7.
438 Глава 13. Соединения сортировки и слияния Таблица 13.7. Результаты использования операций над множествами Операция Результирующий набор данных включает select * from tl union select * from t2 select * from tl intersect select * from t2 select * from tl minus select * from t2 select * from t2 minus select * from tl 2500 записей из таблицы tl 2000 записей из таблицы t2 500 записей из таблицы tl, которые не существуют в таблице t2 Пустой набор данных — нуль записей, которые существуют в таблице t2, но не существуют в таблице tl Результирующие наборы данных очень легко описать, потому что мы знаем, что записи в двух наборах данных, над которыми выполняются операции, уже уникальны и что один набор данных является подмножеством другого. Ситуа- ция становится интереснее, когда мы выполняем запросы, в которых могут быть дублированные записи. Давайте рассмотрим пример, в котором мы делаем выборку только столбцов owner и object_type из двух таблиц. Если мы знаем, что наборы данных не содержат дублированные записи, то мы можем решить, что нужно явно указать требование уникальности перед вы- полнением операции над множествами, сделав следующее: select distinct owner, object_type from tl i ntersect select distinct owner, object_type from t2 С другой стороны, если мы понимаем, как работают операции над множест- вами — возвращая результирующий набор данных с исключением дублирован- ных записей — мы можем решить, что безопаснее выполнить следующее и дать Oracle решить проблему с дублированными записями самому: select owner, object_type from tl intersect select owner, object_type from t2 Ниже показаны два плана выполнения: План выполнения (версия 9.2.0.6 - с использованием distinct - с отключенной системной статистикой) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=30 Card=15 Bytes=37S) 1 0 INTERSECTION 2 1 SORT (UNIQUE) (Cost=15 Card=15 Bytes=180) 3 2 TABLE ACCESS (FULL) OF 'Tl' (Cost=6 Card=2500 Bytes=30000) 4 1 SORT (UNIQUE) (Cost=15 Card=15 Bytes=195)
Агрегирование и другие операции 439 5 4 TABLE ACCESS (FULL) OF 'T2' (Cost=6 Card=2000 Bytes=26000) План выполнения (версия 9.2.0.6 - БЕЗ distinct - с отключенной системной статистикой) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=37 Card=2000 Bytes=56000) 1 0 INTERSECTION 2 1 SORT (UNIQUE) (Cost=19 Card=2500 Bytes=30000) 3 2 TABLE ACCESS (FULL) OF 'Tl' (Cost=6 Card=2500 Bytes=30000) 4 1 SORT (UNIQUE) (Cost=18 Card=2000 Bytes=26000) 5 4 TABLE ACCESS (FULL) OF 'T2' (Cost=6 Card=2000 Bytes=26000) Включаем ли мы оператор di sti net в запрос или нет, оптимизатор указыва- ет в плане операцию sort (unique) (строки 2 и 4) перед выполнением опера- ции intersection. Более того, если вы выполните эти запросы с трассировкой события 10032, установленным для проверки количества отсортированных за- писей и количества выполненных сравнений, то вы обнаружите, что механизм времени выполнения выполняет один и тот же объем работы в обоих случаях. Общеизвестно, что не требуется включать в запрос оператор distinct, как я это сделал в первом примере. На самом деле некоторые говорят, что включение оператора distinct является ошибкой, потому что приводит к лишнему вы- полнению сортировки (чего не происходит, как показывает этот пример). По- этому в общем случае запросы, использующие операции над множествами, не включают в себя оператор distinct. Посмотрите внимательно на значения стоимости и кардинальности. Они аб- солютно разные. При использовании оператора distinct оптимизатор опреде- ляет количество записей, которые будут возвращены каждой половиной запро- са, с помощью нормального расчета Group by Cardinality, который выдает меньшие значения стоимости и кардинальности (разница в стоимости исчезает в этом примере, если включить оценку стоимости использования ресурсов про- цессора — это нормальное поведение, показывающее, как может измениться оценка стоимости для сортировок в памяти, если включить оценку стоимости использования ресурсов процессора). ИСПОЛЬЗОВАНИЕ ОПЕРАЦИЙ НАД МНОЖЕСТВАМИ И ОПЕРАТОРА DISTINCT В отличие от теоретических знаний об операциях над множествами, похоже, что практика исключе- ния оператора distinct из запроса на Самом деле является не очень хорошей идеей: оптимизатор вы- полняет более правильную оценку кардинальности базового результирующего набора данных, если вы включаете в запрос оператор distinct. Включение оператора distinct в запрос не должно влиять на эту оценку, но все-таки влияет. При определении кардинальности результата выполнения самой операции над множествами мы оказываемся на знакомой территории. Выполняя тесты в версии 10g, я получил следующий список значений, показанный в табл. 13.8. Как видите, после того как оптимизатор определил значения кардинальности базовых запросов, он использует вариант с худшим результатом для определе- ния результатов выполнения операции над множествами.
440 Глава 13. Соединения сортировки и слияния Таблица 13.8. Методы расчета кардинальности для операций над множествами Набор данных Кардинальность группировки Причина tl 20 owner (4) * objectjype (7) / 1.4142 t2 15 owner (3) * objectjype (7) / 1.4142 Union 35 Значение из таблицы tl + значение из таблицы t2 Intersect 15 Меньшее значение из значения из таблицы tl и значения из таблицы t2 Minus 20 Первое значение, то есть значение из таблицы tl Нужно быть осторожным, делая предположения о том, как обрабатывается SQL-код. Все может меняться, и вполне может оказаться, что в версиях 8.0 и 7.3 наличие явно лишнего оператора distinct приводило к выполнению двух до- полнительных сортировок. Все изменяется, и также могут появляться ошибки. Кстати, если вы посмотрите трассировку 10053, вы не найдете в ней инфор- мации о самой операции над множествами — есть только секции для двух от- дельных запросов, после них выводится итоговая стоимость, которая и пред- ставляет операцию над множествами. Но никакого объяснения итоговых цифр нет. Поэтому вы можете только предположить, как это сделал я, что оптимиза- тор просто суммирует стоимость выполнения отдельных запросов, чтобы полу- чить итоговую стоимость операции над множествами. Рассмотрим в качестве последнего предупреждения следующую команду СТAS — create table as select (которая также есть в сценарии set_ops.sql в онлайн-хранилище кода): create table t_intersect as select distinct * from ( select owner, object_type from tl intersect select owner, object_type from t2 ) Ясно, что операция di stinet во внешней команде select лишняя — но мы уже видели, что Oracle может успешно избегать выполнения сортировок, кото- рые на самом деле не нужны (не только в операциях над множествами, но так- же и в нашем более раннем примере, в котором выполнялась операция group by с последующей операцией order by). Итак, как же выглядит план выполнения для этой CTAS? Ниже показан результат из dbms_xplan в версии 9.2.0.6 с вклю- ченной оценкой стоимости использования ресурсов процессора: 1 Id Operation I Name | Rows | Bytes | Cost (%CPU)| 0 CREATE TABLE STATEMENT 1 | 2000 | 56000 1 16 (25)| 1 LOAD AS SELECT 1 1 1 1 1 2 VIEW 1 | 2000 | 56000 1 1 з INTERSECTION 1 1 1 1 1 4 SORT UNIQUE 1 | 2500 | 30000 1 8 (25)| 5 TABLE ACCESS FULL 1 Tl | 2500 | 30000 1 7 (15)|
Агрегирование и другие операции 441 | 6 | SORT UNIQUE | | 2000 | 26000 | 8 (25)| | 7 | TABLE ACCESS FULL | T2 | 2000 | 26000 | 7 (1S)| Оптимизатор смог заметить, что операция di sti net не нужна: согласно пла- ну выполнения, результирующий набор данных из подставляемого представле- ния в строке 2 не был отсортирован. Ниже показан план выполнения в версии 10g — также с включенной оцен- кой стоимости использования ресурсов процессора: Hd I Operation | Name | Rows | Bytes I Cost (%CPU)| Time | 1 0 1 CREATE TABLE STATEMENT 1 1 2000 | 56000 | 20 (15)| 00:00:01 | 1 1 1 LOAD AS SELECT 1 1 1 1 1 1 1 2 | SORT UNIQUE 1 1 2000 | 56000 | 18 (17)| 00:00:01 | 1 3 | VIEW 1 1 2000 | 56000 1 17 (12)| 00:00:01 | 1 4 1 INTERSECTION 1 1 1 1 1 1 1 5 1 SORT UNIQUE 1 1 2500 | 32500 | 7 (15)| 00:00:01 | 1 6 | TABLE ACCESS FULL 1 Tl | 2500 | 32500 1 6 (0)1 00:00:01 | 1 7 | SORT UNIQUE 1 1 2000 | 26000 | 7 (15)| 00:00:01 | 1 8 1 TABLE ACCESS FULL 1 T2 | 2000 | 26000 1 6 (0)1 00:00:01 | Вы обратили внимание на операцию sort unique в строке 2? Похоже, что в версии 10g опять появилась лишняя сортировка, которая была убрана в версии 91. К счастью, мы всегда можем проверить, какая сортировка на самом деле вы- полняется, включив событие 10032, — и в плане выполнения версии 91 инфор- мация оказывается неверной. Файлы трассировки и в версии 9i, и в версии 10g показывают одно и то же. Ниже я вывел часть трассировки из версии 10g: Input records Output records 2000 -- Tl 9 Input records Output records 2500 -- T2 10 Input records Output records 9 -- Представление операции intersection 9 Input records Output records 9 9 Input records Output records 2500 10 Input records Output records 2000 9 Видно, что Oracle отсортировал две таблицы, в которых находится 2500 и 2000 записей, чтобы вывести 10 и 9 записей, соответственно. Третья сорти- ровка выполняется после того как операция пересечения выдала свои 9 запи- сей. Если вы удалите лишнюю операцию distinct из запроса, то третья опера- ция сортировки исчезнет. Обе версии выполняют лишнюю сортировку — но только в версии 10g это указывается в плане выполнения.
442 Глава 13. Соединения сортировки и слияния ПРИМЕЧАНИЕ Я указывал ранее в этой главе, что статистика сортировки выводится дважды — то, что статистика в этом примере выводится второй раз в обратном порядке, дает вам понимание сущности механиз- ма времени выполнения, использующего стек. Одним из самых важных моментов, которые вы должны понять из этого примера, является то, что даже если оптимизатор иногда и выдает неверную ин- формацию, при выполнении сортировки трассировка 10032 всегда покажет вам, что реально происходит. Последнее предупреждение Я уже несколько раз делал это предупреждение и снова повторю его, в очеред- ной и последний раз. То, что делает механизм времени выполнения, не обяза- тельно совпадает с тем, что выдал оптимизатор при оценке стоимости. Ниже показан прекрасный пример интересной уловки времени выполнения, о кото- рой оптимизатор ничего не знает (на данный момент). Код находится в сцена- рии short_sort.sql в онлайн-хранилище кода: create table tl as with generator as ( select --+ materialize rownum id, substr(dbms_random.string('U',6),1,6) sortcode from all_ob]’ects where rownum <= 5000 ) select /*+ ordered use_nl(v2) */ substr(v2.sortcode,1,4) || substr(vl.sortcode,1,2) sortcode from generator vl, generator v2 where rownum <= 1 * 1048576 -- Сбор статистики с помощью dbms_stats select sortcode from tl order by sortcode select * from ( select * from tl order by sortcode ) where rownum <= 10
Заключение 443 Стоимость выполнения обоих этих запросов одинакова — большая часть стоимости берется из строки, в которой выполняется операция сортировки; например, ниже показан план для второго запроса в моей обычной среде вер- сии 9i: План выполнения (версия 9.2.0.6, автотрассировка) 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=2757 Card=10 Bytes=50) 1 0 COUNT (STOPKEY) 2 1 VIEW (Cost=2757 Card=1048576 Bytes=5242880) 3 2 SORT (ORDER BY STOPKEY) (Cost=2757 Card=1048576 Bytes=7340032) 4 3 TABLE ACCESS (FULL) OF 'Tl' (Cost=266 Card=1048576 Bytes=7340032) Статистика сессии показывает, что для каждого запроса было отсортировано по миллиону записей, и когда я в то же время запустил трассировку 10032, она подтвердила, что действительно отсортировано по миллиону записей. Но пер- вому запросу требуется около 5 с, чтобы вернуть данные, а второй запрос воз- вращает данные менее чем за полсекунды. Почему? Проверив файл трассировки 10032, можно увидеть, что первый запрос вы- полнил 20 млн сравнений для сортировки данных, а второй выполнил 1 млн сравнений. Выполняя запрос с ограничением rownum, механизм времени выпол- нения просто сканирует таблицу, обновляя список с 10 наибольшими значения- ми. Он на самом деле не выполняет сортировку 1 000 000 записей, а просто про- веряет каждую запись — если значение этой записи больше, чем наименьшее значение в текущем списке, то это значение должно заменить наименьшее зна- чение в списке. После окончания сканирования остается только 10 записей для сортировки (в версии 10g трассировка 10032 также показывает, что выполне- ние полной сортировки требует 28 Мбайт и при этом происходит сохранение данных на диск, а выполнение второй сортировки требует только 8192 байт). Второй запрос выполняется гораздо быстрее первого, но у оптимизатора все еще отсутствует модель оценки стоимости, которая соответствует операции времени выполнения. Заключение Механизмы, использующие sort_area_size и sort_area_retained_size для ручной настройки workarea_si ze_poli су, все еще применяются для разделяе- мых серверов (MTS), поэтому вам нужно хотя бы немного знать о том, как они работают. Код, использующий автоматическую настройку workarea_size_policy и pga_aggregate_target, использует те же механизмы сортировки, но прини- мает гораздо лучшие тактические решения относительно объемов выделяемой памяти. Так как объем используемых на сортировку ресурсов процессора уве- личивается при росте размера бинарного дерева, Oracle применяет стратегию, при которой он пытается выполнить сортировку полностью в памяти (чтобы минимизировать количество дисковых операций ввода-вывода), но если он об- наруживает, что не может выполнить оптимальную сортировку, он выделяет
444 Глава 13. Соединения сортировки и слияния минимальный объем памяти (плюс небольшой запас для накладных расходов), позволяющий выполнить сортировку в один проход. Тем самым минимизиру- ется последующее использование ресурсов процессора без изменения общих требований к количеству операций ввода-вывода. Хотя использование включенной оценки стоимости использования ресурсов процессора и автоматической настройки workarea_si ze_pol i су стратегически является правильным выбором, все равно остается большое различие между моделью оптимизатора для выполнения сортировки и реальной деятельностью во время выполнения, из-за чего люди при выполнении изменений могут со- мневаться в конечном результате. Соединения сортировки/слияния можно использовать в соединениях с пре- дикатами на основе диапазонов, что делает их более гибкими, чем соединения хэширования. Похоже, стоимость соединения сортировки/слияния практиче- ски полностью зависит от стоимости двух шагов получения отсортированных данных: само слияние имеет нулевую стоимость, вне зависимости от его слож- ности. Единственным исключением является соединение слияния с декарто- вым произведением. Однако механизм оптимизации соединений на основе диапазонов требует получения полного второго набора данных, чтобы слияние могло выполняться для каждой записи первого входного потока. Похоже, что в механизме есть де- фект, из-за которого этот тип соединения слияния может начать очень интен- сивно потреблять ресурсы во время выполнения, даже если при оценке стоимо- сти эта дополнительная работа и не будет показана. При выполнении всех операций distinct, group by и order by используёт- ся сортировка (до версии 10gR2), и единственная стоимость, которая учитыва- ется оптимизатором, — это стоимость сортировки. Похоже, что в расчете стои- мости есть компонент, который учитывает размер выходных данных для опера- ции группировки и операции di stinet, начиная с версии 91. Сортировка может интенсивно потреблять ресурсы, и иногда можно видеть, что Oracle выполняет больше работы, чем должно быть выполнено, судя по плану выполнения. Суще- ствуют случаи, когда оптимизатор и механизм времени выполнения по-разному обрабатывают запрос, но в случае сортировки трассировка 10032 очень хорошо показывает, что реально было сделано. Тестовые сценарии Файлы к этой главе, доступные для загрузки, перечислены в табл. 13.9. Таблица 13.9. Тестовые сценарии к главе 13 Сценарий Комментарии sort_demo_01.sql snap_myst.sql snap_ts.sql Первая демонстрация того, как выполняется сортировка Показывает изменения в статистике текущей сессии Показывает изменения в статистике ввода-вывода на уровне табличного пространства c_mystats.sql Создание представления, используемого сценарием snap_myst.sql — должен быть выполнен как SYS
Тестовые сценарии 445 Сценарий Комментарии sort_demo_01a.sql sort_demo_01b.sql Выполнение тестов над набором данных из sort_demo_01.sql Выполнение тестов с включенной оценкой стоимости использования ресурсов процессора над набором данных из sort_demo_01.sql pat_dump.sql Значение pga_aggregate_target устанавливается равным входному параметру и выполняется запрос sas_dump.sql Значение sort_area_size устанавливается равным входному параметру и выполняется запрос pat_nocpu_harness.sql Генерация сценария для установки различных значений pga_aggregate_target и вызова pat_dump.sql для этих значений pat_cpu_hamess.sql То же, что и pat_nocpu_harness.sql, но с включенной системной статистикой sas_nocpu_hamess.sql Генерация сценария для установки различных значений sort_area_size и вызова pat_.dump.sql для этих значений sas_cpu_harness.sql То же, что и sas_nocpu_harness.sql, но с включенной системной статистикой no_sort.sql Слияние «без сортировки» может отсортировать второй набор данных перед получением всего первого набора данных cartesian.sql agg_sort.sql gby_onekey.sql merge_samples.sql set_ops.sql short_sort.sql Пример соединения слияния с декартовым произведением Различные причины сортировки Демонстрация сортировки по множеству столбцов в версии 9i Примеры соединений слияния Примеры операций union, intersect, и minus Показывает, что оптимизатор не может правильно оценить стоимость новой уловки времени выполнения setenv.sql Установка стандартизированной тестовой среды для SQL*Plus
*| Л Файл трассировки *Т 10053 Было бы невозможно написать хорошую книгу о стоимостном оптимизаторе без упоминания события отладочной трассировки оптимизатора 10053. Хоро- шо известно, что файл трассировки 10053 может предоставить нам много ин- формации о том, что делает оптимизатор для расчета значений кардинальности и стоимости при оценке плана выполнения. Эта информация не является пол- ной, и в таком файле трассировки могут присутствовать различные странности. И все же в этой главе рассматривается трассировка 10053, начиная с простого соединения четырех таблиц в версии 10gRl и описания некоторых его особен- ностей. Чтобы включить трассировку 10053, вам нужно выполнить одну из следую- щих команд: alter session set events '10053 trace name context forever'; alter session set events '10053 trace name context forever, level 1'; alter session set events '10053 trace name context forever, level 2'; Первые две команды на самом деле выполняют одно и то же действие, а по- следняя команда выводит немного более короткий файл трассировки, потому что он не включает параметры оптимизатора, обычно выводящиеся в начале файла трассировки. Чтобы остановить трассировку, выполните alter session set events '10053 trace name context off'; Вы можете обнаружить, что трассировка 10053 не работает в SQL embedded в PL/SQL, если только вы не работаете в версии 10g. Визуальное представление этой главы полностью отличается от всей осталь- ной книги. Оригинальный файл трассировки выводится в стиле «кода», и в этой главе показаны большие фрагменты этого файла. Чтобы облегчить чте- ние, я включил подзаголовки в каждый новый порядок соединения. Обратите внимание, что я добавляю комментарии перед тем текстом, который хочу про- комментировать, хотя есть пара замечаний, для которых потребуется возвра- титься назад или перейти вперед.
Запрос 447 ПРИМЕЧАНИЕ Очевидно, что версия 10gR2 работает по-другому. Порядок разделов другой, присутствует большое количество дополнительных подсказок и объяснений (включая список описаний для всех аббревиа- тур, таких как CDN), выводится системная статистика, а также включен хранимый итоговый план выполнения с предикатами. Запрос В более ранних версиях Oracle запрос выводился в начале файла трассировки 10053. В более поздних версиях 9.2 и 10.1 он переместился в конец файла, по- этому ниже я привожу использованный мною запрос. Сценарий для создания данных находится в онлайн-хранилище кода под названием big_10053.sql. Обратите внимание, что имена таблиц дают ясное представление о смысле SQL-кода и что я разместил таблицы в том порядке, в котором, как я думаю, Oracle будет к ним обращаться. Я также следовал соглашению о стиле написания выражения where, соглас- но которому каждый предикат должен читаться следующим образом: жепаемый_стопбец = известное_значение Очевидно, что это правило выполняется и в таких строках: ggp.small_num_ggp between 100 and 150 Что менее очевидно, также оно подходит и для следующей строки: gp.id_ggp = ggp.id Когда я пишу такую строку, я предполагаю, что запрос будет выполнять дей- ствия в таком порядке, согласно которому он уже обратился к таблице great- grandparent (ggp) перед выполнением данной строки, так что выражение ggp. 1 d в данном случае уже является константой. Следуя такому соглашению при написании SQL-кода, я надеюсь, что буду- щие читатели разберутся в моем понимании цели этого SQL-кода, способа, с помощью которого он будет выполнен в Oracle, и индексов, которые могут су- ществовать для выполнения этого запроса. Обратите внимание, что для более сложных запросов я также вставляю пару пустых строк комментария до и после каждой группы предикатов, «относящих- ся?» к определенной таблице. select count(ggp.small_vc_ggp), count(gp.small_vc_gp), count(p.small_vc_p), count(с.small_vc_c) from greatgrandparent ggp, grandparent gp, parent p, child c where ggp.small_num_ggp between 100 and 150 /* */ and gp.id_ggp = ggp.id
448 Глава 14. Файл трассировки 10053 and gp.small_num_gp between 110 and 130 /* */ and p.id_gp = gp.id and p.id_ggp = gp.id_ggp and p.small_num_p between 110 and 130 /* */ and c.id_p = p.id and c.id_gp - p.id_gp and c.id_ggp = p.id_ggp and c.small_num_c between 200 and 215 План выполнения Единственное, что не отображается в файле трассировки, — это итоговый план выполнения; вы можете понять, как он выглядит, с помощью анализа указанно- го порядка соединения. Чтобы упростить понимание, я выполнил запрос через dbms_xplan, так что вы можете ниже увидеть план выполнения, перед чтением содержимого файла трассировки. Вы можете получить такой же план (без ин- формации о предикатах) в файле трассировки, установив событие 10132 вместе с событием 10053. Что немного странно, Oracle версии 10.1.0.4 выбрал путь, не совсем совпа- дающий с «очевидным» порядком, который можно определить по именам таб- лиц. И во время выполнения путь, который выбрал Oracle, использовал чуть меньше ресурсов, чем ожидаемый мною план. ПРИМЕЧАНИЕ В следующем фрагменте я изменил содержимое столбца Name, чтобы он показывал GP вместо на- звания таблицы grandparent и GGP вместо названия таблицы greatgrandparent. Это было сделано, чтобы результаты разместились на странице без переносов строк в середине таблицы. По той же причине я удалил двойные кавычки, которые обычно появляются в разделе Predicate Information. | Id | Operation | Name |Rows|Bytes|Cost (%CPU)|Time | 1 0 SELECT STATEMENT 1 | 1 | 96| 361 (1)| 00:00:04| 1 1 1 2 SORT AGGREGATE NESTED LOOPS 1 | 1 | 96| 1 | 1 ) 96| 1 1 361 (1)| 00:00:04| 1 3 NESTED LOOPS 1 111 77| 360 (1)| 00:00:04| 1 4 NESTED LOOPS | | 6 | 300| 348 (1)| 00:00:04| | *5 TABLE ACCESS FULL | GP | 110| 2530| 128 (1)| 00:00:02| | *6 TABLE ACCESS BY INDEX |PARENT| 1| 27| 2 (0)) 00:00:01| | *7 ROWID INDEX RANGE SCAN |P_PK | 1| | 1 (0)| 00:00:01| 1 *8 TABLE ACCESS BY INDEX ICHILD | 1| 27| 2 (0)| 00:00:01| | *9 ROWID INDEX RANGE SCAN |C_PK | 1| | 1 (0)| 00:00:01| |*10 TABLE ACCESS BY INDEX | GGP | 1| 19 | 1 (0)| 00:00:01| |«11 ROWID INDEX UNIQUE SCAN |GGP_PK| 1| | 0 (0)| 00:00:01| Predicate Information (identified by operation id):
Среда 449 5 - filter(GP.SMALL_NUM_GP<=130 AND GP.SMALL_NUM_GP>=110) 6 - filter(P.SMALL_NUM_P<=130 AND P.SMALL_NUM_P>=110) 7 - access(P.ID_GGP=GP.ID_GGP AND P.ID_GP=GP.ID) 8 - fiIter(C,SMALL_NUM_C<=215 AND C.SMALL_NUM_C>=200) 9 - access(C.ID_GGP=P.ID_GGP AND C.ID_GP=P.ID_GP AND C.ID_P= P.ID) 10 - filter(GGP.SMALL_NUM_GGP>=100 AND GGP.SMALL_NUM_GGP<=150) 11 - access(GP.ID_GGP=GGP.ID) Среда Единственное, чего вы не сможете увидеть в файле трассировки (до версии 10gR2), — это состояние системной статистики (необходимой для оценки стои- мости использования ресурсов процессора). Перед выполнением запроса я вы- полнил следующий неименованный блок PL/SQL: begin dbms_stats .set_system_stats( 'MBRC ,8); dbms_stats.set_system_stats('MREADTIM',20); dbms_stats.set_system_stats('SREADTIM',10); dbms_stats.set_system_stats('CPUSPEED',500); end; / Вспомните, что формула расчета стоимости (измененная), приведенная в «Oracle Performance Tuning Guide and Reference», которую я привел в главе 1, выглядела следующим образом: Стоимость = ( #SRds + #MRds * mreadtim / sreadtim + #CPUCycles / (cpuspeed * sreadtim) ) Это значит, что в зависимости от того, как каждая версия выполняет округ- ление и усечение различных частей в расчетах, стоимость табличного сканиро- вания будет примерно равна следующему значению: 1 + ceil((наибольшее значение / MBRC) * (mreadtim / sreadtim)) + стоимость использования ресурсов процессора = 1 + ceil((наибольшее значение / 8) * 20/10) + (еще немного) = 1 + ceil((наибольшее значение / 4)) + (еще немного) Учитывая количество блоков в четырех таблицах, которые вы увидите поз- же, мы получаем стоимость компонентов ввода-вывода табличного сканирова- ния, показанную в табл. 14.1. Таблица 14.1. Размеры таблиц и стоимость операций ввода-вывода при выполнении табличного сканирования Название таблицы Количество блоков Стоимость операций ввода-вывода greatgrandparent 250 64 grandparent 500 126 (127) parent 2500 626 (627) child 10 000 2501
450 Глава 14. Файл трассировки 10053 Значения в скобках на самом деле являются значениями, полученными из другого теста — я часто обнаруживаю, что мои предположения отличаются от верных значений примерно на единицу (плюс или минус единицу, в зависимо- сти от версии Oracle). Я думаю, что это ошибка в расчетах, связанная с округле- нием значения 0,5, которое получается из наибольшее значение / 4. Наконец, чтобы превратить значения ресурсов процессора в стоимость, нам нужно использовать последнюю строку общей формулы: Стоимость = (#CPUCycles / cpuspeed) / sreadtim) = #CPUCycles / (cpuspeed * sreadtim) Так как значение cpuspeed измеряется в мегагерцах, что означает мидлионы циклов (или операций Oracle) в секунду, а значение sreadtim измеряется в миллисекундах, нам приходится применить коэффициент 1000, чтобы быть уверенными, что мы везде используем одни и те же единицы времени. Итак, при скорости процессора, равной 500 МГц, и времени чтения одного блока, равного 10 000 мкс, нам нужно разделить количество циклов процессора на 5 000 000 перед его добавлением к значению стоимости. Файл трассировки В первой части файла трассировки 10053 выводятся текущие значения, исполь- зуемые оптимизатором в качестве результата считывания значения переменной связывания (следующий пример показан специально для демонстрации форма- та, потому что мой запрос не использует переменные связывания). К сожале- нию, здесь не приводятся имена переменных связывания, а приводятся только их номера позиций. Вам всего лишь понадобится пройти по команде, считая пе- ременные, чтобы установить соответствие между переменными и их значения- ми. ******************************************* Считанные значения переменные связывания в команде SQL ******************************************* bind 0:dty=2 mxl=22(22) mal=00 scl=00 pre=00 oacflg=03 oacfl2=0000 size=48 offset=0 bfp=065ddc30 bln=22 avl=02 flg=05 value=100 bind l:dty=2 mxl=22(22) mal=00 scl=00 pre=0Q oacflg=03 oacf12=0000 size=0 offset=24 bfp=065ddc48 bln=22 avl=03 flg=01 value=150 bind 3:dty=2 mxl=22(22) mal=00 scl=00 pre=00 oacflg=03 oacf12=0000 size=0 offset=48 No bind buffers allocated Настройки параметров Во второй части файла трассировки выводятся текущие настройки параметров, влияющих на оптимизатор. Если вы сравните полный список параметров, дос- тупных в x$ksppi, вы можете обнаружить некоторые элементы, каковые вроде
Файл трассировки 451 бы должны находиться в этом списке. И наоборот, если вы посмотрите на пред- ставление v$sql_opt1mizer_env (см. приложение Б), вы обнаружите, что в этом списке находится гораздо больше значений, чем в представлении. Вывод результатов в версии 10g разделяет список параметров на два набора: параметры, измененные в сессии, и параметры, использующие значения по умолчанию. Если вы думаете, что вы много знаете про оптимизатор и его работу, взгля- ните на некоторые «ориентированные на функциональность?» параметры, обыч- но имеющие значения true/false. Меня настораживает большое количество параметров, влияющих на работу оптимизатора, которых я никогда не видел в действии. *************************************** PARAMETERS USED BY THE OPTIMIZER ******************************** ************************************* PARAMETERS WITH ALTERED VALUES ****************************** ************************************* PARAMETERS WITH DEFAULT VALUES ****************************** optimi zer_mode_hi nted = false optimi zer_features_hi nted = 0.0.0 parallel_execution_enabled = true parallel_query_forced_dop = 0 parallel_dml_forced_dop = 0 parallel_ddl_forced_degree = 0 parallel_ddl_forced_i nstances = 0 _query_rewrite_fudge = 90 optimi zer_features_enable = 10.1.0.4 _optimi zer_search_limi t = 5 cpu_count = 1 active_instance_count = 1 parallel_threads_per_cpu = 2 hash_area_si ze = 131072 bi tmap_merge_area_size = 1048576 sort_area_size = 65536 sort_area_retai ned_si ze = 0 _sort_elimi nation_cost_ratio = 0 _optimi zer_block_si ze = 8192 _sort_multiblock_read_count = 2 _hash_multiblock_io_count = 0 db_file_multi block_read_count = 8 _optimizer_max_permutations = 2000 pga_aggregate_target = 204800 KB _pga_max_si ze = 204800 KB _sort_space_for_wri te_buffers = 1 _query_rewrite_maxdisj unct = 257 _smm_auto_mi n_i o_si ze = 56 KB _smm_auto_max_i o_s i ze = 248 KB _smm_mi n_si ze = 204 KB _smm_max_si ze = 10240 KB _smm_px_max_si ze = 61440 KB _cpu_to_i о = 0
452 Глава 14. Файл трассировки 10053 _optimizer_undo_cost_change = 10.1.0.4 parallel_query_mode = enabled parallel_dml_mode = disabled parallel_ddl_mode = enabled optimizer_mode = all_rows sqT.stat_enabl.ed = false _optimi zer_percent_parallel = 101 _always_anti_join = choosy _always_semi_joi n = choosy _optimizer_mode_force = true _partition_view_enabled = true _always_star_transformation = false _query_rewrite_or_error = false _hash_j oi n_enabled = true cursor_sharing = exact _b_tree_bi tmap_plans = true star_transformation_enabled = false _optimi zer_cost_model = choosy _new_sort_cost_estimate = true _compIex_v few_merg f ng = true _unnest_subquery = true _eliminate_common_subexpr = true _pred_move_around = true _convert_set_to_]oi n = false _pu'sh_] oi n_predicate = true _push_joi n_uni on_vi ew = true _f ast_fuH._scan_enabl.ed = true _optim_enhance_nnull_detecti on = true _parallel_broadcast_enabled = true _px_broadcast_fudge_factor = 100 _ordered_nested_loop = true _no_or_expansion = false optimizer_index_cost_ad] = 100 optimi zer_index_caching = 0 _system_index_caching = 0 _di sable_datalayer_sampli ng = false query_rewri te_enabled = true query_rewrite_i ntegri ty = enforced _query_cost_rewrite = true _query_rewrite_2 = true _query_rewrite_l = true _query_rewrite_expression = true _query_rewrite_]gmigrate = true _query_rewri te_fpc = true _query_rewri te_drj = true _full_pwi se_j oi n_enabled = true _parti al_pwise_join_enabled = true _left_nested_loops_random = true _improved_row_length_enabled = true _i ndex_j oi n_enabled = true _enabl.e_type_dep_sel.ect1 vl ty = true _improved_outerjoi n_card = true _optimi zer_adjust_for_nulls = true _optimizer_degree = 0 _use_column_stats_for_function = true _subquery_pruning_enabled = true
Файл трассировки 453 = false = true = false = true = true = true = true = true = true = true = false = auto = true = true = true = 2 = true true = yes_gset_mvs = true = true = false = typical = true = true = true = 0 = true = false = true = 100 = true true _subquery_pruning_mv_enabled _or_expand_nvl_predi cate _like_wi th_bi nd_as_equali ty _table_scan_cost_plus_one _c os t_eq u aIi ty_s em i _jо i n _default_non_equali ty_sel_check _n ew_i n i t i al_j о i n_o r de r s _oneside_colstat_for_equi j oi ns _optim_peek_user_bi nds _mi nimal_stats_aggregation _force_temptables_for_gsets workarea_si ze_poli cy _smm_auto_cost_enabled _gs_anti_semi_joi n_allowed _optim_new_default_]'oi n_sel optimi zer_dynamic_sampli ng _pre_rewrite_push_pred _opti mi zer_new_joi n_card_computati on _uni on_rewri te_for_gs _generali zed_pruni ng_enabled _optim_adj ust_for_part_skews _force_datefold_trunc statistics_level _optimi zer_system_stats_usage ski p_unusable_i ndexes _remove_aggr_subquery _optimizer_push_down_disti net _dml_moni toring_enabled _optimizer_undo_changes _predi cate_eli mi nati on_enabled _nested_loop_fudge _project_view_columns _local_communication_costi ng_enabled _local_communication_ratio = 50 _query_rewrite_vop_cleanup = true _slave_mapping_enabled = true _optimizer_cost_based_transformation = linear _optimizer_mjc_enabled = true _right_outer_hash_enable = true _spr_push_pred_refspr = true _optimizer_cache_stats = false _optimizer_cbqt_factor = 50 _optimizer_squ_bottomup = true _fic_area_size = 131072 _optimizer_skip_scan_enabled = true _optimizer_cost_filter_pred = false _optimizer_sortmerge_]oin_enabled = true _optimizer_join_sel_sanity_check = true _mmv_query_rewrite_enabled = false _bt_mmv_query_rewrite_enabled = true _add_stale_mv_to_dependency_list = true _distinct_view_unnesting = false _optimizer_dim_subq_]'oin_sel = true _optimizer_disable_strans_sanity_checks = 0 _optimizer_compute_index_stats = true _push_]oin_union_view2 = true
454 Глава 14. Файл трассировки 10053 _optimizer_1gnore_hints = false _optimizer_random_plan = 0 _query_rewrite_setopgrw_enable = true _optimizer_correct_sq_selectivity = true _disable_function_based_index = false _optimizer_join_order_control = 3 _optimizer_push_pred_cost_based = true **********:***:****:*****:***:*****:***:***:***: Column Usage Monitoring is ON: tracking level = 1 *************************************** Блоки запроса Блоки запроса (под которыми, проще говоря, подразумеваются подзапросы и подставляемые представления) в версии 10g получают названия. Вы можете явно назвать блок с помощью подсказки qb_name и использовать его для указа- ния псевдонимов объектов в глобальных подсказках. Если вы не зададите имя, Oracle сгенерирует имя для каждого блока самостоятельно. Если у вас есть сложный запрос, включающий, например, подзапрос или представление, по ко- торым не нужно выполнять слияние, то вы можете обнаружить, что в файле трассировки 10053 может быть несколько отдельных разделов — по одному разде- лу для каждого блока запроса, по которому нельзя выполнять слияние, и раздел, в котором выполняется соединение частей, по которым нельзя выполнять слияние. Именование блока запроса помогает увидеть, как запрос делится на части с помощью перечисления таблиц или, точнее, с помощью псевдонимов, (f ro(N)), которые указаны в этом блоке. Похоже, псевдонимы выводятся в ал- фавитном порядке. QUERY BLOCK SIGNATURE ********************* qb name was generated signature (optimizer): qb_name=SEL$l nbfros=4 flg=0 fro(0): flg=0 ob]’n=58048 hint_alias="C"@"SEL$l" fro(l): flg=0 objn=58045 hint_alias=”GGP"@"SEL$l" fro(2): flg=0 objn=58046 hint_alias="GP"@''SEL$l" fro(3): flg=0 objn=58047 hint_alias="P"@"SEL$l" Хранимая статистика Следующий раздел файла трассировки является простым выводом основной статистики задействованных таблиц и индексов, которые могут быть использо- ваны. В этих значениях просто дублируются значения из user_tabl.es, user_ tab_col_statisties и user_i ndexes (или из эквивалентных секций). Если вы используете секционирование, то вы увидите соответствующие комментарии (в composite stats) или заголовок для определенной секции, идентифицирую- щий секцию по номеру, например PARTITION [0]. Эти комментарии появляют- ся только на уровне таблицы и индекса, но не на уровне столбца. В файле трассировки выводятся только те столбцы, которые могут быть ис- пользованы для фильтрации и доступа к данным — более ранние версии Oracle выводят их в разных местах файла трассировки.
Файл трассировки 455 Существует небольшая странность, связанная с гистограммами: для этого запроса я не создавал гистограмм, но, тем не менее, Oracle выводит 1 uncom- pressed buckets (1 несжатая хэш-группа). Для этого столбца выводятся толь- ко наименыпее/наиболыпее значения (в версии 10g эти значения в трассировке показаны как Min:/Мах :). При отсутствии гистограмм обратите внимание, что количество уникальных значений (number of distinct values, NDV) для каждого столбца равно i/плот- ность. Также есть небольшая интересная деталь: таблицы выводятся в порядке, об- ратном порядку в выражении from. *************************************** Основная статистическая информация *********************** Table stats Table: CHILD Alias: C TOTAL :: CDN: 40000 NBLKS: 10000 AVG_ROW_LEN: 1632 COLUMN: ID_P(NUMBER) Col#: 3 Table: CHILD Alias: C Size: 4 NDV: 10000 Nulls: 0 Density: 1.0000e-004 Min: 1 Max: 10000 No Histogram: #BKT: 1 (1 uncompressed buckets and 2 endpoint values) COLUMN: ID_GP(NUMBER) Col#: 2 Table: CHILD Alias: C Size: 4 NDV: 2000 Nulls: 0 Density: 5.0000e-004 Min: 1 Max: 2000 No Histogram: #BKT: 1 (1 uncompressed buckets and 2 endpoint values) COLUMN: ID_GGP(NUMBER) Col#: 1 Table: CHILD Alias: C Size: 4 NDV: 1000 Nulls: 0 Density: 1.0000e-003 Min: 1 Max: 1000 No Histogram: #BKT: 1 (1 uncompressed buckets and 2 endpoint values) Index stats Index: C_PK COL#: 1 234 TOTAL :: LVLS: 1 #LB: 108 #DK: 40000 LB/K: 1 DB/K: 1 CLUF: 40000 *********************** Table stats Table: PARENT Alias: P TOTAL CDN: 10000 NBLKS: 2500 AVG_ROW_LEN: 1627 COLUMN: ID_GP(NUMBER) Col#: 2 Table: PARENT Alias: P Size: 4 NDV: 2000 Nulls: 0 Density: 5.0000e-004 Min: 1 Max: 2000 No Histogram: #BKT: 1 (1 uncompressed buckets and 2 endpoint values) COLUMN: ID_GGP(NUMBER) Col#: 1 Table: PARENT Alias: P Size: 4 NDV: 1000 Nulls: 0 Density: 1.0000e-003 Min: 1 Max: 1000 No Histogram: #BKT: 1 (1 uncompressed buckets and 2 endpoint values) COLUMN: ID(NUMBER) Col#: 3 Table: PARENT Alias: P Size: 4 NDV: 10000 Nulls: 0 Density: 1.0000e-004 Min: 1 Max: 10000 No Histogram: #BKT: 1 (1 uncompressed buckets and 2 endpoint values) COLUMN: ID_GP(NUMBER) Col#: 2 Table: PARENT Alias: P Size: 4 NDV: 2000 Nulls: 0 Density: 5.0000e-004 Min: 1 Max: 2000 No Histogram: #BKT: 1 (1 uncompressed buckets and 2 endpoint values) COLUMN: ID_GGP(NUMBER) Col#: 1 Table: PARENT Alias: P Size: 4 NDV: 1000 Nulls: 0 Density. 1.0000e-003 Min; 1 Max: 1000 No Histogram: #BKT: 1
456 Глава 14. Файл трассировки 10053 (1 uncompressed buckets and 2 endpoint values) Index stats Index: P_PK COL#: 123 TOTAL :: LVLS: 1 #LB: 24 #DK: 10000 LB/K: 1 DB/K: 1 CLUF: 10000 *********************** Table stats Table: GRANDPARENT Alias: GP TOTAL :: CDN: 2000 NBLKS: 500 AVG_ROW_LEN: 1623 COLUMN: ID_GGP(NUMBER) Col#: 1 Table: GRANDPARENT Alias: GP Size: 4 NDV: 1000 Nulls: 0 Density: 1.0000e-003 Min: 1 Max: 1000 No Histogram: #BKT: 1 (1 uncompressed buckets and 2 endpoint values) COLUMN: ID(NUMBER) Col#: 2 Table: GRANDPARENT Alias: GP Size: 4 NDV: 2000 Nulls: 0 Density: 5.0000e-004 Min: 1 Max: 2000 No Histogram: #BKT: 1 (1 uncompressed buckets and 2 endpoint values) COLUMN: ID_GGP(NUMBER) Col#: 1 Table: GRANDPARENT Alias: GP Size: 4 NDV: 1000 Nulls: 0 Density: 1.0000e-003 Min: 1 Max: 1000 No Histogram: #BKT: 1 (1 uncompressed buckets and 2 endpoint values) Index stats Index: GP_PK COL#: 1 2 TOTAL :: LVLS: 1 #LB: 6 #DK: 2000 LB/K: 1 DB/K: 1 CLUF: 2000 *********************** Table stats Table: GREATGRANDPARENT Alias: GGP TOTAL :: CDN: 1000 NBLKS: 250 AVG_R0W LEN: 1619 COLUMN; ID(NUMBER) Col#: 1 Table: GREATGRANDPARENT Alias: GGP Size: 4 NDV: 1000 Nulls: 0 Density: 1.0000e-003 Min: 1 Max: 1000 No Histogram: #BKT: 1 (1 uncompressed buckets and 2 endpoint values) Index stats Index: GGP_PK COL#: 1 TOTAL :: LVLS: 1 #LB: 2 #DK: 1000 LB/K: 1 DB/K: 1 CLUF: 250 _OPTIMIZER_PERCENT_PARALLEL = 0 Одиночные таблицы В этом разделе рассматриваются способы доступа к «одиночной таблице», ко- гда оптимизатор отдельно для каждой таблицы определяет количество записей, которые будут получены, и самую низкую стоимость их получения, основыва- ясь на предположении, что способами доступа к таблице являются только те, которые содержат константы (указанные изначально или полученные из пере- менных связывания), присутствующие в исходном запросе. В этом случае оптимизатор игнорирует любые условия для столбцов, отно- сящиеся к условиям соединения; однако при этом он использует ограничения базы данных и применяет переходное замкнутое выражение для создания но- вых условий, например: Если у вас есть предикаты coll = 'х' and col2 = coll то Oracle рассматривает их как со12 = 'х1 или
Файл трассировки 457 Если у вас есть ограничение coIX = upper(colx) то Oracle может определить предикат как upper(colX) = 'АВС что приведет к предикату coIX = 'АВС Такая обработка ограничений немного изменилась в версии 10.1 и больше не работает, когда константы заменяются переменными связывания. Возможно, это ошибка, но возможно, это сделано специально, чтобы избежать неверных результатов при наличии значений NULL. Ниже показан рабочий пример для таблицы greatgrandparent: У нас есть предикат: small_num_ggp between 100 and 150 Количество уникальных значений (Number of distinct values - NDV) = 200 Количество значений NULL (NULLS) = 0 Плотность (DENS) = 0.005 (1/200) Наименьшее значение (Min) = 0 Наибольшее значение (Max) = 199 Согласно формуле селективности для диапазона ‘between’. Селективность = (val2 - vail) / (наибольшее значение - наименьшее значение) + 2 / num_distinct = 50/199 + 2/200 = 0.261256 Умножьте результат на 1000 записей и округлите для получения рассчитан- ной кардинальности, равной 261. Стоимость табличного сканирования приве- дена в табл. 14.1, но обратите внимание, что BEST_CST (лучшая стоимость) по- лучения данных равна 64,41, в то время как приведенная в табл. 14.1 стоимость равна всего лишь 64. Разница в стоимости возникает из-за компонента процес- сора. К сожалению, файл трассировки не показывает общую стоимость отдель- но в виде 10 cost (стоимость ввода-вывода) и CPU cost (стоимость использова- ния ресурсов процессора), как это происходит в других местах. *************************************** SINGLE TABLE ACCESS PATH COLUMN: SMALL_NUM_(NUMBER) Col#: 2 Table: GREATGRANDPARENT Alias: GGP Size: 4 NDV: 200 Nulls: 0 Density: 5.0000e-003 Min: 0 Max: 199 No Histogram: #BKT: 1 (1 uncompressed buckets and 2 endpoint values) TABLE: GREATGRANDPARENT Alias: GGP Original Card: 1000 Rounded: 261 Computed: 261.26 Non Adjusted: 261.26 Access Path: table-scan Resc: 64 Resp: 64 BEST_CST: 64.41 PATH: 2 Degree: 1 ft*********:*:*:*:*:*:*:*:*:*:*:*:*:*:*:*:*:**:*:*:*:*:*:*:*:*:*:*:*: SINGLE TABLE ACCESS PATH COLUMN: SMALL_NUM_(NUMBER) Col#: 3 Table: GRANDPARENT Alias: GP Size: 4 NDV: 400 Nulls: 0 Density: 2.5000e-003 Min: 0 Max: 399 No Histogram: #BKT: 1 (1 uncompressed buckets and 2 endpoint values) TABLE: GRANDPARENT Alias: GP Original Card: 2000 Rounded: 110 Computed: 110.25 Non Adjusted: 110.25
458 Глава 14. Файл трассировки 10053 Access Path: table-scan Resc: 128 Resp: 128 BEST_CST: 127.82 PATH: 2 Degree: 1 *************************************** SINGLE TABLE ACCESS PATH COLUMN: SMALL_NUM_(NUMBER) Col#: 4 Table: PARENT Alias: P Size: 4 NDV: 2000 Nulls: 0 Density: 5.0000e-004 Min: 0 Max: 1999 No Histogram: #BKT: 1 (1 uncompressed buckets and 2 endpoint values) TABLE: PARENT Alias: P Original Card: 10000 Rounded: 110 Computed: 110.05 Non Adjusted: 110.05 Access Path: table-scan Resc: 631 Resp: 631 BEST_CST: 631.09 PATH: 2 Degree: 1 *************************************** SINGLE TABLE ACCESS PATH COLUMN: SMALL_NUM_(NUMBER) Col#: 5 Table: CHILD Alias: C Size: 4 NDV: 10000 Nulls: 0 Density: 1.0000e-004 Min: 0 Max: 9999 No Histogram: #BKT: 1 (1 uncompressed buckets and 2 endpoint values) TABLE: CHILD Alias: C Original Card: 40000 Rounded: 68 Computed: 68.01 Non Adjusted: 68.01 Access Path: table-scan Resc: 2517 Resp: 2517 BEST_CST: 2517.49 PATH: 2 Degree: 1 Контроль ошибок Следующий раздел является нововведением версии 10g, относящимся к воз- можности контроля ошибок (sanity check) при соединении с использованием множества столбцов. Если между двумя таблицами существуют два (или боль- ше) условия соединения, то оптимизатор обрабатывает каждое условие отдель- но, чтобы решить, в каких таблицах какая селективность. В версии 10g оптими- затор получает селективность только с одной стороны соединения. Для этого он действует двумя способами. Если соединение покрывает весь составной ин- декс, то Oracle будет рассматривать столбец disti nct_keys из представления user_i ndexes в качестве возможной селективности соединения. Если подходя- щего индекса нет, то Oracle рассматривает произведение селективностей из од- ной таблицы или из другой, но не выбирает их с каждой стороны соединения. В выведенном значении concatenated index card (кардинальность состав- ного индекса) есть небольшая странность. В этом примере таблица child выво- дится с кардинальностью, равной 10 000 — что равно значению di sti nct_keys индекса таблицы parent, по которой производится соединение. Но оптимиза- тор просто сообщает нам, что это значение является самым большим возмож- ным количеством уникальных значений в нашей выборке из таблицы child из-за соединения с таблицей pa rent, а мы знаем, что в таблице parent хранятся только 10 000 значений. Аналогично, таблица parent выводится с кардиналь- ностью, равной 2000 — что равно значению distinct_keys таблицы great- grandparent, по которой происходит соединение. В файлах трассировки версии 9i вы обнаружите строки по составным индек- сам, но не обнаружите строк по ключу соединения с множеством столбцов.
Файл трассировки 459 Table: CHILD Concatenated index card: 10000.000000 Table: PARENT Concatenated index card: 2000.000000 Table: CHILD Multi-column join key card: 40000.000000 Table: PARENT Multi-column join key card: 10000.000000 Table: PARENT Multi-column join key card: 10000.000000 Table: GRANDPARENT Multi-column join key card: 2000.000000 *************************************** OPTIMIZER STATISTICS AND COMPUTATIONS *************************************** Общие планы выполнения Наконец мы переходим к соединениям. В этом случае нам придется иметь дело с 24 перестановками. Первые шесть могут начинаться с таблицы great- grandparent: ggp-gp-p-c, ggp-gp-c-p, ggp-p-gp-c, ggp-p-c-gp, ggp-c-gp-p, ggp-c-p-gp. После этого Oracle может выполнить шесть соединений, начинающихся с таблицы greatgrandparent, потом шесть с таблицей parent и еще шесть с таблицей child. Будет ли он проверять их все, и какая же таблица окажется первой? Первый вопрос довольно прост, ответ на него — не обязательно. Для очень короткого списка таблиц (до пяти включительно) оптимизатор может рассмат- ривать все возможные порядки соединений, но даже в этом случае он может не- которые из них практически сразу проигнорировать. Для ответа на второй вопрос нужно чуть больше времени. Рассмотрим ре- зультаты из секции Single Table Access Path, показанные в табл. 14.2. Таблица 14.2. Результаты расчетов из секции Single Table Access Path Имя таблицы Стоимость Рассчитанная кардинальность greatgrandparent 64,41 261,26 grandparent 127,82 110,25 parent 631,09 110,05 child 2517,49 68,01 Первый порядок соединения (раздел Join order[l]) При проверке первым порядком окажется: child, parent, grandparent и great- grandparent. Из этого мы можем сделать два предположения. Либо первый по- рядок соединения соответствует порядку рассчитанной кардинальности, либо он соответствует обратному порядку стоимости (тест показывает, что он соот- ветствует порядку кардинальности). На самом деле существует специальное правило «таблицы, возвращающей одну запись». Таблицы, которые точно возвращают одну запись (обычно это значит, что используется предикат с одиночным значением на уникальном/пер- вичном ключе таблицы, хотя это может быть и простое агрегатное представление), перемещаются в начало списка и остаются там. Конечно, эти таблицы могут быть задействованы в соединениях с декартовым произведением, но соединение
460 Глава 14. Файл трассировки 10053 с декартовым произведением, включающее две таблицы, возвращающие по од- ной записи, все равно выдает одну запись, так что их влиянием на стоимость можно пренебречь, и поэтому такие таблицы не играют роли в основных расче- тах стоимости. В правиле «таблицы, возвращающей одну запись», есть исключение: если вы будете использовать подсказку leadingO для определения первой таблицы в порядке соединения, то это правило работать не будет, и все таблицы, кроме первой (первых), будут участвовать в полном процессе оптимизации. Обратите внимание, что каждая таблица имеет свой псевдоним (возможно, сгенерированный системой) и номер после имени в строке Join order []. Эти псевдонимы и номера используются в дальнейшем для ссылки на таблицу на различных стадиях в файле трассировки. GENERAL PLANS *********************** Join order[l] ; CHILD[C]#0 PARENT[P]#1 GRANDPARENT[GP]#2 GREATGRANDPARENT[GGP]#3 Первое соединение (раздел Now joining (1)) За один раз мы всегда производим соединение только с еще одной таблицей. Для каждой таблицы Oracle пытается использовать соединение с использова- нием вложенных циклов, соединение сортировки/слияния (иногда дважды) и соединение хэширования — в таком порядке. Подсказки use_nl, use_merge и use_hash применяются именно в разделе Now joining и указывают Oracle, какой механизм соединения нужно использовать. В случае соединения с ис- пользованием вложенных циклов стоимость будет рассчитываться следующим образом: Стоимость внешней таблицы (2517) + кардинальность внешней таблицы (68) * лучшая стоимость одного обращения к таблице (1) = 2585 Конечно, видно, что значение Best NL cost указано в конце раздела как рав- ное 2586. В отдельном тесте стоимость сканирования и получения нескольких записей из таблицы parent была равна 2517,82, что объясняет небольшую ошибку в наших расчетах. Любая информация из трассировки 10053 будет не- много неточной, потому что полученная информация, во-первых, неполная и, во-вторых, для нас не предназначена. Я думаю, что индекс Р_РК оценивается дважды, потому что одна часть кода использует стандартную формулу для доступа к одиночной таблице по индексу (unique), а другая использует особый случай, в котором уникальный индекс га- рантированно возвращает не больше одной записи (eq_unique). Обратите внимание, что значения ix_sel и ix_sel_with_filters (которые начинались с tb_sel в более ранних версиях Oracle) равны нулю для пара- метра eq_unique. Это явно указывает на тот факт, что специальный расчет стоимости доступа к уникальным значениям по уникальному индексу равен blevel + 1 и не включает сложные расчеты количества обращений к листовым и табличным блокам. Так как предикаты в нашем запросе используют индексы обычным образом (без пропуска столбцов или использования диапазонов на лидирующих столб-
Файл трассировки 461 цах), мы увидим, что во всем примере значение ix_sel будет равно значению ix_sel_wi th_f1Iters. Now joining: PARENT[P]#1 ******* NL Join Outer table: cost: 2517 cdn: 68 rcz: 27 resp: 2517 Inner table: PARENT Alias: P Access Path: table-scan Resc: 629 Join: Resc: 45302 Resp: 45302 Access Path: index (unique) Index: P_PK rsc_cpu: 15620 rsc_io: 1 ix_sel: 1.0000e-004 ix_sel_with_fiIters: 1.0000e-004 NL Join: resc: 2586 resp: 2586 Access Path: index (eq-unique) Index: P_PK rsc_cpu: 15820 rsc_io: 1 ix_sel: 0.0000e+000 ix_sel_with_fiIters: 0.0000e+000 NL Join: resc: 2586 resp: 2586 Best NL cost: 2586 resp: 2586 Кардинальность соединения рассчитывается в конце расчетов стоимости вложенных циклов. Я предполагаю, что по этой причине1 расчеты стоимости вложенных циклов всегда присутствуют в файле трассировки, даже если вы ис- пользуете подсказки use_merge или use_hash в вашем запросе, чтобы избежать использования вложенных циклов. Обратите внимание, как используется контроль ошибок. Традиционный ме- ханизм селективности выдал значение, меньшее, чем 1/(количество записей в таблице), поэтому было использовано другое значение — в данном случае се- лективность полностью использованного индекса Р_РК. Using concatenated index cardinality for table PARENT Revised join selectivity: 1.0000e-004 = 7.9445e-007 * (1/10000) * (l/7.9445e-007) Join Card: 0.75 = outer (68.01) * inner (110.05) * sei (1.0000e-004) Теперь мы рассмотрим соединения сортировки/слияния. В этом примере мы используем один механизм для соединения сортировки/слияния. Немного ниже показан пример, где мы используем два разных параметра для соединения сортировки/ слияния. Изначально по стоимости соединения сортировки/слияния выводилась не- много странная статистика — например, было впечатление, что отношение меж- ду значением стоимости операций ввода-вывода за один проход и значением итоговой стоимости операций ввода-вывода для выполнения сортировки осно- вывается на какой-то необычной формуле. Важные расчеты находятся в Row size (сумма средних длин столбцов) и Total Rows (рассчитанная кардинальность) для каждой таблицы. Oracle рас- считывает стоимость использования ресурсов процессора на сортировку этих записей и возможное количество операций ввода-вывода, которые потребуются в случае, если потребность в памяти превысит ее размер. Так как здесь мы ис- пользуем pga_aggregate_target и пару очень маленьких сортировок, то Area ' size равно _smm_mi n_size (0,1 % от значения pga_aggregate_target, равного 200 Мбайт), a Max Area size равно _smm_max_size (5 % от pga_aggregate_ target).
462 Глава 14. Файл трассировки 10053 В этом случае объем данных из обеих таблиц, которые требуют сортировки, очень мал, поэтому стоимость операций ввода-вывода отсутствует. (Вы также можете заметить, что здесь не используется рассчитанное значение Total Temp space used). Таким образом, стоимость выполнения соединения равна Стоимость получения данных из внешней таблицы + Стоимость сортировки внешней таблицы (в этом случае используются только ресурсы процессора) + Стоимость получения данных из внутренней таблицы + Стоимость сортировки внутренней таблицы (в этом случае используются только ресурсы процессора) Чтобы перевести итоговую стоимость использования ресурсов процессора на сортировку в стандартные единицы измерения, мы разделим результат на 5 000 000 (500 МГц х 10 000 мс из sreadtim), чтобы получить: Стоимость соединения = 2517 + 5 018 650/5 000 000 + 631 + 5 033 608/5 000 000 = 3148 + 2 плюс еще немного В конце следующего раздела вы можете увидеть, что действительная стои- мость слияния равна 3151, что очень похоже на 3150 плюс еще немного, как я только что рассчитал. Так как я не могу гарантировать, когда и в какую сторо- ну будет выполнено округление, я не могу точно сказать, откуда берется разни- ца в единицу — из-за округления или из-за небольшого значения стоимости, ко- торое напрямую относится к самой операции слияния. SM Join Outer table: resc: 2517 cdn: 68 rcz: 27 deg: 1 resp: 2517 Inner table: PARENT Alias: P resc: 631 cdn; 110 rcz: 27 deg: 1 resp: 631 using join:l distribution:2 #groups:l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 40 Total Rows: 68 Initial runs: 1 Merge passes: 0 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5018650 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree; 1 Blocks to Sort: 1 Row size: 40 Total Rows: 110 Initial runs: 1 Merge passes: 0 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5033608 Total Temp space used: 0 Merge join Cost: 3151 Resp: 3151 Наконец, рассмотрим соединение хэширования. Для оптимальных соедине- ний хэширования стоимость соединения равна чуть большему значению, чем стоимость получения двух наборов данных. В этом случае: Стоимость получения данных из внешней таблицы = 2517 Стоимость получения данных из внутренней таблицы = 631 Стоимость соединения хэширования = 3149 = 2517 + 631 + 1
Файл трассировки 463 Дополнительная единица — это стоимость соединения хэширования на одну хэш-секцию (Hash join one ptn (partition) cost). HA Join Outer table: resc: 2517 cdn: 68 rcz: 27 deg: 1 resp: 2517 Inner table: PARENT Alias: P resc: 631 cdn: 110 rcz: 27 deg: 1 resp: 631 using join:8 distribution.^ #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag: 1 probefrag: 1 ppasses: 1 Hash join Resc: 3149 Resp: 3149 Мы протестировали все три (четыре) механизма соединения со следующими значениями лучшей стоимости в каждом случае: NL 2586 SM 3151 НА 3149 Итак, мы выводим самую низкую стоимость и помним, что это стоимость на формирование промежуточного набора данных. Join result: cost: 2586 cdn: 1 rcz: 54 Второе соединение (раздел Now joining (2)) Оптимизатор всегда соединяет только две таблицы — так что теперь пришло время соединить следующую таблицу в текущем порядке соединения с нашим промежуточным набором данных. Конечно, внешняя таблица на самом деле таблицей не является. Now joining: GRANDPARENT[GP]#2 ******* NL Join Outer table: cost: 2586 cdn: 1 rcz: 54 resp: 2586 Inner table: GRANDPARENT Alias: GP Access Path: table-scan Resc: 128 Join: Resc: 2714 Resp: 2714 Access Path: index (unique) Index: GP_PK rsc_cpu: 15589 rsc_io: 1 ix_sel: 5.0000e-004 ix_sel_with_fliters: 5.0000e-004 NL Join: resc: 2587 resp: 2587 Access Path: index (eq-unique) Index: GP_PK rsc_cpu: 15789 rsc_io: 1 ix_sel: 0.0000e+000 ix_sel_with_fliters: 0.0000e+000 NL Join: resc: 2587 resp: 2587 Best NL cost: 2587 resp: 2587 Using concatenated index cardinality for table GRANDPARENT Revised join selectivity: 5.0000e-‘004 = 8.3417e-005 * (1/2000) * (1/8.3417e-005) Join Card: 0.04 = outer (0.75) * inner (110.25) * sei (5.0000e-004) SM Join Outer table: resc: 2586 cdn: 1 rcz: 54 deg: 1 resp: 2586 Inner table: GRANDPARENT Alias: GP resc: 128 cdn: 110 rcz: 23 deg: 1 resp: 128
464 Глава 14. Файл трассировки 10053 using join:l distribution^ #groups:l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 70 Total Rows: 1 Initial runs: 1 Merge passes: 0 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5000000 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size; 36 Total Rows: 110 Initial runs: 1 Merge passes: 0 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5033608 Total Temp space used: 0 Merge join Cost: 2716 Resp: 2716 HA Join Outer table: resc: 2586 cdn: 1 rcz: 54 deg: 1 resp: 2586 Inner table: GRANDPARENT Alias: GP resc: 128 cdn: 110 rcz: 23 deg: 1 resp: 128 using join:8 distribution:! #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag; 1 probefrag: 1 ppasses: 1 Hash join Resc: 2714 Resp: 2714 Join result: cost: 2587 cdn: 1 rcz: 77 Третье соединение (раздел Now joining (3)) Мы выполнили соединение с третьей таблицей, так что теперь к промежуточно- му результату мы добавим и четвертую таблицу. Обратите внимание, что стои- мость выборки из внешней таблицы теперь равна стоимости соединения преды- дущих трех таблиц. Now joining: GREATGRANDPARENT[GGP]#3 ******* NL Join Outer table: cost: 2587 cdn; 1 rcz: 77 resp: 2587 Inner table: GREATGRANDPARENT Alias: GGP Access Path: table-scan Resc: 64 Join: Resc: 2651 Resp: 2651 Access Path: index (unique) Index: GGP_PK rsc_cpu: 15558 rsc_io: 1 1x_sel: 1.0000e-003 ix_sel_with_fliters: 1.0000e-003 NL Join; resc: 2588 resp: 2588 Access Path; index (eq-unique) Index: GGP_PK rsc_cpu: 15758 rsc_io: 1 ix_sel: 0.0000e+000 ix_sel_with_fliters: 0.0000e+000 NL Join: resc: 2588 resp: 2588 Best NL cost: 2588 resp: 2588 Join Card: 0.04 = outer (0.04) * inner (261.26) * sei (3.8168e-003) SM Join
Файл трассировки 465 Outer table: resc: 2587 cdn: 1 rcz: 77 deg: 1 resp: 2587 Inner table: GREATGRANDPARENT Alias: GGP resc: 64 cdn: 261 rcz: 19 deg: 1 resp: 64 using join;l distributions #groups:l SORT resource Sort statistics Sort width: 58 Area size; 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 95 Total Rows: 1 Initial runs: 1 Merge passes: 0 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5000000 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: . 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 31 Total Rows: 261 Initial runs: 1 Merge passes: 0 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5094402 Total Temp space used: 0 Merge join Cost: 2653 Resp: 2653 HA Join Outer table: resc: 2587 cdn: 1 rcz: 77 deg: 1 resp: 2587 Inner table; GREATGRANDPARENT Alias: GGP resc: 64 cdn: 261 rcz: 19 deg: 1 resp: 64 using join:8 distributions #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag: 1 probefrag: 1 ppasses: 1 Hash join Resc: 2652 Resp: 2652 Join result: cost: 2588 cdn: 1 rcz: 96 Лучшая стоимость на данный момент (Best so far) Мы дошли до конца первого порядка соединения. Каждый раз, когда мы дохо- дим до конца порядка соединения, оптимизатор сравнивает полученную стои- мость с лучшей стоимостью на данный момент, и, если полученная стоимость оказалась лучше, она выводится и запоминается. Конечно, в этом случае первое значение стоимости является самым лучшим, потому что предыдущего значе- ния стоимости нет. Экземпляры TABLE# соответствуют номерам таблиц, перечисленным в пер- вом порядке соединения. Значения CST, CDN и BYTES в таблице плана являются стоимостью, карди- нальностью и количеством байтов столбцов соответственно. В конце предыду- щего раздела в Join result (результатах соединения) вы можете увидеть, что у нас есть поля cdn (кардинальность) и rcz (размер записи) — значение BYTES получается из произведения этих двух значений. Best so far: Best so far: TABLE#: 0 TABLE#: 1 CST: CST: 2517 2586 CDN: CDN: 68 1 BYTES: BYTES: 1836 54 Best so far: TABLE#: 2 CST: 2587 CDN: 1 BYTES: 77 Best so far: TABLE#: 3 CST: 2588 CDN: 1 BYTES: 96 «**4с4с**4с4с***4с4с**4с4с***4с*
466 Глава 14. Файл трассировки 10053 Второй порядок соединения (раздел Join order[2]) Мы переходим ко второму порядку соединения. Для небольшого количества таблиц (обычно для пяти или меньше) Oracle просто будет переставлять их в цикле, начиная с конца первого порядка соединения и проходя по всем воз- можным вариантам перестановки. Поэтому второй порядок соединения — это просто перестановка двух последних таблиц. Join order[2]: CHILD[C]#0 PARENT[P]#1 GREATGRANDPARENT[GGP]#3 GRANDPARENT[GP]#2 Но подождите: почему следующая строка в файле трассировки показывает, что мы выполняем соединение с таблицей greatgrandparent? Ведь вместо нее должна была быть таблица parent? Нет, потому что Oracle запомнил, что пер- вый порядок соединения начинался с child -> parent, и перенес промежуточ- ные результаты первого порядка соединения во второй. Помните, что по мере роста «размера» команд SQL объем памяти, используемый для их оптимиза- ции, увеличивается. Частично это вызвано тем, что Oracle требуется запомнить все большее количество промежуточных результатов по мере увеличения коли- чества таблиц. Now joining: GREATGRANDPARENT[GGP]#3 ******* NL Join Outer table: cost: 2586 cdn: 1 rcz: 54 resp: 2586 Inner table: GREATGRANDPARENT Alias: GGP Access Path: table-scan Resc: 64 Join: Resc: 2650 Resp; 2650 Best NL cost: 2650 resp: 2650 Join Card: 195.53 = outer (0.75) * inner (261.26) * sei (1,0000e+000) *********************** Что же произошло с соединением сортировки/слияния и соединением хэ- ширования? Где строка Now Joi nt ng: GRANDPARENT? Мы переходим к третьему порядку соединения без завершения второго порядка соединения. Посмотрите на стоимость выполнения соединения с использованием вложенных циклов с таблицей greatgrandparent (2650) — они уже превысили дучшую стоимость (2588) для всего запроса, так что нет смысла продолжать работу с этим поряд- ком соединения, поэтому оптимизатор переходит к следующему порядку. Причина, по которой вы не видите соединение слияния и соединения хэши- рования, заключается в том, что соединение с таблицей greatgrandparent яв- ляется декартовым произведением, так что лучшая стоимость соединения со- стояла бы как минимум из суммы стоимости выборки из внешней таблицы и стоимости выборки из внутренней таблицы. Нет смысла повторять это три раза. В этом порядке запроса есть небольшая тонкость, которую сразу можно не заметить. Oracle использовал соединение child -> grandparent как декартово произведение — но мы знаем (из кода определения таблиц), что первичный ключ таблицы child на самом деле начинается с ключа greatgrandparent и что этот столбец должен был быть использован в соединении. Но в этом при- мере Oracle не использует переходное замкнутое выражение. Мы можем увидеть (изменив наш запрос), что
Файл трассировки 467 child.id_ggp = parent.id_ggp and parent.id_ggp = grandparent.id_ggp and grandparent.id_ggp = greatgrandparent.id Так что мы можем сделать вывод, что child.id_ggp = greatgrand- parent. id. Но оптимизатор не пытается использовать такую логику. Третий порядок соединения (раздел Join order[3]) При переходе к третьему порядку соединения изменение порядка производится на один шаг ближе к началу порядка соединения. Мы протестировали все по- рядки, начинающиеся с (child, parent), так что теперь перейдем к порядкам соединения, начинающимся с (child, grandparent). Мы снова останавливаемся без полного тестирования порядка соединения и без тестирования соединений слияния и хэширования на самом первом со- единении. Более того, мы даже не выводим соединение для следующего порядка (child, grandparent, greatgrandparent, parent). Это просто не нужно, пото- му что мы уже знаем, что все порядки, начинающиеся с (child, grandparent), имеют слишком высокую стоимость. Join order 13]: CHILDIC]#0 GRANDPARENT[GP]#2 PARENT[P]#1 GREATGRANDPARENT[GGP]#3 Now joining: GRANDPARENT[GP]#2 ******* NL Join Outer table: cost: 2517 cdn: 68 rcz: 27 resp: 2517 Inner table: GRANDPARENT Alias: GP Access Path: table-scan Resc: 126 Join: Resc; 11074 Resp: 11074 Best NL cost: 11074 resp: 11074 Join Card: 7497.70 = outer (68.01) * inner (110.25) * sei (1.0000e+000) *********************** Четвертый порядок соединения (раздел Join order[4]) To же происходит при указании следующей таблицы на втором месте (child, greatgrandparent): мы оцениваем первое соединение (только вариант соеди- нения с использованием вложенных циклов) и обнаруживаем, что это решение требует более высокой стоимости, чем лучшая стоимость, так что нам не нужно выполнять полное тестирование порядка соединения и оценивать любые дру- гие порядки соединения, которые начинаются так же. Join order[4]: CHILD[C]#0 GREATGRANDPARENT[GGP]#3 PARENT[P]#1 GRANDPARENT[GP]#2 Now joining: GREATGRANDPARENT[GGP]#3 ******* NL Join Outer table: cost: 2517 cdn: 68 rcz: 27 resp: 2517 Inner table: GREATGRANDPARENT Alias: GGP Access Path: table-scan Resc: 63 Join: Resc: 6796 Resp: 6796 Best NL cost: 6796 resp: 6796 Join Card: 17766.99 = outer (68.01) * inner (261.26) * sei (1.0000e+000) *********************** Пятый порядок соединения (раздел Join order[5]) Это порядок соединения, который будет завершен. Это неудивительно, если мы посмотрим на имена таблиц, — мы начинаем с пары таблиц, которые связаны
468 Глава 14. Файл трассировки 10053 друг с другом, и к моменту их соединения с третьей таблицей (grandparent) у нас все еще есть данные для соединения. Нам бы не так повезло, если бы третьей таблицей была greatgrandparent. Join order[5]: PARENT[P]#1 CHILD[C]#0 GRANDPARENT[GP]#2 GREATGRANDPARENT[GGP]#3 Now joining: CHILD[C]#0 ******* NL Join Outer table: cost: 631 cdn: 110 rcz: 27 resp: 631 Inner table: CHILD Alias: C Access Path: table-scan Resc: 2517 Join: Resc: 277488 Resp: 277488 Access Path: index (scan) Index: C_PK rsc_cpu: 15543 rsc_io: 2 ix_sel: 5.0000e-011 ix_sel_with_fliters: 5.0000e-011 NL Join: resc: 851 resp: 851 Best NL cost: 851 resp: 851 Using concatenated index cardinality for table PARENT Revised j’oin selectivity: 1.0000e-004 = 7.9445e-007 * (1/10000) * (l/7.9445e-007) Join Card: 0.75 = outer (110.05) * inner (68.01) * sei (1.0000e-004) SM Join Outer table: resc: 631 cdn: 110 rcz: 27 deg: 1 resp: 631 Inner table: CHILD Alias: C resc: 2517 cdn: 68 rcz: 27 deg: 1 resp: 2517 using joinil distributions #groups:l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 40 Total Rows: 110 Initial runs: 1 Merge passes: 0 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5033608 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree; 1 Blocks to Sort: 1 Row size: 40 Total Rows: 68 Initial runs: 1 Merge passes: 0 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5018650 Total Temp space used: 0 Merge join Cost: 3151 Resp: 3151 HA Join Outer table: resc: 631 cdn: 110 rcz: 27 deg: 1 resp: 631 Inner table: CHILD Alias: C resc: 2517 cdn: 68 rcz: 27 deg; 1 resp: 2517 using join:8 distributions #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag: 1 probefrag: 1 ppasses: 1 Hash join Resc: 3149 Resp: 3149 Наше текущее значение наилучшей стоимости для полного соединения рав- но 2588, поэтому имеет смысл продолжать оценку этого порядка соединения, потому что стоимость соединения первых двух таблиц равна 851.
Файл трассировки 469 Join result: cost: 851 cdn: 1 rcz: 54 Now joining: GRANDPARENT[GP]#2 ******* NL Join Outer table: cost: 851 cdn: 1 rcz: 54 resp: 851 Inner table: GRANDPARENT Alias: GP Access Path: table-scan Resc: 128 Join: Resc: 979 Resp: 979 Access Path: index (unique) Index; GP_PK rsc_cpu: 15589 rsc_io; 1 ix_sel: 5.0000e-004 ix_sel_with_fiIters; 5.0000e-004 NL Join: resc: 852 resp: 852 Access Path: index (eq-unique) Index: GP_PK rsc_cpu: 15789 rsc_io: 1 ix_sel: 0.0000e+000 ix_sel_with_fliters: 0.OOO0e+0OO NL Join: resc: 852 resp: 852 Best NL cost: 852 resp: 852 Using concatenated index cardinality for table GRANDPARENT Revised join selectivity: 5.0000e-OO4 = 8.3417e-005 * (1/2000) * (l/8.3417e-005) Join Card: 0.04 = outer (0.75) * inner (110.25) * sei (5.0000e-004) SM Join Outer table: resc; 851 cdn: 1 rcz: 54 deg; 1 resp: 851 Inner table: GRANDPARENT Alias: GP resc: 128 cdn: 110 rcz: 23 deg: 1 resp: 128 using join;l distribution^ #groups:l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 70 Total Rows: 1 Initial runs: 1 Merge passes: 0 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5000000 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 36 Total Rows: 110 Initial runs: 1 Merge passes: 0 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5033608 Total Temp space used: 0 Merge join Cost: 981 Resp: 981 HA Join Outer table: resc: 851 cdn: 1 rcz: 54 deg: 1 resp: 851 Inner table: GRANDPARENT Alias: GP resc: 128 cdn: 110 rcz: 23 deg: 1 resp: 128 using join:8 distribution^ #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag: 1 probefrag: 1 ppasses: 1 Hash join Resc: 980 Resp: 980
470 Глава 14. Файл трассировки 10053 Наше текущее значение наилучшей стоимости равно 2588, поэтому имеет смысл продолжать оценку этого порядка соединения, потому что стоимость со- единения первых трех таблиц равна 852. Join result: cost: 852 cdn: 1 rcz: 77 (Vow joining: GREATGRANDPARENHGGPI#! ******* NL Join Outer table: cost: 852 cdn: 1 rcz: 77 resp: 852 Inner table: GREATGRANDPARENT Alias: GGP Access Path: table-scan Resc: 64 Join: Resc: 917 Resp: 917 Access Path: index (unique) Index: GGP_PK rsc_cpu: 15558 rsc_io: 1 ix_sel: l.O0OOe~003 ix_sel_with_fliters: 1.0000e-003 NL Join: resc: 853 resp: 853 Access Path: index (eq-unique) Index: GGP_PK rsc_cpu: 15758 rsc_io: 1 ix_sel: 0.0000e+QQ0 ix_sel_with_filters: 0.0000e+000 NL Join; resc: 853 resp: 853 Best NL cost: 853 resp: 853 Join Card: 0.04 = Outer (0.04) * inner (261.26) * sei (3.8168e-003) SM Join Outer table: resc: 852 cdn; 1 rcz; 77 deg: 1 resp: 852 Inner table: GREATGRANDPARENT Alias: GGP resc: 64 cdn: 261 rcz; 19 deg; 1 resp: 64 using join:l distribution^ #groups:l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485750 Degree: 1 Blocks to Sort: 1 Row size: 95 Total Rows: 1 Initial runs: 1 Merge passes: 0 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5000000 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 31 Total Rows: 261 Initial runs: 1 Merge passes: 0 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost; 5094402 Total Temp space used: 0 Merge join Cost: 919 Resp: 919 HA Join Outer table: resc: 852 cdn: 1 rcz: 77 deg: 1 resp: 852 Inner table: GREATGRANDPARENT Alias: GGP resc; 64 cdn: 261 rcz: 19 deg; 1 resp: 64 using join;8 distribution^ #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag; 1 probefrag: 1 ppasses: 1 Hash join Resc; 917 Resp: 917 Join result: cost: 853 cdn: 1 rcz: 96
Файл трассировки 471 В пятом порядке соединения мы имеем новое лучшее значение стоимости, равное 853. Теперь оно становится той целью,(пределом), с которой будут срав- ниваться стоимости других порядков соединения. Best SO far: TABLE#: 1 CST: 631 CDN: 110 BYTES; 2970 Best so far: TABLE#; 0 CST: 851 CDN; 1 BYTES; 54 Best so far: TABLE#: 2 CST: 852 CDN: 1 BYTES: 77 Best so far: TABLE#: 3 CST: 853 CDN: 1 BYTES: 96 Шестой порядок соединения (раздел Join order[6]) В пятом порядке соединения у нас были таблицы (parent, chi Id, grandparent, greatgrandparent), так что следующая перестановка просто меняет местами две последние таблицы. Это значйт, что мы можем использовать промежуточ- ные результаты из (parent, child) и сразу перейти к соединению с таблицей greatgrandparent — еще одному из соединений, для которых Oracle не видит связи с промежуточным результатом, хотя они и не являются декартовыми произведениями. Поэтому мы практически сразу отказываемся от завершения шестого порядка соединения. Join order[6]: PARENT[P]#1 CHILD[C]#0 GREATGRANDPARENT[GGP]#3 GRANDPARENT[GP]#2 Now joining: GREATGRANDPARENT[GGP]#3 ******* NL Join Outer table: cost: 851 cdn: 1 rcz: 54 resp: 851 Inner table: GREATGRANDPARENT Alias: GGP Access Path: table-scan Resc: 64 Join: Resc: 916 Resp: 916 Best NL cost: 916 resp: 916 Join Card: 195.53 = outer (0.75) * inner (261.26) * sei (1.0000e+0O0) *****4c**4c**4c4c4c4c4c4c4c***** Седьмой порядок соединения (раздел Join order[7]) Это еще один порядок соединения, который будет завершен и приведет к появ- лению нового наилучшего значения стоимости на данный момент. Join order[7]: PARENT[P]#1 GRANDPARENT[GP]#2 CHILD[C]#0 GREATGRANDPARENT[GGP]#3 Now joining: GRANDPARENT[GP]#2 ******* NL Join Outer table: cost: 631 cdn: 110 rcz: 27 resp: 631 Inner table: GRANDPARENT Alias: GP Access Path: table-scan Resc: 126 Join: Resc: 14473 Resp: 14473 Access Path: index (unique) Index: GP_PK rsc_cpu: 15589 rsc_io: 1 ix_sel: 5.O000e-004 ix_sel_with_filters: 5.00O0e-004 NL Join: resc: 741 resp: 741 Access Path: index (eq-unique) Index: GP_PK rsc_cpu: 15789 rsc_io: 1 ix_sel: 0.0000e+000 ix_sel_with_fliters: 0.0000e+000 NL Join: resc: 741 resp: 741 Best NL cost: 741 resp: 741 Using concatenated index cardinality for table GRANDPARENT
472 Глава 14. Файл трассировки 10053 Revised join selectivity: 5.0000е-004 = 8.3417е-005 * (1/2000) * (1/8.3417е-005) Join Card: 6.07 = outer (110.05) * inner (110.25) * sei (5.0000e-004) SM Join Outer table: resc: 631 cdn: 110 rcz: 27 deg: 1 resp: 631 Inner table: GRANDPARENT Alias: GP resc: 128 cdn: 110 rcz: 23 deg: 1 resp: 128 using join:l distribution^ #groups:l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 40 Total Rows: 110 Initial runs: 1 Merge passes: 0 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5033608 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 36 Total Rows: 110 Initial runs: 1 Merge passes: 0 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5033608 Total Temp space used: 0 Merge join Cost: 761 Resp: 761 HA Join Outer table: resc: 631 cdn: 110 rcz: 27 deg: 1 resp: 631 Inner table: GRANDPARENT Alias: GP resc: 128 cdn: 110 rcz: 23 deg: 1 resp: 128 using join:8 distribution.^ #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag: 1 probefrag: 1 ppasses: 1 Hash join Resc: 759 Resp: 759 Join result: cost: 741 cdn: 6 rcz: 50 Now joining: CHILD[C]#0 ******* NL Join Outer table: cost: 741 cdn: 6 rcz: 50 resp: 741 Inner table: CHILD Alias: C Access Path: table-scan Resc: 2517 Join: Resc: 15844 Resp: 15844 Access Path: index (scan) Index: C_PK rsc_cpu: 15543 rsc_io: 2 ix_sel: 5.0000e-011 ix_sel_with_fliters: 5.0000e-011 NL Join: resc: 753 resp: 753 Best NL cost: 753 resp: 753 Using concatenated index cardinality for table PARENT Revised join selectivity: 1.0000e-004 = 7.9445e-007 * (1/10000) * (l/7.9445e-007) Join Card: 0.04 = outer (6.07) * inner (68.01) * sei (1.0000e-004) SM Join Outer table: resc: 741 cdn: 6 rcz: 50 deg: 1 resp: 741
Файл трассировки 473 Inner table: CHILD Alias: C resc: 2517 cdn: 68 rcz: 27 deg: 1 resp: 2517 using join:l distribution^ #groups:l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 65 Total Rows: 6 Initial runs: 1 Merge passes: 0 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5000699 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 40 Total Rows: 68 Initial runs: 1 Merge passes: 0 io Cost / pass: 0 Total 10 sort cost Total Temp space Merge join Cost: : 0 Total CPU used: 0 sort cost: 5018650 3261 3261 Resp: HA Jain Outer table: resc: 741 cdn: 6 rcz: 50 deg: 1 resp: 741 Inner table: CHILD resc: 2517 cdn: 68 Alias rcz: : C 27 deg: 1 resp: 2517 using join:8 distribution^ #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag: 1 probefrag: 1 ppasses: 1 Hash join Resc: 3259 Resp: 3259 Join result: cost: 753 Cdn: 1 rcz: 77 Now joining: GREATGRANDPARENT[GGP]#3 ******* nl Join Outer table: cost: 753 cdn: 1 rcz: 77 resp: 753 Inner table: GREATGRANDPARENT Alias: GGP Access Path: table-scan Resc: 64 Join: Resc: 818 Resp: 818 Access Path: index (unique) Index: GGP_PK rsc_cpu: 15558 rsc_io: 1 ix_sel: 1.0000e-003 ix_sel_with_filters: 1.0000e-003 NL Join: resc: 754 resp: 754 Access Path: index (eq-unique) Index: GGP_PK rsc_cpu: 15758 rsc_io: 1 ix_sel: 0.0000e+000 ix_sel_with_filters: 0.0000e+000 NL Join: resc: 754 resp: 754 Best NL cost: 754 resp: 754 Join Card: 0,04 = outer (0.04) * inner (261.26) * sei (3.8168e-003) SM Join Outer table: resc: 753 cdn: 1 rcz: 77 deg: 1 resp: 753 Inner table: GREATGRANDPARENT Alias: GGP resc: 64 cdn: 261 rcz: 19 deg: 1 resp: 64 using join:l distribution^ #groups:l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760
474 Глава 14. Файл трассировки 10053 Degree: 1 Blocks to Sort: 1 Row size: 95 Total Rows: 1 Initial runs: 1 Merge passes: 0 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5000000 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 31 Total Rows: 261 Initial runs: 1 Merge passes: 0 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5094402 Total Temp space used: 0 Merge join Cost: 820 Resp: 820 HA Join Outer table: resc: 753 cdn: 1 rcz: 77 deg: 1 resp: 753 Inner table: GREATGRANDPARENT Alias: GGP resc: 64 cdn: 261 rcz: 19 deg: 1 resp: 64 using join:8 distribution^ #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: Hash join Join result: 124 (max=2560) Resc: 818 cost: 754 buildfrag: 1 probefrag: 1 ppasses: 1 Resp: 818 cdn: 1 rcz: 96 Best so far: TABLE#: 1 CST: 631 CDN: 110 BYTES: 2970 Best so far: TABLE#: 2 CST: 741 CDN: 6 BYTES: 300 Best so far: TABLE#: 0 CST: 753 CDN: 1 BYTES: 77 Best so far: TABLE#: 3 *********************** CST: 754 CDN: 1 BYTES: 96 Восьмой порядок соединения (раздел Join order[8]) Для этого порядка соединения оптимизатор запоминает стоимость соединения таблиц (parent, grandparent) и проходит до конца соединения — но новое зна- чение наилучшей стоимости не появляется. Join order [8] : PARENT[P]#1 GRANDPARENT[GP]#2 GREATGRANDPARENT[GGP]#3 CHILD[C]#0 Now joining: GREATGRANDPARENT[GGP]#3 ******* NL Join Outer table: cost: 741 cdn: 6 rcz: 50 resp: 741 Inner table: GREATGRANDPARENT Alias: GGP Access Path: table-scan Resc: 63 Join: Resc: 1121 Resp: 1121 Access Path: index (unique) Index: GGP_PK rsc_cpu: 15558 rsc_io: 1 ix_sel: 1.0000e-003 ix_sel_with_filters: 1.0000e-003 NL Join: resc: 747 resp: 747 Access Path: index (eq-unique) Index: GGP_PK rsc_cpu: 15758 rsc_io: 1 ix_sel: 0.0000e+000 ix_sel_with_fiIters: 0.0000e+000 NL Join: resc: 747 resp: 747 Best NL cost: 747 resp: 747 Join Card: 6.05 = outer (6.07) * inner (261.26) * sei (3.8168e-003)
Файл трассировки 475 SM Join Outer table: resc: 741 cdn: 6 rcz: 50 deg: 1 resp: 741 Inner table: GREATGRANDPARENT Alias: GGP resc: 64 cdn: 261 rcz: 19 deg: 1 resp: 64 using join:l distribution^ #groups:l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 65 Total Rows: 6 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5000699 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 31 Total Rows: 261 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5094402 Total Temp space used: 0 Merge join Cost: 808 Resp: 808 HA Join Outer table: resc: 741 cdn: 6 rcz: 50 deg: 1 resp: 741 Inner table: GREATGRANDPARENT Allas: GGP resc: 64 cdn: 261 rcz: 19 deg: 1 resp: 64 using join:8 distribution.^ #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag: 1 probefrag: 1 ppasses: 1 Hash join Resc: 806 Resp: 806 Join result: cost: 747 cdn: 6 rcz: 69 Now joining: CHILD[C]#0 ******* NL Join Outer table: cost: 747 cdn: 6 rcz: 69 resp: 747 Inner table: CHILD Alias: C Access Path: table-scan Resc: 2517 Join: Resc: 15850 Resp: 15850 Access Path: index (scan) Index: C_PK rsc_cpu: 15543 rsc_io: 2 ix_sel: 5.0000e-011 ix_sel_with_fliters: 5.0000e-011 NL Join: resc: 759 resp: 759 Best NL cost: 759 resp: 759 Using concatenated index cardinality for table PARENT Revised join selectivity: 1.0000e-004 = 7.9445e-007 * (1/10000) * (l/7.9445e-007) Join Card: 0.04 = outer (6.05) * inner (68.01) * sei (1.0000e-004) Join cardinality for NL: 0.04, outer: 6.05, inner: 68.01, sei: 1.0000e-004 SM Join Outer table: resc: 747 cdn: 6 rcz: 69 deg: 1 resp: 747 Inner table: CHILD Alias: C resc: 2517 cdn: 68 rcz: 27 deg: 1 resp: 2517
476 Глава 14. Файл трассировки 10053 using join:l distributions #groups;l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 86 Total Rows: 6 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5000699 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 40 Total Rows: 68 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5018650 Total Temp space used: 0 Merge join Cost: 3267 Resp: 3267 HA Join Outer table: resc: 747 cdn: 6 rcz: 69 deg: 1 resp: 747 Inner table: CHILD Alias: C resc: 2517 cdn: 68 rcz: 27 deg: 1 resp: 2517 using join:8 distributions #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag: 1 probefrag: 1 ppasses: 1 Hash join Resc: 3265 Resp: 3265 *********************** Девятый порядок соединения (раздел Join order[9]) Этот порядок соединения сразу начинается с одного из соединений с декарто- вым произведением с высокой стоимостью и очень быстро прекращается — сле- дующее соединение (parent, greatgrandparent, grandparent, child) даже не рассматривается. Join order[9]: PARENT[P]#1 GREATGRANDPARENT[GGP]#3 CHILD[C]#0 GRANDPARENT[GP]#2 Now joining: GREATGRANDPARENT[GGP]#3 ******* NL Join Outer table: cost: 631 cdn: 110 rcz: 27 resp: 631 Inner table: GREATGRANDPARENT Alias: GGP Access Path: table-scan Resc: 63 Join: Resc: 7553 Resp: 7553 Best NL cost: 7553 resp: 7553 Join Card: 28751.26 = outer (110.05) * inner (261.26) * sei (1.0000e+000) *********************** Десятый порядок соединения (раздел Join ordeiflO]) И снова порядок соединения начинается с одного из затратных соединений с декартовым произведением с высокой стоимостью и немедленно прекращает- ся — следующее соединение (grandparent, child, greatgrandparent, parent) не рассматривается. Join order[10]: GRANDPARENT[GP]#2 CHILD[C]#0 PARENT[P]#1 GREATGRANDPARENT[GGP]#3 Now joining: CHILD[C]#0 ******* NL Join
Файл трассировки 477 Outer table: cost: 128 cdn: 110 rcz: 23 resp: 128 Inner table: CHILD Alias: C Access Path: table-scan Resc: 2517 Join: Resc: 276985 Resp: 276985 Best NL cost: 276985 resp: 276985 Join Card: 7497.70 = outer (110.25) * inner (68.01) * sei (1.0000e+000) *********************** Одиннадцатый порядок соединения (раздел Join order[ll]) Наконец мы дошли до соединения без декартовых произведений. Это соедине- ние выполняется до конца, и в результате появляется новое наилучшее значе- ние стоимости на данный момент, которое является значительным улучшением по сравнению с предыдущим результатом. Join order[11]: GRANDPARENT[GP]#2 PARENT[P]#1 CHILD[C]#0 GREATGRANDPARENT[GGP]#3 Now joining: PARENT[P]#1 ******* NL Join Outer table: cost: 128 cdn: 110 rcz: 23 resp: 128 Inner table: PARENT Alias: P Access Path: table-scan Resc: 629 Join: Resc: 69338 Resp: 69338 Access Path: index (scan) Index: P_PK rsc_cpu: 15523 rsc_io: 2 ix_sel: 5.0000e-007 ix_sel_with_fliters: 5.0000e-007 NL Join: resc: 348 resp: 348 Best NL cost: 348 resp: 348 Using concatenated index cardinality for table GRANDPARENT Revised join selectivity: 5.0000e-004 = 8.3417e-005 * (1/2000) * (l/8.3417e-005) Join Card: 6.07 = outer (110.25) * inner (110.05) * sei (5.0000e-004) SM Join Outer table: resc: 128 cdn: 110 rcz: 23 deg: 1 resp: 128 Inner table: PARENT Alias: P resc: 631 cdn: 110 rcz: 27 deg: 1 resp: 631 using join:l distribution.^ #groups:l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 36 Total Rows: 110 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5033608 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 40 Total Rows: 110 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5033608 Total Temp space used: 0 Merge join Cost: 761 Resp: 761 HA Join
478 Глава 14. Файл трассировки 10053 Outer table: resc: 128 cdn: 110 rcz: 23 deg: 1 resp: 128 Inner table: PARENT Alias: P resc: 631 cdn: 110 rcz: 27 deg: 1 resp: 631 using join:8 distribution^ #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag: 1 probefrag: 1 ppasses: 1 Hash join Resc: 759 Resp: 759 Join result: cost: 348 cdn: 6 rcz: 50 Now joining: CHILD[C]#0 ******* NL Join Outer table: cost: 348 cdn: 6 rcz: 50 resp: 348 Inner table: CHILD Alias: C Access Path: table-scan Resc: 2517 Join: Resc: 15450 Resp: 15450 Access Path: index (scan) Index: C_PK rsc_cpu: 15543 rsc_io: 2 ix_sel: 5.0000e-011 ix_sel_with_filters: 5.0000e-011 NL Join: resc: 360 resp: 360 Best NL cost: 360 resp: 360 Using concatenated index cardinality for table PARENT Revised join selectivity: 1.0000e-004 = 7.9445e-007 * (1/10000) * (l/7.9445e-007) Join Card: 0.04 = outer (6.07) * inner (68.01) * sei (1.0000e-004) SM Join Outer table: resc: 348 cdn: 6 rcz: 50 deg: 1 resp: 348 Inner table: CHILD Alias: C resc: 2517 cdn: 68 rcz: 27 deg: 1 resp: 2517 using join:l distribution^ #groups:l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 65 Total Rows: 6 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5000699 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 40 Total Rows: 68 initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5018650 Total Temp space used: 0 Merge join Cost: 2868 Resp: 2868 HA Join Outer table: resc: 348 cdn: 6 rcz: 50 deg: 1 resp: 348 Inner table: CHILD Alias: C resc: 2517 cdn: 68 rcz: 27 deg: 1 resp: 2517 using join:8 distribution.^ #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag: 1 probefrag: 1 ppasses: 1 Hash join Resc: 2866 Resp: 2866
Файл трассировки 479 Join result: cost: 360 cdn: 1 rcz: 77 Now joining: GREATGRANDPARENT[GGP]#3 ******* nl Join Outer table: cost: 360 cdn: 1 rcz: 77 resp: 360 Inner table: GREATGRANDPARENT Alias: GGP Access Path: table-scan Resc: 64 Join: Resc: 425 Resp: 425 Access Path: index (unique) Index: GGP_PK rsc_cpu: 15558 rsc_io: 1 ix_sel: 1.0000e-003 ix_sel_with_fiIters: 1.0000e-003 NL Join: resc: 361 resp: 361 Access Path: index (eq-unique) Index: GGP_PK rsc_cpu: 15758 rsc_io: 1 ix_sel: 0.0000e+000 ix_sel_with_fliters: 0.0000e+000 NL Join: resc: 361 resp: 361 Best NL cost: 361 resp: 361 Join Card: 0.04 = outer (0.04) * inner (261.26) * sei (3.8168e-003) SM Join Outer table: resc: 360 cdn: 1 rcz: 77 deg: 1 resp: 360 Inner table: GREATGRANDPARENT Alias: GGP resc: 64 cdn: 261 rcz: 19 deg: 1 resp: 64 using join:l distribution^ #groups:l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 95 Total Rows: 1 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5000000 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 31 Total Rows: 261 Initial runs: 1 Merge passes: 0 Ю Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5094402 Total Temp space used: 0 Merge join Cost: 427 Resp: 427 HA Join Outer table: resc: 360 cdn: 1 rcz: 77 deg: 1 resp: 360 Inner table: GREATGRANDPARENT Alias: GGP resc: 64 cdn: 261 rcz: 19 deg: 1 resp: 64 using join:8 distribution^ #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag: 1 probefrag: 1 ppasses: 1 Hash join Join result: Resc: cost: 3 : 425 161 Resp: 425 cdn: 1 rcz: 96 Best so far: TABLE#: : 2 CST: 128 CDN: 110 BYTES: 2530 Best so far: TABLE#: ; 1 CST: 348 CDN: 6 BYTES: 300 Best so far: TABLE#: : 0 CST: 360 CDN: 1 BYTES: 77 Best so far: TABLE#: : 3 CST: 361 CDN: 1 BYTES: 96 ***********************
480 Глава 14. Файл трассировки 10053 Двенадцатый порядок соединения (раздел Join order[12]) Этот порядок соединения экономит немного времени, потому что может ис- пользовать частичные результаты одиннадцатого порядка соединения (grand- parent, parent). Но далее этот порядок останавливается после третьего соеди- нения (greatgrandparent), потому что стоимость намного превышает наилуч- шую стоимость на данный момент. Join order[12]: GRANDPARENT[GP]#2 PARENT[P]#1 GREATGRANDPARENT[GGP]#3 CHILD[C]#0 Now joining: GREATGRANDPARENT[GGP]#3 ******* NL Join Outer table: cost: 348 cdn: 6 rcz: 50 resp: 348 Inner table: GREATGRANDPARENT Alias: GGP Access Path: table-scan Resc: 63 Join: Resc: 728 Resp: 728 Access Path: index (unique) Index: GGP_PK rsc_cpu: 15558 rsc_io: 1 ix_sel: 1.0000e-003 ix_sel_with_filters: 1.0000e-003 NL Join: resc: 354 resp: 354 Access Path: index (eq-unique) Index: GGP_PK rsc_cpu: 15758 rsc_io: 1 ix_sel: 0.0000e+000 ix_sel_with_filters: 0.0000e+000 NL Join: resc: 354 resp: 354 Best NL cost: 354 resp: 354 Join Card: 6.05 = outer (6.07) * inner (261.26) * sei (3,8168e-003) SM Join Outer table: resc: 348 cdn: 6 rcz: 50 deg: 1 resp: 348 Inner table: GREATGRANDPARENT Alias: GGP resc: 64 cdn: 261 rcz: 19 deg: 1 resp: 64 using join:l distribution.^ #groups:l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 65 Total Rows: 6 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5000699 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 31 Total Rows: 261 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5094402 Total Temp space used: 0 Merge join Cost: 415 Resp: 415 HA Join Outer table: resc: 348 cdn: 6 rcz: 50 deg: 1 resp: 348 Inner table: GREATGRANDPARENT Alias: GGP resc: 64 cdn: 261 rcz: 19 deg: 1 resp: 64
Файл трассировки 481 using join:8 distribution^ #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag: 1 probefrag: 1 ppasses: 1 Hash join Resc: 413 Resp: 413 Join result: cost: 354 cdn: 6 rcz: 69 Now joining: CHILD[C]#0 **й*** NL Join Outer table: cost: 354 cdn: 6 rcz: 69 resp: 354 Inner table: CHILD Alias: C Access Path: table-scan Resc: 2517 Join: Resc: 15456 Resp: 15456 Access Path: index (scan) Index: C_PK rsc_cpu: 15543 rsc_io: 2 ix_sel: 5.0000e-011 ix_sel_with_fiIters: 5.0000e-011 NL Join: resc: 366 resp: 366 Best NL cost: 366 resp: 366 Using concatenated index cardinality for table PARENT Revised join selectivity: 1.00O0e-004 = 7.9445e-007 * (1/10000) * (177.9445e-@@7) Join Card: 0.04 = outer (6.05) * inner (68.01) * sei (1.0000e-004) Join cardinality for NL: 0.04, outer: 6.05, inner: 68.01, sei: 1.0000e-004 SM Join Outer table: resc: 354 cdn: 6 rcz: 69 deg: 1 resp: 354 Inner table: CHILD Alias: C resc: 2517 cdn: 68 rcz: 27 deg: 1 resp: 2517 using join:l distribution:! #groups:l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 86 Total Rows: 6 Initial runs: 1 Merge passes: Q 10 Cost / pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5000699 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 40 Total Rows: 68 Initial runs: 1 Merge passes: Q 10 Cost / pass: 0 Total 10 sort cost Total Temp space Merge join Cost: : 0 Total CPU used: 0 2874 Resp: sort cost: 5018650 2874 HA Join Outer table: resc: 354 cdn; 6 Inner table: CHILD resc: 2517 cdn: 68 rcz: Ali as rcz: 69 deg: : C 27 deg: 1 resp: 354 1 resp: 2517 using join:8 distribution^ #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag: 1 probefrag: 1 ppasses: 1 Hash join Resc: 2872 Resp: 2872 ***********************
482 Глава 14. Файл трассировки 10053 Тринадцатый порядок соединения (раздел Join order[13]) Мы начинаем проходить по этому порядку соединения, после чего останавли- ваемся на третьем соединении — снова из-за соединения с декартовым произве- дением с таблицей child. Join order[13]: GRANDPARENT[GP]#2 GREATGRANDPARENT[GGP]#3 CHILD[C]#0 PARENT[P]#1 Now joining: GREATGRANDPARENT[GGP]#3 ******* NL Join Outer table: cost: 128 cdn: 110 rcz: 23 resp: 128 Inner table: GREATGRANDPARENT Alias: GGP Access Path: table-scan Resc: 63 Join: Resc: 7049 Resp: 7049 Access Path: index (unique) Index: GGP_PK rsc_cpu: 15558 rsc_io: 1 ix_sel: 1.0000e-003 ix_sel_with_fliters: 1.0000e-003 NL Join: resc: 238 resp: 238 Access Path: index (eq-unique) Index: GGP_PK rsc_cpu: 15758 rsc_io: 1 ix_sel: 0.0000e+000 ix_sel_with_fiIters: 0.0000e+000 NL Join: resc: 238 resp: 238 Best NL cost: 238 resp: 238 Join Card: 109.94 = outer (110.25) * inner (261.26) * sei (3.8168e-003) Join cardinality for NL; 109.94, outer: 110.25, inner: 261.26, sei; 3.8168e-003 SM Join Outer table: resc: 128 cdn: 110 rcz: 23 deg: 1 resp: 128 Inner table: GREATGRANDPARENT Alias; GGP resc: 64 cdn: 261 rcz: 19 deg: 1 resp: 64 using join:l distribution.^ #groups:l SORT resource Sort statistics Sort width; 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size; 36 Total Rows: 110 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5033608 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 31 Total Rows: 261 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5094402 Total Temp space used: 0 Merge join Cost: 194 Resp: 194 HA Join Outer table: resc: 128 cdn: 110 rcz: 23 deg: 1 resp: 128 Inner table: GREATGRANDPARENT Alias: GGP resc: 64 cdn: 261 rcz: 19 deg: 1 resp: 64 using join:8 distribution.^ #groups:l
Файл трассировки 483 Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag: 1 probefrag: 1 ppasses: 1 Hash join Resc: 193 Resp: 193 Join result: cost: 193 cdn: 110 rcz: : 42 Now joining: CHILD[C]#0 ***** ** NL Join Outer table: cost: 193 cdn: 110 rcz: 42 resp: 193 Inner table: CHILD Alias: C Access Path: table-scan Resc: 2517 Join: Resc: 277050 Resp: 277050 Best NL cost: 277050 resp: 277050 Join Card: 7476.42 = outer (109.94) * inner (68.01) * sei (1.0000e+000) ***********4c*4c4c4c4c4c4c4c4c4c4c Четырнадцатый порядок соединения (раздел Join order[14]) Этот порядок соединения повторно использует частичные результаты для таб- лиц (grandparent, greatgrandparent) из тринадцатого порядка соединения, но останавливается после всех возможных попыток соединения с третьей таб- лицей parent. Join order[14]: GRANDPARENT[GP]#2 GREATGRANDPARENT[GGP]#3 PARENT[P]#1 CHILD[C]#0 Now joining: PARENT[P]#1 ******* nl Join Outer table: cost: 193 cdn: 110 rcz: 42 resp: 193 Inner table: PARENT Alias: P Access Path: table-scan Resc: 629 Join: Resc: 69403 Resp: 69403 Access Path: index (scan) Index: P_PK rsc_cpu: 15523 rsc_io: 2 ix_sel: 5.0000e-007 ix_sel_with_fliters: 5.0000e-007 NL Join: resc: 413 resp: 413 Best NL cost: 413 resp: 413 Using concatenated index cardinality for table GRANDPARENT Revised join selectivity: 5.0000e-004 = 8.3417e-005 * (1/2000) * (l/8.3417e-005) Join Card: 6.05 = outer (109.94) * inner (110.05) * sei (5.0000e-004) SM Join Outer table: resc: 193 cdn: 110 rcz: 42 deg: 1 resp: 193 Inner table: PARENT Alias: P resc: 631 cdn: 110 rcz: 27 deg: 1 resp: 631 using join:l distribution^ #groups:l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 57 Total Rows: 110 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5033608 Total Temp space used: 0 SORT resource Sort statistics
484 Глава 14. Файл трассировки 10053 Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 40 Total Rows: 110 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5033608 Total Temp space used: 0 Merge join Cost: 826 Resp: 826 HA Join Outer table: resc: 193 cdn: 110 rcz: 42 deg: 1 resp: 193 Inner table: PARENT Alias: P resc: 631 cdn: 110 rcz: 27 deg: 1 resp: 631 using join:8 distribution^ #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag: 1 probefrag: 1 ppasses: 1 Hash join Resc: 824 Resp: 824 *********************** Пятнадцатый порядок соединения (раздел Join order[15]) К пятнадцатому порядку соединения таблица greatgrandparent наконец дос- тигла начала порядка соединения. К сожалению, вторая таблица является таб- лицей child, что приводит к соединению с декартовым произведением. Это ос- танавливает выполнение соединения и гарантированно приводит к тому, что оптимизатор даже не рассматривает соединение (greatgrandparent, child, grandparent, parent). Join order[15]: GREATGRANDPARENT[GGP]#3 CHILD[C]#0 PARENT[P]#1 GRANDPARENT[GP]#2 Now joining: CHILD[C]#0 ******* NL Join Outer table: cost: 64 cdn: 261 rcz: 19 resp: 64 Inner table: CHILD Alias: C Access Path: table-scan Resc: 2517 Join: Resc: 656970 Resp: 656970 Best NL cost: 656970 resp: 656970 Join Card: 17766.99 ~ outer (261.26) * inner (68.01) * sei (1.0000e+000) Шестнадцатый порядок соединения (раздел Join order[16]) Здесь снова используется соединение с декартовым произведением между дву- мя первыми таблицами, что сокращает расчеты стоимости этого соединения и исключает другое соединение даже без его вывода. Join order[16]: GREATGRANDPARENT[GGP]#3 PARENT[P]#1 CHILD[C]#0 GRANDPARENT[GP]#2 Now joining: PARENT[P]#1 ******* NL Join Outer table: cost: 64 cdn: 261 rcz: 19 resp: 64 Inner table: PARENT Alias: P Access Path: table-scan Resc: 629 Join: Resc: 164281 Resp: 164281 Best NL cost: 164281 resp: 164281 Join Card: 28751.26 = outer (261.26) * inner (110.05) * sei (1.0000e+000) 4c4c4c4c*c4c*c*c*c*c*c*c4c*c*c*c*c*c*c*c*c*c#
Файл трассировки 485 Семнадцатый порядок соединения (раздел Join order[17]) Наконец мы добрались до порядка соединения, в котором используется абсо- лютно новая возможность: когда мы соединяем таблицу grandparent с табли- цей greatgrandparent, мы обнаруживаем, что для соединения сортировки /слияния можно использовать две стратегии. Мы можем избежать сортиров- ки данных из таблицы grandparent, если будем использовать индекс ее первич- ного ключа для получения записей. Но это потребует получения избыточных данных и исключения части этих данных из рассмотрения, и дополнительная ра- бота может оказаться более затратной, чем стоимость сортировки, которой мы хотим избежать. Расчет стоимости прекращается после трех таблиц — опять же из-за соеди- нения с декартовым произведением. Join order[17]: GREATGRANDPARENT[GGP]#3 GRANDPARENT[GP]#2 CHILD[C]#0 PARENT[P]#1 Now joining: GRANDPARENT[GP]#2 ******* NL Join Outer table: cost: 64 cdn: 261 rcz: 19 resp: 64 Inner table: GRANDPARENT Alias: GP Access Path: table-scan Resc: 126 Join: Resc: 32906 Resp: 32906 Access Path: index (scan) Index: GP_PK rsc_cpu: 23207 rsc_io: 3 ix_sel: 1.0000e-003 ix_sel_with_fliters: 1.0000e-003 NL Join: resc: 849 resp: 849 Best NL cost: 849 resp: 849 Join Card: 109.94 = outer (261.26) * inner (110.25) * sei (3.8168e-003) Join cardinality for NL: 109.94, outer: 261.26, inner: 110.25, sei: 3.8168e-003 SM Join Outer table: resc: 64 cdn: 261 rcz: 19 deg: 1 resp: 64 Inner table: GRANDPARENT Alias: GP resc: 128 cdn: 110 rcz: 23 deg: 1 resp: 128 using join:l distribution^ #groups:l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 31 Total Rows; 261 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5094402 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 36 Total Rows: 110 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5033608 Total Temp space used: 0 Merge join Cost: 194 Resp: 194
486 Глава 14. Файл трассировки 10053 Затем мы выполняем расчеты для второй стратегии соединения слияния — стратегии, которая использует индекс первичного ключа, чтобы избежать сор- тировки. Из-за того, что первый набор данных перед его получением будет от- сортирован, мы видим только одну секцию с названием, помеченную ресурсом SORT resource. К сожалению, это бесполезные усилия, так как эта стратегия приводит к большей стоимости, чем подход без использования индекса. Несмотря на утверждения в документации, если Oracle может получить от- сортированный второй набор данных, он все равно его отсортирует. Я думаю, это связано с общим случаем, когда условие соединения не является проверкой на равенство и второй набор данных должен быть полностью получен, чтобы соединение на основе диапазона могло установить стартовую позицию во вто- ром наборе данных. SM Join (with index on outer) Access Path: index (no start/stop keys) Index: GGP_PK rsc_cpu: 2266849 rsc_io: 2S3 ix_sel: 1.0000e+000 ix_sel_with_filters: 1.0000e+000 Outer table: resc: 253 cdn: 261 rcz: 19 deg: 1 resp: 253 Inner table: GRANDPARENT Alias: GP resc: 128 cdn: 110 rcz: 23 deg: 1 resp: 128 using join:l distribution^ #groups:l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 36 Total Rows: 110 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5033608 Total Temp space used: 0 Merge join Cost: 382 Resp: 382 HA Join Outer table: resc; 64 cdn; 261 rcz: 19 deg: 1 resp; 64 Inner table: GRANDPARENT Alias: GP resc: 128 cdn: 110 rcz: 23 deg; 1 resp; 128 using join:8 distribution^ #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag: 1 probefrag: 1 ppasses: 1 Hash join Resc: 193 Resp: 193 Join result: cost: 193 cdn: 110 rcz: 42 Now joining: CHILD[C]#0 ******* NL Join Outer table: cost: 193 cdn: 110 rcz: 42 resp: 193 Inner table: CHILD Alias: C Access Path: table-scan Resc: 2517 Join: Resc: 277050 Resp: 277050 Best NL cost: 277050 resp: 277050 Join Card; 7476.42 = outer (109.94) * inner (68.01) * sei (1.0000e+000) ***********************
Файл трассировки 487 Восемнадцатый порядок соединения (раздел Join order[18]) Наш последний порядок соединения повторно использует частичные результа- ты из семнадцатого порядка соединения (greatgrandparent, grandparent) и оста- навливается на третьей таблице. Join order[18]: GREATGRANDPARENT[GGP]#3 GRANDPARENT[GP]#2 PARENT[P]#1 CHILD[C]#0 Now joining: PARENT[P]#1 ******* NL Join Outer table: cost; 193 cdn: 110 rcz: 42 resp: 193 1 Inner table: PARENT Alias: P Access Path: table-scan Resc: 629 ' Join: Resc: 69403 Resp: 69403 Access Path: index (scan) Index: P_PK rsc_cpu: 15523 rsc_io: 2 ix_sel: 5.0000e-007 ix_sel_with_fiIters: 5.0000e-007 NL Join: resc: 413 resp: 413 Best AIL cost: 413 resp: 413 Using concatenated index cardinality for table GRANDPARENT Revised join selectivity: 5.0000e-004 = 8.3417e-005 * (1/2000) * (1/8.3417e-005) Join Card: 6.05 = outer (109.94) * inner (110.05) * sei (5.0000e-004) SM Join Outer table: resc; 193 cdn: 110 rcz: 42 deg: 1 resp; 193 Inner table: PARENT Alias: P resc: 631 cdn: 110 rcz: 27 deg: 1 resp: 631 using join:l distribution:! #groups;l SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 57 Total Rows: 110 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5033608 Total Temp space used: 0 SORT resource Sort statistics Sort width: 58 Area size: 208896 Max Area size: 10485760 Degree: 1 Blocks to Sort: 1 Row size: 40 Total Rows: 110 Initial runs: 1 Merge passes: 0 10 Cost I pass: 0 Total 10 sort cost: 0 Total CPU sort cost: 5033608 Total Temp space used: 0 Merge join Cost: 826 Resp: 826 HA Join Outer table: resc: 193 cdn: 110 rcz: 42 deg: 1 resp; 193 Inner table: PARENT Alias: P resc: 631 cdn: 110 rcz: 27 deg: 1 resp: 631 using join:8 distribution:! #groups:l Hash join one ptn Resc: 1 Deg: 1 hash_area: 124 (max=2560) buildfrag: 1 probefrag: 1 ppasses: 1 Hash join Resc; 824 Resp: 824
488 Глава 14. Файл трассировки 10053 Следующие две строки появляются в версии 10g. Я не понимаю важность строки (newjo-stop-1), хотя количество перестановок (perm:), равное 18, и максимальное количество перестановок (maxperm:), равное 2000, являются ссылками на уже рассмотренные нами выше перестановки и на параметр _ор - timizer_max_permutations, соответственно. Строка (newjо-save) просто указывает порядок соединения,’который был в итоге выбран, перечисляя номера таблиц в порядке соединения и используя при этом номера, данные таблицам в первом порядке соединения. (newjo-stop-1) k:0, spcnt:0, perm:18, maxperm:2000 (newjo-save) [210 3 ] Наконец мы завершили работу, и Oracle выводит порядок соединения, кото- рый дает лучший по стоимости план выполнения (это улучшение было введено в версии 10g и перенесено в поздние выпуски 9i. В ранних версиях нужно было искать в файле место, где строка Best so fаг: встречалась последний раз). Также здесь показана итоговая стоимость, состоящая из стоимости опера- ций ввода-вывода и стоимости использования ресурсов процессора. Как и рань- ше, мы можем разделить указанную стоимость использования ресурсов процес- сора на 5 000 000, чтобы добавить полученное значение к стоимости операций ввода-вывода для получения общей стоимости. Final - All Rows Plan: JOIN ORDER: 11 CST: 361 CDN: 1 RSC: 361 RSP: 361 BYTES: 96 IO-RSC: 360 IO-RSP: 360 CPU-RSC: 5892137 CPU-RSP: 5892137 Результаты оценки соединений Было сделано много работы, которая состояла из множества повторяющихся действий. Чтобы подвести итог работе с порядками соединения, я извлек по- рядки соединений, которые рассматривал оптимизатор, и вывел их ниже. До- вольно информативно просто использовать grep (в Unix) или find (в Win- dows) для поиска всех строк, начинающихся с Join order, чтобы увидеть, сколько порядков соединения и сколько подпланов рассмотрел оптимизатор. Join order[l]: CHILD[C]#0 PARENT[P]#1 GRANDPARENT[GP]#2 GREATGRANDPARENT[GGP]#3 Join order[2]: CHILD[C]#0 PARENT[P]#1 GREATGRANDPARENT[GGP]#3 GRANDPARENT[GP]#2 Join order[3]: CHILD[C]#0 GRANDPARENT[GP]#2 PARENT[P]#1 GREATGRANDPARENT[GGP]#3 Join order[4]: CHILD[C]#0 GREATGRANDPARENT[GGP]#3 PARENT[P]#1 GRANDPARENT[GP]#2 Join order[5]: PARENT[P]#1 CHILD[C]#0 GRANDPARENT[GP]#2 GREATGRANDPARENT[GGP]#3 Join order[6]: PARENT[P]#1 CHILD[C]#0 GREATGRANDPARENT[GGP]#3 GRANDPARENT[GP]#2 Join order[7] : PARENT[P]#1 GRANDPARENT[GP]#2 CHILD[C]#0 GREATGRANDPARENT[GGP]#3 Join order[8]: PARENT[P]#1 GRANDPARENT[GP]#2 GREATGRANDPARENT[GGP]#3 CHILD[C]#0
Результаты оценки соединений 489 Join order[9]: PARENT[P]#1 GREATGRANDPARENT[GGP]#3 CHILD[C]#0 GRANDPARENT[GP]#2 Join order[10]: GRANDPARENT[GP]#2 CHILD[C]#0 PARENT[P]#1 GREATGRANDPARENT[GGP]#3 Join order[11]: GRANDPARENT[GP]#2 PARENT[P]#1 CHILD[C]#0 GREATGRANDPARENT[GGP]#3 Join order[12]: GRANDPARENT[GP]#2 PARENT[P]#1 GREATGRANDPARENT[GGP]#3 CHILD[C]#0 Join order[13]: GRANDPARENT[GP]#2 GREATGRANDPARENT[GGP]#3 CHILD[C]#0 PARENT[P]#1 Join order[14]: GRANDPARENT[GP]#2 GREATGRANDPARENT[GGP]#3 PARENT[P]#1 CHILD[C]#0 Join order[15]: GREATGRANDPARENT[GGP]#3 CHILD[C]#0 PARENT[P]#1 GRANDPARENT[GP]#2 Join order[16]: GREATGRANDPARENT[GGP]#3 PARENT[P]#1 CHILD[C]#0 GRANDPARENT[GP]#2 Join order[17]: GREATGRANDPARENT[GGP]#3 GRANDPARENT[GP]#2 CHILD[C]#0 PARENT[P]#1 Join order[18]: GREATGRANDPARENT[GGP]#3 GRANDPARENT[GP]#2 PARENT[P]#1 CHILD[C]#0 Сократив результат только до номеров для простоты и указав «пропущенные» порядки соединений там, где они должны были бы быть, мы получим следующее: Join orderfl]: #0 #1 #2 #3 Лучший результат *** Join order[2] : #0 #1 #3 #2 Прерывание на третьей таблице Join order[3]: Пропущен: #0 #2 #1 #3 #0, #2, #3, #1 Прерывание на второй таблице Join order[4]: Пропущен: #0 #3 #1 #2 #0, #3, #2, #1 Прерывание на второй таблице Join order[5] : #1 #0 #2 #3 Лучший результат *** Join order[6]: #1 #0 #3 #2 Прерывание на третьей таблице Join order[7]: Join order [8]: #1 #2 #0 #3 #1 #2 #3 #0 Лучший результат *** *** Joi n order[9]: Пропущен: #1 #3 #0 #2 #1, #3, #2, #0 Прерывание на второй таблице Join order[10] Пропущен: : #2 #0 #1 #3 #2, #0, #3. #1 Прерывание на второй таблице Join orderfll] Join order[12] : #2 #1 #0 #3 : #2 #1 #3 #0 Лучший результат *** *** Join order[13] : #2 #3 #0 #1 Прерывание на третьей таблице Join order[14] : #2 #3 #1 #0 Прерывание на третьей таблице Join order[15] Пропущен: : #3 #0 #1 #2 #3, #0, #2, #1 Прерывание на третьей таблице Join order[16] Пропущен: : #3 #1 #0 #2 #3, #1, #2, #0 Прерывание на третьей таблице Join order[17] : #3 #2 #0 #1 Прерывание на третьей таблице Join order[18] : #3 #2 #1 #0 Прерывание на третьей таблице Как вы видите из этого списка, для небольшого количества таблиц оптими- затор просто проходит в цикле по всем возможным перестановкам. Он просто пропускает перестановки, которые никак не могут дать лучшие значения стои- мости, потому что перестановки с тем же начальным порядком таблиц оказа- лись неэффективны. На самом деле из 24 порядков соединения Oracle выпол- нил полные расчеты только для 6 порядков (помечены знаками ***). Для большего количества таблиц все может быть иначе. Сначала оптими- затор может решить после тестирования нескольких порядков соединения: стоимость выполнения запроса настолько мала, что можно выполнить его без
490 Глава 14. Файл трассировки 10053 тестирования других порядков соединения. Как только время на проверку по- рядков соединения превысит оценочное время, Oracle выполнит запрос с луч- шим известным порядком на этот момент. Это значит, что если оптимизатор ставит первой неподходящую таблицу (из-за неправильной кардинальности), то стандартный цикл перестановок может. вызвать проблемы. В соёдинении с десятью таблицами может потребоваться много времени, чтобы пройти в цик- ле по всем вариантам с первой таблицей. Поэтому в оптимизаторе есть алго- ритм, который срабатывает при большом количестве таблиц. Этот алгоритм на- чинает менять лидирующие таблицы, если было протестировано слишком большое количество порядков соединения без получения разумных значений стоимости. ПРИМЕЧАНИЕ Для пользователей Grade 8 изменение параметра optimizer_max_permutations со значения по умол- чанию, равного 80 000, на любое другое значение переключит Grade на использование нового алго- ритма. Пользователи Oracle 10 могут увидеть, что появился новый параметр -.optimizer_join_ order_control со значением 3—так что корпорация Oracle могла уже задействовать другой, еще луч- ший алгоритм для перестановки порядков соединения. Другой особый случай имеет место при использовании большого количества таблиц — минимум семи. По мере перебора возможных порядков соединения оптимизатор просто игнорирует и даже не выводит порядки соединения при наличии множества соединений с декартовым произведением (речь идет не о соединениях с декартовым произведением, возвращающих одну запись). По- хоже, параметр _optimizer_search_limit является параметром, контролирую- щим максимально допустимое количество соединений с декартовым произведе- нием, и по умолчанию он равен 5. Конечно, интересно посмотреть, что случит- ся, если вы передадите Oracle запрос, в котором семь таблиц соединяются только с помощью соединений с декартовым произведением. Можно еще долго изучать трассировку 10053, в особенности информацию по более сложным командам SQL, которые требуют от оптимизатора оценки подпланов для нескольких преобразований вашего запроса и затем выбора од- ного из нескольких механизмов выполнения, таких как соединение типа «звез- да» (star join). И я даже не упомянул, что происходит при использовании под- сказки оптимизации fi rst_rows(N). Но я должен оставить кое-что для второго и третьего томов. Тестовые сценарии Файлы к этой главе, доступные для загрузки, перечислены в табл. 14.3. Таблица 14.3. Тестовые сценарии к главе 14 Сценарий Комментарии big_.10053.sql Сценарий для создания данных для файла трассировки этой главы setenv.sql Установка стандартизированной тестовой среды для SQL*Plus
Приложение А. Проблемы при обновлении версий Когда вы обновляете одну версию Oracle на другую или даже когда обновляете один выпуск с патчем на другой, могут обнаружиться участки вашего кода, ко- торые начнут работать неправильно. Эта проблема в основном связана с опти- мизатором. Нововведения, которые улучшают 99 % всех возможных запросов, могут испортить вашу базу данных, потому что в ней проявился оставшийся 1 % особых случаев. Это приложение является кратким обзором функциональности оптимизато- ра, которая может вызывать проблемы из-за того, что детали реализации меня- ются в версиях 8i, 9i и 10g. Эта функциональность (и возможные проблемы, ко- торые из-за нее могут возникнуть) гораздо подробнее описана в основной части книги. Цель сведения всех изменений в одно приложение — желание дать вам краткое справочное руководство, к которому вы могли бы обращаться при воз- никновении странной проблемы с производительностью после обновления вер- сии. СЕЛЕКТИВНОСТЬ И КАРДИНАЛЬНОСТЬ Понятая селективности и кардинальности очень тесно связаны: селективность представляет собой ту долю записей, которые будут выбраны из набора данных, а кардинальность представляет собой количество записей, которые будут выбраны. В общем случае можно сказать: кардинальность = количество выбранных записей = = селективность х общее количество записей В этом приложении я упоминаю только селективность, когда комментирую влияние обновления версий на работу оптимизатора. Это сделано просто для того, чтобы реже повторять такие фразы, как «и когда изменяется селективность, кардинальность также изменяется, следовательно...» Следует подчеркнуть: я думаю, что все (практически все) изменения, ука- занные в этом приложении, являются в общем случае положительными. Но лю- бое изменение в работе оптимизатора может оказаться достаточным для того, чтобы для части SQL-кода был выбран другой план выполнения, и иногда это изменение в плане выполнения может иметь отрицательное влияние на произ- водительность. К счастью, многие возможности могут быть отключены с помощью установки соответствующего скрытого параметра, как показано в приложе- нии Б в списке параметров, контролируемом главным параметром optimi- zer_features_enable.
492 Приложение А. Проблемы при обновлении версий Пакет dbms.stats Если вы еще не перешли от использования команды analyze к использованию пакета dbms_stats, вам все равно когда-то придется это сделать. Когда наста- нет этот момент, убедитесь, что вы тщательно все протестировали, так как есть три проблемы, с которыми вы столкнетесь. Во-первых, часть статистики будет отличаться, потому что два механизма просто по-разному работают, поэтому некоторые планы выполнения могут из- мениться. Не очень критичным примером является то, что analyze не включа- ет байт длины в значение avg_col_length, которое используется для расчета стоимости выполнения сортировки и соединения хэширования. Более сущест- венный пример: что команда analyze не очень хорошо собирает статистику по секционированным таблицам, особенно по значениям num_ di sti net, в резуль- тате чего пакет dbms_stats выдает совсем другие значения. Во-вторых, вам нужно убедиться, что вы собираете ту же статистику с помо- щью dbms_stats, что и с помощью команды analyze: например, analyze table и gather_table_stats по умолчанию ведут себя по-разному — команда ana- lyze собирает статистику индекса, а процедура gather_table_stats — нет. Наконец, в зависимости от версии, пакет dbms_stats использует нормаль- ный SQL-код для сбора большой части статистики, а не специальный код низ- кого уровня. Это дает возможность автоматически использовать параллелизм, но это значит, что сбор части статистики может занять больше времени, чем при использовании эквивалентных команд analyze. Даже если вы уже используете dbms_stats, вы не застрахованы от измене- ний. Когда вы выполняете обновление версий Oracle, поведение по умолчанию процедур пакета dbms_stats изменяется. Например, для method_opt по умол- чанию установлено значение for all columns size auto для таблиц в версии 10g, поэтому вы получите гистограммы там, где не ожидаете, если позволите версии 10g вести себя по-своему, потому что версия 10g решает, для каких столбцов нужны гистограммы и сколько групп использовать для этих гисто- грамм. Более того, в версии 10g вы обнаружите, что автоматически создается задание, которое выполняется каждые 24 часа для сбора статистики по всем таблицам, в которых статистика отсутствует или она «устаревшая». Это может привести к выполнению ненужной работы, объем которой может сильно и произвольно меняться день ото дня. В dbms_stats встроены несколько решений, которые могут привести к зна- чительным колебаниям результатов сбора статистики и времени на ее сбор. По- хоже, наиболее сильно это заметно при сборе статистики по индексам — соглас- но одному сообщению на MetaLink, код хочет выполнять тестовую выборку по крайней мере 919 листовых блоков для генерации статистики индекса. Это мо- жет сделать использование тестовых выборок небольшого размера невозмож- ным, и проблема может оказаться очень серьезной для битовых индексов. Учтите, что большинству объектов не нужна абсолютно точная статистика; и детальный, минималистический подход для сбора статистики вполне хо- рош — если вы найдете на него время и если сможете обойти ошибки.
Ошибки округления 493 Частотные гистограммы Если вы используете частотные гистограммы, то, когда захотите перестать ис- пользовать команду analyze, можете обнаружить, что dbms_stats не очень хо- рошо работает. Команда analyze создает частотную гистограмму по N отдельным значени- ям, если вы укажете N групп. В версиях 9i и 10.1 при эквивалентном вызове dbms_stats может потребоваться гораздо большее количество групп, чтобы было принято решение построить частотную гистограмму. На самом деле где-то около значения 200 вы можете обнаружить, что у вас уже не получится создать частотную гистограмму, потому что не можете ука- зать достаточно большое количество групп. В таких случаях вам может понадо- биться написать ваш собственный SQL-код для сбора нужных данных и ис- пользовать процедуру dbms_stats. set_column_stats() для занесения этих данных в словарь данных (эта ошибка была исправлена в версии 10.2). Оценка стоимости использования ресурсов процессора Есть два варианта оценки стоимости использования ресурсов процессора (CPU costing) — или системной статистики — в версии 10g. Если только вы еще не знакомы с использованием системной статистики в версии 9i, вы обнаружите, что в версии 10g в вашей системе включается оценка стоимости использования ресурсов процессора с помощью специальной статистики noworkload. Это силь- но влияет на стоимость табличного сканирования, но степень изменения отли- чается от изменения, вызванного нормальным алгоритмом оценки стоимости использования ресурсов процессора, применяемым в версии 9i. Вы также можете заметить особенно большую разницу в стоимости, если у вас есть код, который изменяет значение db_file_multiblock_read_count для различных процессов. Если вы захотели собрать «рабочую» системную статистику в версии 10g, результаты могут быть достаточно неожиданными при сравнении с системной статистикой noworkload. Более того, похоже, процедура dbms_stats ,delete_ system_stats() не работает, поэтому единственным вариантом избавиться от собранной статистики, если вы решили, что она вам больше не нужна, является явное удаление из таблицы sys.aux_stats$ (эта ошибка исправлена в версии 10.2 — поэтому теперь у вас есть легальная возможность вернуться к системной статистике noworkload после экспериментирования с собранной системной статистикой). Ошибки округления Подход, с помощью которого оптимизатор выполняет округление, изменяется от версии к версии. Одни результаты округляются правильно, другие округля- ются до ближайшего меньшего целочисленного значения, а некоторые — до
494 Приложение А. Проблемы при обновлении версий ближайшего большего целочисленного значения. Это может привести к стран- ным изменениям в рассчитанных значениях стоимости и кардинальности, что может привести к изменению плана выполнения при обновлении версии. Что еще важнее, при обновлении версии с 8i до 9i, возможно, есть случаи, когда вер- сия 8i выполняет округление промежуточных результатов, а версия 9i — только в конце расчетов. Это может сильно изменить итоговый результат. Важный момент, которого следует опасаться при округлении: параметр ор- timizer_index_cost_adj может использоваться для уменьшения стоимости индексных одноблоковых операций чтения, но это увеличивает относительное влияние ошибок округления. Если вы работаете в версии 9i, вы должны ис- пользовать системную статистику (оценку стоимости использования ресурсов процессора) для решения проблем с несогласованностью табличных сканирова- ний и операций индексного доступа. Оценка стоимости использования ресур- сов процессора увеличивает стоимость многоблоковых операций чтения, то есть она уменьшает влияние ошибок округления. Если вам нужно выполнить обновление с версии 8i, изменение в стратегии округления может вызвать определенные проблемы, если в вашей системе хра- нится множество наборов ссылочных данных в одной таблице со столбцом type. Если вы действительно столкнетесь с такой проблемой, пересоздание таб- лицы в виде секционированной по списку таблицы (list-partitioned table) долж- но помочь. Считывание значений переменных связывания Переменные связывания всегда вызывают проблемы. Для ограниченного диа- пазона при использовании переменных связывания указывается селективность, равная 0,0025 (0,25 % или 1 из 400), в результате чего индексный доступ будет выбран с большей вероятностью. Для неограниченного диапазона при исполь- зовании переменных связывания указывается селективность, равная 0,05 (5 % или 1 из 20), в результате чего, скорее всего, будет выбрано табличное сканиро- вание. Для проверки на равенство при использовании переменных связывания указывается значение user_tab_columns .density. Во всех этих трех случаях гистограммы игнорируются и ресурсы на йх создание затрачиваются впустую (после создания гистограммы может измениться плотность, так что усилия все-таки не пропадают даром совсем). Как и в версии 9i, оптимизатор обычно считывает текущие значения, когда ему нужно оптимизировать команду, содержащую переменные связывания, и использует эти текущие значения для оценки плана выполнения. Так что ко- гда вы обновляете версию 8i до версии 9i, вы можете обнаружить, что у некото- рых команд SQL изменился план выполнения без всякой причины. Более того, у команды может меняться план выполнения каждый день или каждый час, по- тому что он периодически получается из разделяемого пула и при следующем выполнении команды она заново, оптимизируется с учетом нового набора зна- чений, из-за которых план выполнения может измениться.
Поиск в индексе с пропусками 495 Использование значений null в соединениях Базовая формула обработки значений null в соединениях, похоже, изменилась при переходе от версии 8i к версии 9i. В версии 8i оптимизатор обрабатывал значения null в столбцах соединения, вынося их в формулу расчета селектив- ности соединения, но, похоже, в версии 9i появилась альтернативная стратегия обработки этих значений в формуле расчета кардинальности соединения, кото- рая добавляет is not null в качестве явного предиката соединения для каждо- го участвующего в соединении столбца. Это альтернативное правило начинает действовать, когда значение num_ nulls превышает 5 % от значения num_rows. Конвертация индексов со структурой бинарного дерева в битовые индексы Одной из стратегий оптимизатора является сканирование индексов со структу- рой бинарного дерева (В-tree indexes) по диапазону, чтобы получить списки идентификаторов записей, сконвертировать эти списки идентификаторов запи- сей в эквивалентные битовые карты и выполнить битовые операции для опреде- ления небольших наборов записей. Оптимизатор также может получить набо- ры идентификаторов записей с помощью сканирований диапазонов по индек- су и сконвертировать их в битовые индексы прямо во время выполнения перед тем, как выполнить i ndex_combiпе на результирующих битовых индексах. В версии 8i это может быть выполнено только над таблицами, на которых существуют битовые индексы, если только параметр _b_tree_bi tmap_plans не установлен таким образом, чтобы игнорировать требование существования би- тового индекса на таблице. В версии 9i значение по умолчанию для этого параметра изменяется с false на true, поэтому после обновления версии вы можете видеть планы выполне- ния, в которых выполняется битовая конвертация, даже если в вашей базе дан- ных нет ни одного битового индекса. К сожалению, из-за неявного предположе- ния об упаковке, которое оптимизатор применяет к битовым индексам, это иногда может иметь крайне отрицательные последствия. Также из-за этого изменения может иметь смысл использовать параметр minimize_records_per_block на всех ваших важных таблицах. Поиск в индексе с пропусками Я не упомянул о поиске в индексе с пропусками (index skip-scan) в этом томе; это механизм доступа к данным с использованием индекса, появившийся в вер- сии 9i, для выполнения запроса, который не ссылается на первый столбец (столбцы) индекса, но может использовать индекс как набор нескольких не- больших индексов.
496 Приложение А. Проблемы при обновлении версий Эта функциональность полезна в некоторых случаях, особенно если у вас сжатые индексы (compressed indexes) с очень небольшим количеством уникаль- ных значений в первом столбце (столбцах). Но иногда вы можете обнаружить, что поиск в индексе с пропусками вызывает проблемы с производительностью. Вы можете отключить эту функциональность, установив для параметра _ор- timizer_skip_scan_enabled значение false. Механизм AND-Equal Механизм выполнения, известный как and_equal, может использоваться в за- просах, которые включают проверку на равенство по неуникальным индексам, созданным на одном столбце. Хотя существует множество (плохих) индексов, созданных на одном столбце, вы вряд ли будете часто видеть применение этого механизма в стоимостной оптимизации, пока не обновите версию 8i до версии 9i. В качестве отрицательного побочного эффекта включения оценки стоимости использования ресурсов процессора в версии 9i могут обнаружиться ситуации, когда оптимизатор вдруг начинает использовать механизм and_equal там, где он раньше использовал индекс, созданный на одном столбце, или табличное сканирование. Результатом не всегда является улучшение. Однако в версии 10g оптимизатор больше не использует механизм and_ equal, если только не указана подсказка — а подсказка считается устаревшей. Этот механизм был в значительной степени заменен механизмом index_ combine. Механизм Index_combine использует битовые операции на битовых индексах; этот механизм появился еще в версии 8i и он гораздо гибче, чем and_equal, потому что не ограничен предикатами равенства или неуникальны- ми индексами, созданными на одном столбце (индексное соединение — index join — еще один новый, более гибкий механизм, который также помогает за- быть об and_equal). Для того чтобы оптимизатор мог использовать i ndex_combi пев качестве за- мены and_equal, обычно требуется, чтобы значение параметра _b_tree_ bitmap_plans было равно true — значение по умолчанию в версиях 9i и 10g (см. выше). Когда значение этого параметра равно true, оптимизатор может выполнять конвертацию индексов со структурой бинарного дерева в битовые индексы на таблицах, не имеющих битовых индексов. Я думаю, что обычно это приводит к несколько большему использованию ресурсов процессора и памяти по сравнению с выполнением простого соединения слияния наборов идентифи- каторов записей напрямую — но при этом преимуществом является большая доступность плана выполнения. Но есть случаи, когда изменение механизма приводит к изменению стоимо- сти; иногда это изменение стоимости приводит к изменению плана выполне- ния; и изредка новый механизм бывает менее эффективным, чем старый. В итоге обновление с версии 8i до версии 9i может привести к нежелатель- ному изменению плана выполнения из-за того, что оптимизатор начинает чаще использовать механизм and_equal; обновление с версии 9i до версии 10g может
Исправление ошибок при обработке входного списка 497 привести к нежелательному изменению плана выполнения, потому что оптими- затор полностью перестает использовать механизм and_equal. Индексное соединение хэширования Индексное соединение хэширования (index hash join) нечасто появляется в ка- честве плана выполнения по умолчанию в версии 8i, но после обновления до версии 9i и включения оценки стоимости использования ресурсов процессора этот механизм начинает использоваться чаще, потому что стоимость таблично- го сканирования стала выше. Индексное соединение хэширования (или просто индексное соединение) выполняется с помощью объединения содержимого двух (или больше) индексов таблицы, чтобы избежать обращения к самой таблице. В отличие от старого механизма and_equal, индексы не обязательно должны быть созданы на одном столбце, и предикаты не обязательно должны быть пре- дикатами равенства. На данный момент я не видел случаев, когда индексное со- единение приводило бы к проблемам с производительностью. Исправление ошибок при обработке входного списка Оптимизатор автоматически конвертирует входной список (in-list) в набор предикатов, разделенных оператором о г. В тестовом коде ниже показаны вер- сии до и после такой конвертации: where colx in (1,8,32) where colx = 1 or colx = 8 or colX = 32 Стандартная формула, которую использует оптимизатор для расчета селек- тивности предикатов, объединенных оператором or, не подходит к конвертации входного списка в набор операторов or, так как формула включает коэффици- ент (относящийся к двойному учету одного из предикатов), который неприме- ним к этому особому случаю. Однако оптимизатор использует эту стандартную формулу для обработки входных списков в версии 8i, и только в версии 9i появилась правильная фор- мула для входных списков. Это значит, что селективность входного списка по- высилась в версии 9i. Самым большим побочным эффектом является то, что итератор значений входного списка, который изначально использовал индекс, теперь может переключиться на табличное сканирование. Кроме того, измене- ние селективности может даже привести к изменению порядка соединения. На данный момент (включая версию 10.1.0.4) изменение формулы пока не коснулось выражения not in.
498 Приложение А. Проблемы при обновлении версий Переходное замкнутое выражение о Если х>10иг/ = х, то оптимизатор считает, что у > 10. о Если х = 10 и г/ = х, то оптимизатор считает, что у = 10. Такое предположение имеет название переходное замкнутое выражение (transitive closure), и оптимизатор применяет его двумя различными методами, в зависимости от версии и значений параметров. В случае, когда предикат с константой не является равенством, оптимизатор создает третий предикат для запроса, в результате чего первый пример имеет вид: where х > 10 and у > 10 and х = у В случае, когда предикат с константой является равенством (как показано во втором примере), оптимизатор создает второй предикат с константой, но удаляет предикат соединения, если query_rewrite_enabled = false, таким об- разом where х = 10 and у = 10 С другой стороны, если query_rewri te_enabled = true, то в версиях 8i и 9i предикат соединения остается даже в этом случае, таким образом where х = 10 and у = 10 and х = у Каждый раз, когда оптимизатор добавляет дополнительный предикат, это может повлиять на расчет кардинальности, поэтому переходное замкнутое вы- ражение — это то, о чем всегда надо помнить. Однако самым важным в этом аб- заце является то, что обновление до версии 10g может испортить некоторые планы выполнения, потому что при этом значение по умолчанию query_ rewrite_enabled становится равным true и при использовании переходного замкнутого выражения предикаты соединения перестают удаляться, в результа- те чего некоторые ваши планы выполнения изменятся (в версии 10gR2 внесены изменения, которые позволяют использовать другую стратегию переходного замкнутого выражения при проверке на равенство, в зависимости от нового скрытого параметра _transi ti vi ty_closure_retai n). Исправление расчетов, связанных с sysdate Рассмотрим следующие предикаты: where date_col between sysdate and sysdate + 1 where date_col between sysdate - 1 and sysdate where date_col between sysdate - 1 and sysdate +1 До версии 10g оптимизатор при расчете селективности обрабатывает sysdate как константу с известным значением, Hosysdate+/ - 1 обрабатывает как пере- менную связывания (или неизвестное значение). Эта разница также проявляет- ся при таких вызовах функций, как trunc(sysdate) и trunc(sysdate) +/- 1.
Параметр pga aggregate target 499 В результате оптимизатор обрабатывает показанные предикаты на основе диа- пазона как операцию AND по двум отдельным предикатам, используя 0,05 (5 %) в качестве селективности выражений sysdate +/- N. При этом рассчитанная кардинальность обычно абсолютно не верна. Эта ошибка исправлена в версии 10g. Для многих запросов на основе диа- пазонов, включающих различные варианты использования sysdate, значения селективности и кардинальности вдруг стали правильными. Но любые измене- ния в селективности и кардинальности могут иметь катастрофические послед- ствия для плана выполнения, даже если эти изменения связаны с исправлением ошибки. Индексирование значений null Есть варианты индексного доступа, по которым расчеты изменяются очень сильно при обновлении до версии 10.1.0.4. Если у вас есть индексы со структурой бинарного дерева на множестве столбцов, первые столбцы индекса используются в сканировании диапазона по индексу и при этом user_indexes . num_rows < user_tables . num_rows, то, по- хоже, оптимизатор перестает учитывать стоимость обращения к листовым блокам индекса в своих расчетах. Чтобы произошло такое изменение, нужно иметь в таблице всего одну запись, в которой во всех столбцах будут значения null, после чего еще произойдет изменение пути доступа к данным или измене- ние порядка соединения. Параметр pga_aggregate_target Выделение памяти для различных «объемных» операций в памяти теперь управляется параметром pga_aggregate_target, если вы установите для workarea_size_policy значение auto. Объем рабочей области для одной опе- рации (такой, как сортировка или хэширование) может достигать 5 % от значе- ния pga_aggregate_target (или 30 % при суммировании объемов всех подчи- ненных сессий — slaves — при выполнении параллельной операции). Оптимизатор рассчитывает стоимость операции, основываясь на значении 5 % (или 30 %). Если вы установите значение параметра, которое гораздо боль- ше реального, то возможно, что ограничение в 5 % никогда не будет достигнуто во время выполнения. Это значит, что оптимизатор может рассчитать для со- единений хэширования и соединений сортировки/слияния стоимость, которая окажется слишком низкой при сравнении с реальным использованием ресурсов при выполнении запроса. В версии 9i pga_aggregate_target не используется для сессий, работаю- щих через разделяемые серверы (shared servers, ранее называвшиеся MTS), по- этому для них по-прежнему используются sort_area_si ze и hash_area_si ze. Однако оптимизатор в своих расчетах использует данные, относящиеся к со- единению с выделенным сервером, даже когда соединение производится через
500 Приложение А. Проблемы при обновлении версий разделяемый сервер, поэтому вы можете обнаружить, что сессии, соединенные через разделяемые серверы, выдают неэффективные планы выполнения. В вер- сии 10g это было исправлено, и в PGA теперь выделяются большие объемы па- мяти даже для разделяемых серверов. Есть пара скрытых параметров (_smm_max_size и _pga_max_size), которые влияют на максимальный размер одной операции — упомянутое ранее ограни- чение в 5 % дополнительно ограничивается значением _pga_max_si ze / 2. Это может серьезно ограничивать работу системы, которая выполняет операции сортировки или хэширования на больших объемах данных, поэтому вам может потребоваться временно переключить некоторые сессии обратно на использо- вание ручной настройки размера рабочей области и явно указать значения sort_area_size и hash_area_size. Сортировка Алгоритмы расчета стоимости сортировки изменились в версии 9i при вклю- ченной системной статистике (оценке стоимости использования ресурсов про- цессора) — а оценка стоимости использования ресурсов процессора включена в версии 10g по умолчанию. Что еще более важно, стоимость сортировки в па- мяти больше не включает лишний компонент сохранения отсортированного на- бора данных на диск. Это значит, что могут быть случаи, когда стоимость сорти- ровки может внезапно стать гораздо меньше, чем была в предыдущих версиях Oracle, что может привести к значительным изменениям в плане выполнения. Однако если вы также включили автоматическую настройку размера рабочей области, то расчеты опять изменятся и эта проблема может не проявиться. Группировка Начиная с версии 9i, стоимость сортировки выражений group by и distinct была изменена, чтобы учесть тот факт, что размер результирующего набора данных может быть меньше размера входного потока данных. Если у вас слож- ные запросы, включающие агрегирующие подзапросы, которые превращают большое количество записей в небольшое, то стоимость этих подзапросов мо- жет значительно снизиться, что приведет к изменению планов выполнения. Контроль ошибок В новых версиях Oracle есть различные виды контроля ошибок (sanity checks), используемые при выполнении соединений, включающих множество столбцов. Из-за этого стоимостный оптимизатор может использовать отдельные значения селективности только с одной стороны соединения вместо расчета селективно- сти отдельно для каждого условия соединения. Это также может привести к тому, что оптимизатор использует значение l/num_rows одной из таблиц в ка-
Параметр optimizer mode 501 честве общей селективности, если иначе селективность окажется ниже этого значения. Выход за границы диапазона Только в версии 10.1.0.4 есть исправление ошибки, связанной со значениями предиката, которые выходят за границы наименыпего/наиболыпего значе- ний столбца. Исторически селективность предикатов столбец = {констан- та} и столбец between {константа 1} and {константа 2} соответствовала плотности столбца, то есть l/num_di sti net, когда {константа} находилась вне границ наименыпего/наиболыпего значений столбца. В версии 10.1.0.4, похоже, был добавлен код обработки «особого случая», который изменяет селектив- ность в зависимости от того, насколько далеко за пределами границы диапазона находится {константа}. Это применяется к равенствам, входным спискам и диапазонам. Это значит, что вы можете обнаружить, что у запросов, которые нормально работали даже с устаревшей статистикой (особенно в случае наименыпего/наи- болыпего значений), внезапно изменились планы выполнения. В большинстве случаев количество записей, рассчитываемое оптимизатором, уменьшается по мере того, как значения запроса все больше удаляются от известного диапазона. Использование неправильных типов данных Только в версии 10.1.0.4 реализована внутренняя обработка «особого случая», которая решает некоторые проблемы, связанные с тем, что приложения сторон- них разработчиков хранят числа (особенно порядковые номера) в символьном столбце. Если оптимизатор видит предикат на основе диапазона по символьно- му столбцу, в котором наименьшее и наибольшее значения выглядят как числа, и предикат производит сравнение с чем-то, что также выглядит как число, то оптимизатор применяет стандартные расчеты для числовых эквивалентов сим- вольных значений для получения селективности. Похоже, что есть случаи, когда в результате этого селективность очень силь- но вырастает, что может изменить планы выполнения некоторых часто исполь- зуемых запросов. Параметр optimizer.mode Взгляните на сценарий first_rows.sql в главе 1, чтобы увидеть пример проблем, связанных с использованием f 1 rst_rows. Параметр f 1 rst_rows является уста- ревшим, начиная с версии 9i — f i rst_rows_l может оказаться лучшим вариан- том для многих OLTP-систем, но и f i rst_rows_10 также может оказаться хо- рошим вариантом. Значение по умолчанию в версиях 8i и 9i равно choose, что
502 Приложение А. Проблемы при обновлении версий обычно означает использование оптимизации по синтаксису (rule based opti- mization), если ни один объект, участвующий в запросе, не имеет статистики. Но если хотя бы один объект, участвующий в запросе, имеет статистику или если у одного из объектов запроса имеется функциональность, в результате ко- торой используется стоимостная оптимизация, то происходит переключение на оптимизацию all_rows. Оптимизация по синтаксису больше не поддерживается в версии 10g, в ко- торой теперь значение по умолчанию -all_rows. Упорядоченные по убыванию индексы В версиях, предшествующих 10g, существует ошибка, из-за которой оптимиза- тор дважды учитывает влияние предиката, содержащего упорядоченный по убыванию индекс (descending index), что приводит к слишком низкой карди- нальности. Эта ошибка была исправлена в версии 10g, поэтому у запросов, ис- пользующих упорядоченные по убыванию индексы, могут измениться планы выполнения, потому что изменилась рассчитанная кардинальность таблицы. Слияние комплексных представлений Значение параметра_complex_vi ew_merging по умолчанию равно false в вер- сии 8i и true, начиная с версии 9i. Однако в версии 9i слияние представлений производится без учета стоимости. Только в версии 10g происходит расчет стоимости при использовании слияния и без использования слияния и выбира- ется вариант с меньшей стоимостью. При обновлении с версии 8i до версии 9i вы можете захотеть не выполнять слияние в некоторых запросах — для этого может быть использована подсказка no_merge(view_alias), указанная в бло- ке внешнего запроса, или подсказка no_merge, указанная в самом представле- нии. Уменьшение уровней вложенности запроса Значение параметра_unnest_subquery по умолчанию равно false в версии 8i и true, начиная с версии 9i. Однако в версии 9i уменьшение уровней вложенно- сти запроса производится без учета стоимости. Только в версии 10g происходит расчет стоимости двух вариантов и выбирается вариант с меньшей стоимостью. При обновлении с версии 8i до версии 9i вы можете захотеть блокировать уменьшение уровней вложенности в некоторых запросах. Для этого может быть использована подсказка no_unnest, указанная в подзапросе. Уменьшение уровней вложенности запроса отключено в версиях 8i и 9i (даже при указании подсказки), если star_transformation_enabled = true.
Динамическая выборка 503 Скалярные подзапросы и подзапросы фильтрации В версиях 8i и 9i размер хэш-таблицы, используемой для оптимизации скаляр- ных подзапросов и подзапросов фильтрации во время выполнения, похоже, фик- сирован на уровне 256 записей. В версии 10g под размер хэш-таблицы выделя- ется фиксированный объем памяти. Это значит, что количество сохраненных записей может быть разным. Когда вы обновляете версию, это изменение способно повысить производи- тельность выполнения некоторых запросов, потому что количество сохранен- ных значений может увеличиться. Однако если входные и выходные значения подзапросов большие (подзапрос, возвращающий значение типа varchar2 без ограничения длины, является самой большой проблемой), то производитель- ность может ухудшиться без изменения плана выполнения, так как сохраняется меньшее количество значений и подзапросы выполняются чаще. Изменения при выполнении параллельных запросов Существует интересное изменение в стратегии оценки стоимости выполнения параллельных запросов, когда вы переходите от версии к версии Oracle. В вер- сии 8i запросы оптимизируются для получения наилучшего плана последова- тельного выполнения перед тем, как выполнить запрос параллельно. В версии 9i оптимизатор изначально предполагает, что запрос будет выполняться парал- лельно, и выполняет соответствующую оценку стоимости — это приводит к большему использованию в планах выполнения соединений хэширования и сортировки/слияния. В версии 10g правила опять изменяются — был добав- лен поправочный коэффициент, равный 90 %, который делает стратегии соеди- нения, интенсивно использующие ресурсы, несколько менее привлекательными. Динамическая выборка Динамическая выборка (dynamic sampling) была добавлена в версии 9.2 со зна- чением по умолчанию, равным 1. Она позволяет оптимизатору выбирать (по меньшей мере) 32 блока из таблиц во время выполнения оптимизации, если для этого есть соответствующие условия. Существуют 11 уровней динамиче- ской выборки, все они могут быть установлены на уровне системы, сессии, за- проса или таблицы (внутри запроса). В версии 10g для динамической выборки установлен уровень по умолчанию 2, то есть из любой таблицы без статистики будет динамически выбрано 32 бло- ка. Это может быть очень полезным при использовании глобальных временных таблиц (global temporary tables, GTTs) с нормальными таблицами. Однако если вы регулярно выполняете множество очень небольших специфических запросов,
504 Приложение А. Проблемы при обновлении версий включающих глобальные временные таблицы, то стоимость динамической вы- борки может оказаться выше, чем стоимость самого запроса, поэтому вам мо- жет понадобиться найти некий оптимальный путь отключения динамической выборки или использовать пакет dbms_stats для добавления актуальной ста- тистики в определение таблицы. Временные таблицы В версии 10g значение по умолчанию параметра optimizer_dynamic_sampli ng равно 2, то есть из любого объекта, не имеющего статистики, будет осуществ- ляться динамическая выборка 32 блоков во время выполнения. Так как глобаль- ные временные таблицы (global temporary tables, GTTs) не имеют статистики (если только вы не использовали какой-нибудь хитрый трюк), то у запросов, использующих глобальные временные таблицы, могут произойти неожиданные изменения в планах выполнения после обновления версии. Статистика в словаре данных Хотя я не думаю, что это явно утверждалось в руководствах, есть краткий ком- ментарий в документации к патчу 9.2.0.5, в котором написано, что вы должны удалить статистику в словаре данных перед установкой патча, а затем пересоз- дать ее после установки — то есть похоже, что корпорация Oracle не имеет ни- чего против наличия статистики на таблицах словаря данных в версии 9.2. Более того, учитывая автоматический сбор статистики, который произво- дится в версии 10g, вы обнаружите, что статистика на таблицах словаря данных будет создана в течение 24 часов после создания вами первой базы данных в версии 10g. Если у вас есть SQL-код, работающий со словарем данных (на- пример, в пакетах, которые динамически создают новые секции для секциони- рованных таблиц), то вы можете обнаружить, что наличие статистики в словаре данных может дать определенные преимущества. Вы также можете обнару- жить, что эта статистика может привести к проблемам с производительностью при обновлении версии. Не забудьте протестировать ваш код технической под- держки при выполнении регрессионного тестирования при обновлении версии.
Приложение Б. Параметры оптимизатора Существует множество параметров, которые относятся к оптимизации, и мно- гие из них изменяют свои значения по умолчанию при переходе с версии на версию. Если у вас есть проблема с некоторым запросом после обновления, свя- занная с производительностью, всегда полезно проверить, существуют ли ка- кие-то параметры с новыми значениями по умолчанию, которые, судя по их именам, могут иметь отношение к проблемному запросу. Например, несколько людей жаловались на проблемы с производительно- стью планов выполнения, которые неожиданно стали включать операцию bit- map conversion (from rowids) после обновления с 8i на 9i. Проверьте список изменений, и вы увидите, что параметр с названием _b_tree_bi tmap_plans из- менил свое значение с false на true. Не выглядит ли это достаточно подозри- тельно? Возможно, вы можете проверить, что произойдет, если вы вернете зна- чение false. (Лучший вариант появился в 10g, где вы можете использовать подсказку no_i ndex_combine () для некоторых запросов с такой проблемой.) Существуют еще два других источника информации о параметрах, которые могут влиять на вычисления оптимизаторов. Один набор значений появляется в файле трассировки 10053; другой (только в 10g) в динамических представле- ниях производительности v$sys_optimizer_env, v$ses_optimizer_env и v$sql_ optimizer_env. Эти представления показывают параметры, относящиеся к оп- тимизатору, на уровне системы, сеанса и отдельного дочернего курсора, соот- ветственно, — последнее представление также содержится как один столбец optimizer_env в v$sql. Возможно, лучшее место, куда можно обратиться для исследования эффек- тов изменения параметров на оптимизатор, — это один особый параметр opti - mizer_features_enable. Особая польза этого параметра состоит в том, что он позволяет вам точно определить параметры, относящиеся к оптимизации, кото- рые могут вызвать проблемы с производительностью при обновлении. Параметр optimizer_features_enable Если вы возьмете базу данных Oracle 10.1.0.4, измените параметр optimi- zer_features_enable на 8.1.7, 9.2.0 и 10.1.0 и поместите системные параметры в таблицу, вы затем можете выполнять запросы к таблице, чтобы посмотреть, какие из параметров изменяются в то же самое время. В табл. Б.1 приведены 48 затрагиваемых параметров (многие из них скры- тые). Некоторые из этих параметров не существуют в 9i или в 8i.
506 Приложение Б. Параметры оптимизатора Таблица Б.1. Параметры, на которые влияет optimize >r_features_enable i [log) Параметр 10.1.0.4 9.2.0.6 8.1.7.4 _always_antijoin choose choose off *** _always_semijoin choose choose Qff *** _b_tree_bitmap_plans true true false _complex_view_merglng true true false _cost_equality_semijoin true true false _cpu_to_io 0 0 100 *** _generalized_pruning_enabled true true false _gs_anti_semiJoin_allowed true true false JndexJoin_enabled true true false Joad_without_compile none none none Jocal_communicatlon_costing_enabled true false *** false __newjnitialjoin_orders true true false __new_sort_cost_estimate true true false _optim_adjust_for_part_skews true true false _optim_new_defaultjoin_sel true true false _optim_peek_user_binds true true false _optimizer_computejndex_stats true false false _optimizer_correct_sq_selectivity true false false _optimizer_cost_based_transformation linear off off _optimizer_cost_model choose choose io _optimizer_dim_subqjoin_sel true false false _optimizerjoin_order_control 3 0 0 _opb'mizerjoin_sel_sanity_check true false false _optimizer_max_permutatlons 2000 2000 80000 _optimizer_newjoin_card_computation true true false _optimizer_skip_scan_enabled true true false _optimizer_squ_bottomup true false false _optimizer__system_stats_usage true true fqlse _optimizer_undo_cost_change 10.1.0 9.2.0 8.1.7 _ordered_nested_loop true true false _parallel_broadcast_enabled true true false _partition_view_enabled true false false _pre_rewrite_push_pred true true false _pred_move_around true true false _pushjoin_predicate true true false _pushjoin_union_view true true false _pushjoin_union_view2 true false false _query_rewrite_setopgrw_enable true false false _remove_aggr_subquery true false false _rig ht_outer_hash_enable true false false _table_scan_cost_plus_one true true false _union_rewrite_for_gs yes_gset_mvs yes_gset_mvs *** off _unnest_subquery true true false optimizer_dynamic_sampling 2 1 0 optimizer_features_enable 10.1.0 9.2.0 8.1.7
Файл трассировки 10053 507 Параметр 10.1.0.4 9.2.0.6 8.1.7.4 optimizer_mode all_rows choose choose query_rewrite_enabled true false false sklp_unusablejndexes true false false Знак *** говорит о том, что существуют некоторые параметры, у которых значение по умолчанию в «реальной» версии базы данных не согласуется со значением, устанавливаемым Oracle 10g. Вот эти отличия: О _always_anti_join по умолчанию nested_loops, не off в 8.1.7.4. О _always_semi_join по умолчанию standard, не off в 8.1.7.4. О _cpu_to_io по умолчанию null, не 100 в 8.1.7.4. О _union_rewri te_for_gs по умолчанию choose, не yes_gset_mvs в 9.2.O.6. О _local_communication_costing_enabled по умолчанию true, не false в 9.2.0.6 (этот параметр, вероятно, имеет отношение к RAC, а не к распределенным запросам. Мне никогда не удавалось найти отличия в стоимостях между рас- пределенными запросами и запросами к одной базе данных). Файл трассировки £0053 Когда вы включаете трассировку оптимизатора (событие 10053) на уровне 1, начальная секция файла трассировки показывает список параметров оптимиза- тора. Этот список значительно варьируется в разных версиях, поэтому я привел перекрестные ссылки значений, отображаемых в разных версиях, в табл. Б.2. Я нахожу особенно полезным обращаться к этой таблице время от времени при обновлении системы, тщательно следя за параметрами, которые меняют свое значение с false на true (например, unnest_subquery). Иногда проблемы с производительностью могут возникнуть из-за включе- ния новой возможности, и этот список может подсказать вам, какая из возмож- ностей работает не очень хорошо с вашим распределением данных. Основная особенность данного списка — это набор параметров 10g. Все параметры, скры- тые в 10g, но не скрытые в более ранних версиях, помечены знаком ** в столб- цах, соответствующих версиям, в которых они видимы. Таблица Б.2. Перекрестные ссылки для параметров, приведенных в файле трассировки 10053 Параметр 10.1.0.4 9.2.0.6 8.1.7Д _add_stale_mv_to_dependency_list true _always_antijoln choose choose _always_semijoin choose choose _always_star_transformation false false false _b_tree_bitmap_plans true true false _bt_mmv_query_rewrite_enabled true _complex_vlew_merging true true false продолжение &
508 Приложение Б. Параметры оптимизатора Таблица Б.2 (продолжение) Параметр 10.1.0.4 9.2.0.6 8.1.7.4 _convert_set_tojoln false _cost_equallty_semijoin true _cpu_tojo 0 0 _default_nori_equality_sel_check true true true _dlsable_datalayer_sampling false _dlsable_function_based_index false _distinct_view_unnesUng false _dml_monltorlng_enabled true _ellmlnate_common_subexpr true _enable_type_dep_selectivity true true true _fast_full_scan_enabled true true true _fic_area_slze 131 072 _force_datefold_trunc false _force_temptables_for_gsets (10g) false false _gsets_always_use_temptables (9i) _full_pwlsejoln_enabled true _generalized_prunlng_enabled true _gs_antl_semljoln_allowed true true _hashjoln_enabled true true ** true ** _hash_multiblockjo_count 0 0 0 Jmproved_outeijoln_card true true true Jmproved_row_length_enabled true true true Jndex_joln_enabled true true false _left_nested_loops_random true _like_with_blnd_as_equallty false false JocaLcommunlcation_costing_enabled true Jocal_communlcation_ratio 50 _minimal_stats_aggregatlon true _mmv_query_rewrlte_enabled false _nested_loop_fudge 100 100 100 _new_lnitlaljoln_orders true true false _new_sort_cost_estimate true true _no_or_expanslon false false false _oneside_colstat_for_equljoins true true true _optim_adjust_for_part_skews true _optim_enhance_nnull__detection true true true _optim_new_defaultjoin_sel true _optim_peek_user_binds true _optlmizer_adjust_for_nulls true true true _optlmizer_block_slze 8192 _optlmlzer_cache_stats false _optimlzer_cbqt_factor 50 _optimizer_cbqt_no_size_restriction true compute_index_stats true
Файл трассировки 10053 509 Параметр 10.1.0.4 9.2.0.6 8.1.7.4 _optimizer_correct_sq_selectivity true _optimizer_cost_based_transformation linear _optimizer_cost_filter_pred false _optimizer_cost_model choose choose _optimizer_degree 0 _optimizer_dim_subqjoin_sel true _optimizer_disable_strans_sanity_checks 0 _optimizerjgnore_hints false _optimizerjoin_order_control 3 _optimizerjoiri_sel_sanity_check true _optimizer_max_permutations 2000 2000 ** 80 000 ** _optlmizer_mjc_enabled true _optimlzer_mode_force true true true _optimlzer_newjoin_card_computation true _optimlzer_percent_parallel 101 101 Q ** _optimlzer_push_down_distlnct 0 _optimlzer_push_pred_cost_based true _optimizer_random _plan 0 _optimizer_search_limit 5 5 ** 5 ** _optimizer_skip_scan_enabled true _optimizer_sortmerge_join_enabled true _optimizer_squ_bottomup true _opb’mizer_system_stats_usage true _optimlzer_undo_changes false false false _optimizer_undo_costLchange 10.1.04 _or_expand_nvl_predicate true true true _ordered_nested_loop true true false _parallel_broadcast_enabled true true ** false ** _partial_pwisejoin_enabled true _partib’on_vlew_enabled true false ** false ** _pga_max_size 204 800 Кбайт _pre_rewrite_push_pred true _pred_move_around true true _predicate_eliminatlon_enabled true _project_view_columns true _pushjoin_predicate true true false _pushjoln_union_vlew true true false _pushjoin_union_view2 true _px_broadcast_fudge_factor 100 _query_cost_rewrite true true true _query_rewrite_l true _query_rewrite_2 true _query_rewrite_drj true _query_rewrite_expression true true ** true ** _query_rewrite_fpc true _query_rewrite_fudge 90
510 Приложение Б. Параметры оптимизатора Таблица Б.2 (продолжение) Параметр 10.1.0.4 9.2.0.6 8.1.7.4 _query_rewritejgmigrate true _query_rewrite_maxdisjunct 257 _query_rewrite_or_error false _query_rewrite_setopgrw_enable true _query_rewrite_vop_cleanup true _remove_aggr_subquery true _right_outer_hash_enable true _slave__mapping_enabled true _smm_auto_cost_enabled true _smm_auto_max_lo_slze 248 Кбайт _smni_auto_mirL.io_size 56 Кбайт _smm_max_size 10 240 Кбайт _smm_min_size 204 Кбайт _smm_px_max_size 61 440 Кбайт _sort_elimination_cost_ratio 0 0 0 _sort_multiblock_read_count 2 _sort_space_for_write_buffers 1 _spr_push_pred_refspr true _subquery_pruning_enabled true true true _subquery_prunlng_mv_enabled false _systemjndex_caching 0 0 _table_scan_cost_plus_one true true false _union_rewrite_for_gs yes_gset_mvs _unnest_subquery true true false _use_column stats_for_funcdon true true true _attive_instance_count 1 bitma p_merge_a rea_size 1 048 576 cpu_count 2 cursor_sharing exact db_file_multiblock_read_count 8 8 8 flashback_table_rpi non_fbt hash_area_size 131 072 2 097 152 2 097 152 optimizer_dynamic_sampling 2 1 optimizer_features_enable 10.1.0.4 9.2.0 8.1.7 optimizerjeatures_hinted 0.0.0 optimizer_index__caching 0 0 0 optimizerjndex_cost_adj 100 100 100 optimizerjnode all_rows choose choose optimizer_mode_hinted false parallel_ddLforced_degree 0 parallel_ddl_forcedjnstances 0 parallel_ddLmode enabled parallel_dml_forced_dop 0 parallel_dml_mode disabled parallel_execution_enabled true parallel query mode 0
Представление v$sql_optimlzer_env 511 Параметр 10.1.0.4 9.2.0.6 8.1.7.4 query_forced_dop enabled parallel_threads_per_cpu 2 pga_aggregate_target 204 800 Кбайт query_rewrite_enabled false false false query_rewritejntegrity enforced enforced enforced skip_u nusableJndexes true sort_area_retained_size 0 sort_area_size 65 536 1 048 576 2 097 152 sqlstat_enabled false star_transfbrmation_enabled false false false statisticsjevel typical workarea_size_policy auto Следующие параметры приводятся в файлах трассировки 8i и 9i, существу- ют в 10g, но не приводятся в файле трассировки 10g: _OPTIMIZER_CHOOSE_PERMUTATION = 0 _SUBQUERY_PRUNING_COST_FACTOR = 20 _SUBQUERY_PRUNING_REDUCTION_FACTOR = 50 _USE_NOSEGMENT_INDEXES = FALSE Следующий параметр приводится только в файле трассировки 9i, существу- ет в 10g, но не приводится в файле трассировки 10g: _OPTIMIZER_DYN_SMP_BLKS = 32 Следующий параметр приводится только в файле трассировки 9i и не суще- ствует в 10g: _SORTMERGE_INEQUALITY_JOIN_OFF = FALSE Представление v$sql_optimizer_env Существует короткий список параметров оптимизатора, показанный в табл. Б.З, которые есть у каждого курсора в SGA. Они могут быть получены с помощью представления v$sql_optimizer_env. Тот же самый список существует на уровне сеанса (как v$ses_optimizer_env) и на уровне системы (как v$sys_ optimizer_env). Учитывая длинный список параметров из трассировки 10053, несколько странно, что этот список такой короткий. Значения, приведенные здесь, — это значения по умолчанию для 10.1.0.4 из системы с одним процессо- ром, использующей технологию hyper-threading, в порядке, в котором они по- являются при выполнении запроса к представлению. Таблица Б.З. Параметры, записываемые для каждого курсора Параметр Значение parallel_execution_enabled true optimizer_features_enable 10.1.0.4 cpu_count 2 Л -------------------------------------------------------------продолжение
512 Приложение Б. Параметры оптимизатора Таблица Б.З (продолжение) Параметр Значение active Jnstance_count parallel_threads_per_cpu hash_area_size bitmap_merge_area_size sort_area_size sort_area_retained_size db_file_multiblock_read_count pga_aggregate_target parallel_query_mode parallel_dml_mode parallel_ddl_mode optimizer_mode cursor_sharing star_transformation_enabled optimizer_index_cost_adj optimlzer_index_caching query_rewrite_enabled query_rewritejntegrity workarea_size_policy optimizer_dynamic_sampling statisticsJevel skip_unusable_indexes 1 2 131 072 1 048 576 65 536 0 8 204 800 Кбайт enabled disabled enabled all_rows exact false 100 0 false enforced auto 2 typical true
Алфавитный указатель ..Параметры. Значения в различных версиях Oracle _active_instance_count, 510 _add_stale_mv_to_dependency_list, 507 _always_anti_Join, 506, 507 _always_semi_join, 506, 507 _always_star_transformation, 507 _b_tree_bitmap_plans, 506, 507 _bt_mmv_query_rewrite_enabled, 507 _complex_view_merging, 506, 507 _convert_set_to_Join, 508 _cost_equality_semi_join, 506, 508 _cpu_to_io, 506, 508 _default_non_equality_sel_check, 508 _disable_datalayer_sampling, 508 _disable_function_based_index, 508 _distinct_view_unnesting, 508 _dml_monitoring_enabled, 508 _eliminate_common_subexpr, 508 _enable_type_dep_selectivity, 508 _fast_full_scan_enabled, 508 _fic_area_size, 508 _force_datefold_trunc, 508 _force_temptables_for_gsets, 508 _full_pwise_join_enabled, 508 -generalized—pruning_enabled, 506, 508 _gs_anti_semi_Join_allowed, 506, 508 _gsets_always_use_temptables, 508 _hash_join_enabled, 508 _hash_multiblock_io_count, 508 _improved_outeijoin_card, 508 _improved_row_length_enabled, 508 _index_Join_enabled, 506, 508 _left_nested_loops_random, 508 _like_with_bind_as_equality, 508 _load_without_compile, 506 local communication costing enabled, 506, 508 _local_communication_ratio, 508 _minimal_stats_aggregation, 508 _mmv_query_rewrite_enabled, 508 _nested_loop_fudge, 508 _new_initial_join_orders, 506, 508 _new_sort_cost_estimate, 506, 508 _no_or_expansion, 508 _oneside_colstat_for_equijoins, 508 _optim_adjust_for_part_skews, 506, 508 _optim_enhance_nnull_detection, 508 _optim_new_default_join_sel, 506, 508 _optim_peek_user_binds, 506, 508 _optimizer_adjust_for_nulls, 508 _optimizer_block_size, 508 _optimizer_cache_stats, 508 _optimizer_cbqt_factor, 508 _optimizer_cbqt_no_size_restriction, 508 _optimizer_ceil_cost, параметр влияние на округление, 47 _optimizer_compute_index_stats, 506 _optimizer_correct_sq_selectivity, 506, 509 _optimizer_cost_based_transformation, 506, 509 _optimizer_cost_filter_pred, 509 _optimizer_cost_model, 506, 509 _optimizer_degree, 509 _optimizer_dim_subq_Join_sel, 506, 509 _optimizer_disable_strans_sanity_checks, 509 _optimizerJgnoreJbints, 509 _optimizer_Join_order_control, 506, 509 -OptimizerJoin_sel_sanity_check, 506, 509 _optimizer_max_permutations, 506, 509 _optimizer_mjc_enabled, 509 _optimizer_mode-force, 509 _optimizer_newJoin_card_computation, 506, 509 _optimizer_percent_parallel, 509 _optimizer_push-down_distinct, 509 _optimizer_push_pred_cost_based, 509 _optimizer_random _plan, 509 -Optimizer_search_limit, 509 _optimizer_skip_scan_enabled, 506, 509 _optimizer_sortmerge_Join_enabled, 509 _optimizer_squ_bottomup, 506, 509
514 Алфавитный указатель _optimizer_system_stats_usage, 506, 509 _optimizer_undo_changes, 509 _optimizer_undo_cost_change, 506, 509 _or_expand_nvl_predicate, 509 _ordered_nested_loop, 506, 509 _parallel_broadcast_enabled, 506, 509 _partial_pwisejoin_enabled, 509 _partition_view_enabled, 506, 509 _pga_max_size, 509 _pre_rewrite_push_pred, 506, 509 _pred_move_around, 506, 509 _predicate_elimination_enabled, 509 _project_view_columns, 509 _push_Join_predicate, 506, 509 _pushjoin_union_view, 506, 509 _pushjoin_union_view2, 506, 509 _px_broadcast_fudge_factor, 509 _query_cost_rewrite, 509 _query_rewrite_l, 509 _query_rewrite_2, 509 _query_rewrite_dij, 509 _query_rewrite_expression, 509 _query_rewrite_fpc, 509 _query_rewrite_ftidge, 509 _query_rewrite_Jgmigrate, 510 _query_rewrite_maxdisj unct, 510 _query_rewrite_or_error, 510 _query_rewrite_setopgrw_enable, 506, 510 _query_rewrite_vop_cleanup, 510 _remove_aggr_subquery, 506, 510 _right_outer_hash_enable, 506, 510 _slave_mappmg_enabled, 510 _smm_auto_cost_enabled, 510 _smm_auto_max_io_size, 510 _smm_auto_min_io_size, 510 _smm_max_size, 510 _smm_min_size, 510 _smm_px_max_size, 510 _sort_elimination_cost_ratio, 510 _sort_multiblock_read_count, 510 _sort_space_for_write_buffers, 510 _spr_push_pred_refspr, 510 subquery pruning enabled, 510 _subquery_pruning_mv_enabled, 510 _system_index_caching, 510 _table_scan_cost_plus_one, 506, 510 _temp_tran_block_threshold, параметр, 289 __union_rewrite_for_gs, 506, 510 _unnest_subquery, 506, 510 _use_column stats_for_function, 510 1-9 10.1.0.4, проблемы с индексным доступом к В-дереву, 107 10g граничные случаи со списками значений, 75 и параллельное выполнение, 56 переход с других версий, 324 поведение за пределами диапазона, 76 появление автономного оптимизатора, 27 расширение селективности, 77 стоимость использования процессорных ресурсов, 493 эффекты системной статистики, 45 10gR2 изменения в трассировке 10053, 447 новая реализация выражения group by, 434 8i «двойной подсчет», 75 optimizer_mode, параметр, 104 граничные случаи со списками значений, 75 и параллельное выполнение, 54 недооценка кардинальности списка значений, 87 поведение оптимизатора, 26 слияние комплексных представлений, 31 9i граничные случаи со списками значений, 75 и параллельное выполнение, 54 оценка стоимости процессорных ресурсов, 41 поведение оптимизатора, 26 эффекты размеров блоков, 39 А access_predicates, столбец в plan_table, 102 active _instance_count, параметр курсора, 512 agg_sort,sql, тестовый сценарий, 431 agg_sort_2.sql, тестовый сценарий, 434 all_rows, режим оптимизатора, 24 alter session, команда, 101
Алфавитный указатель 515 analyze, команда, 492 использование для создания частотной гистограммы, 192 AND использование со значениями null и not in, 283 anti_01.sql, тестовый сценарий, 278 AskTom, веб-сайт, 235 ASSM, 34, 124, 126 блоки, 125 и отметка максимального уровня заполнения, 37 побочный эффект, 41 assm_test.sql, тестовый сценарий, 125 В base_line.sql, тестовый сценарий, 116, 137 BCHR, 51 проблемы, 51 begin имя_процедуры (...)end, 186 best_cst, и битовые индексы, 216 big 10053.sql, тестовый сценарий, 447 bind_between.sql, тестовый сценарий, 85 birth_month._01.sql, тестовый сценарий, 69 birth_month_02.sql, тестовый сценарий, 72 bitmap conversion (to rowids), строка, 233, 234 bitmap minus, операция, 226 bitmap_cost_02.sql, тестовый сценарий, 219 bitmap_cost_03.sql, тестовый сценарий, 222 bitmap_cost_03a.sql, тестовый сценарий, 224 bitmap_cost_04.sql, тестовый сценарий, 225 bitmap_cost_05.sql, тестовый сценарий, 228 bitmap_cost_06.sql, тестовый сценарий, 231 bitmap_cost_07.sql, тестовый сценарий, 234 bitmap_cost_08.sql, тестовый сценарий, 235 bitmap_mbrc.sql, тестовый сценарий, 218 bitmap_merge_area_size, параметр значения в разных версиях Oracle, 510 bitmap_merge_area_size, параметр курсора, 512 bitmap_or.sql, тестовый сценарий, 227 blevel, 90 значения, 98 изменение при трансформации битовых индексов, 234 установка в единицу для индексов, 112 book_subq.sql, тестовый сценарий, 280 btree_cost_01.sql, тестовый сценарий, 92 btree_cost_02.sql, тестовый сценарий, 99, 106, 107, 108 btree_cost_02a.sql, тестовый сценарий, 108, 109 btree_cost_03.sql, тестовый сценарий, 104, 105 btree_cost_04.sql, тестовый сценарий, 102 btree_cost_05.sql, тестовый сценарий, 110 buildfrag, элемент трассировки 10053, 376 С c_mystats.sql, тестовый сценарий, 395 c_skew_freq.sql, тестовый сценарий, 190 c_skew_freq_01-sql, тестовый сценарий, 192 c_skew_freq_02.sql, тестовый сценарий, 194 c_skew_ht_01.sql, тестовый сценарий, 197 calc_mbrc.sql, тестовый сценарий, 37 cartesian.sql, тестовый сценарий, 429 СВО, 24 char_fun.sql, тестовый сценарий, 148 char_seq.sql, тестовый сценарий, 151, 152 char_types.sql, тестовый сценарий, 145 choose, значение параметра optimizer_mode, 25 clufac_calc.sql, тестовый сценарий, 138 clustering_factor, 115 col_order.sql, тестовый сценарий, 130 compute_index_stats, параметр значения в разных версиях Oracle, 508 constraint_01-sql> тестовый сценарий, 173 constraint_02.sql, тестовый сценарий, 174 constraint_03.sql, тестовый сценарий, 175 CPU costing, 26 cpu_costing.sql, тестовый сценарий, 48 cpu_count, параметр значения в разных версиях Oracle, 510 cpu_count, параметр курсора, 511 cpuspeed, единицы измерения, 28 cursor_sharing, параметр, 186 значение, 510 и переменные связывания, 186 cursor_sharing, параметр курсора, 512 cursor_sharing,napaMerp, 187 /*+ cursor_sharing_exact */, подсказка, 186 D date_oddity.sql, тестовый сценарий, 147, 203, 204 db_file_multiblock_read_count, параметр, 218, 219, 225, 230, 232, 235, 236 значения в разных версиях Oracle, 510
516 Алфавитный указатель db_file_multiblock_read_count, параметр курсора, 512 dbf_mbrc, скорректированное значение вычисление, 36, 37 проверка, 39 dbms_lock, пакет, 116 dbms_random, пакет, 179 использование, 35 сбор статистики кэширования, 53 dbms_repair.rebuild_freelists(), процедура, 129 dbms_stats, пакет, 136, 315, 492 get_param, процедура, 71 использование с включенной трассировкой SQL-кода, 182 dbms_stats.gather_index_stats(), вызов, 137 dbms_stats.gather_table_stats, процедура method_opt, параметр, 71 dbms_stats.set_column_stats, процедура, 70 dbms_stats.set_index_stats, процедура, 224, 234, 236 dbms_xplan, пакет, 34 использование с предикатами на основе ограничений, 175 планы выполнения, 63 создание, 34 dbms_xplan.display(), 170 dbmsutiLsql, сценарий, 34 dbmsutksql, сценарий, 34 defaults.sql, тестовый сценарий, 153, 207 deg, элемент трассировки 10053, 375 delete_anomaly.sql, тестовый сценарий, 336 dependent.sql, тестовый сценарий, 163, 165, 167 descending_bug.sql, тестовый сценарий, 335 discrete_01.sql, тестовый сценарий, 154, 158 discrete_01a.sql, тестовый сценарий, 158 discrete_02.sql, тестовый сценарий, 155 discrete_02a.sql, тестовый сценарий, 158 dist_hist.sql, тестовый сценарий, 189 DML-операции и битовые индексы, 214 DSS-системы использование переменных связывания, 82 dynamicjsampling, подсказка, 164, 165, 166 Е endpoint_actual_value, столбец, 145 explain plan, 43, 49, 65 использование параметра cursor_sharing, 187 extra_col.sql, тестовый сценарий, 133 F feke_hist.sql, тестовый сценарий, 195 FBI (function-based indexes), 149 filter_cost_01.sql, тестовый сценарий, 239, 270 filter_cost_01a.sql, тестовый сценарий, 248 filter_cost_02.sql, тестовый сценарий, 247 filter_predicates, стоблец в plan_table, 102 first_rows, режим оптимизатора, 25 first_rows.sql, тестовый сценарий, 25 first_rows_N, режим оптимизатора, 25 flashback_table_rpi, параметр значения в разных версиях Oracle, 510 flg.sql, тестовый сценарий, 128 free list, класс в представлении v$waitstat, 128 free_lists.sql, тестовый сценарий, 120 freelists.sql, тестовый сценарий, 138, 139 fun_sel.sql, тестовый сценарий, 161 G gather_index_stats(), процедура, 150 gby_onekey.sql, тестовый сценарий, 432 get_index_stats, процедура, 136 get_param, процедура, 71 group by новая реализация в 10gR2, 434 ошибка, 433 н hack stats.sql, тестовый сценарий, 59, 70, 1077136,196, 223 has_cpu_hamess.sql, тестовый сценарий, 379 has_dump.sql, тестовый сценарий, 380 has_nocpu_harness.sql, тестовый сценарий, 379 Hash join one ptn, элемент трассировки 10053, 376 Hash join, элемент трассировки 10053, 376 hash_area, элемент трассировки 10053,375 hash_area_size, параметр значения в разных версиях Oracle, 510 hash_area_size, параметр курсора, 512 hash_multi.sql, тестовый сценарий, 370, 374
Алфавитный указатель 517 hash._one.sql, тестовый сценарий, 360, 363, 367, 370, 377, 378 hash._one_bad.sql, тестовый сценарий, 377 hash._opt.sql, тестовый сценарий, 354, 375 hash._pat_bad.sql, тестовый сценарий, 377 hasb._stream_a.sql, тестовый сценарий, 383 hist_intro.sql, тестовый сценарий, 178 hist_sel.sql, тестовый сценарий, 201 HWM, 37 I in_list.sql, тестовый сценарий, 72, 74 in_list_02.sql, тестовый сценарий, 74 in_list_03.sql, тестовый сценарий, 76 in_list_10g.sql, тестовый сценарий, 76 index_ffe.sql, тестовый сценарий, 58, 60 inline, подсказка, 256, 257 Inner table, элемент трассировки 10053,375 intersect_Join.sql, тестовый сценарий, 293 io_cost, получение из cpu_cost, 47 ITL, 52 J join_card_01.sql, тестовый сценарий, 299, 308, 312, 337 join_card_01a.sql, тестовый сценарий, 303 join_card_02.sql, тестовый сценарий, 301 join_card_03.sql, тестовый сценарий, 301 join_card_04.sql, тестовый сценарий, 305, 309,312,317 join_card_05.sql, тестовый сценарий, 310 join_card_06.sql, тестовый сценарий, 312, 316 join_card_07.sql, тестовый сценарий, 317, 321 join_card_08.sql, тестовый сценарий, 321 join_card_09.sql, тестовый сценарий, 325 join_card_10.sql, тестовый сценарий, 332 join_cost_03.sql, тестовый сценарий, 348 join_cost_03a.sql, тестовый сценарий, 348 L lag(), аналитическая функция, 182 like_test.sql, тестовый сценарий, 80, 161 м materialize, подсказка, 256 пример, 62 тах(), запрос использование при табличном сканировании, 36 MBRC, 44 установка, 45, 46,47 merge_samples.sql, тестовый сценарий, 424, 427 method_opt, параметр, 71 mod(), функция, и порядок столбцов, 131 mreadtim установка, 45, 46 mreadtim, установка, 45 N nchar_types.sql, тестовый сценарий, 146 no_merge, подсказка, 248, 250, 251, 258, 263, 270, 271 no_sort.sql, тестовый сценарий, 422 no_unnest, подсказка, 240, 243, 251, 272, 276 not in использование со списками значений, 75 NOT NULL, ограничение и предикаты, основанные на ограничениях, 174 notin.sql, тестовый сценарий, 282 ntile(), аналитическая функция, 179, 183 NULL использование специального значения, 206 представление значений типа «дата», 152 О oddities.sql, тестовый сценарий, 75 OLTP-система, 185 и считывание переменных связывания, 185 opt_estimate, подсказка, 169 optimizer_dynamic_sampling, параметр, 175 значения в разных версиях Oracle, 506, 510 optimizer_dynamic_sampling, параметр курсора, 512 optimizer_features_enable, параметр значения в разных версиях Oracle, 506, 510
518 Алфавитный указатель optimizer_features_enable, параметр курсора, 511 optimizer_features_hinted, параметр значения в разных версиях Oracle, 510 optimizer_index_caching, параметр значения в разных версиях Oracle, 510 optimizer_index_caching, параметр курсора, 512 optimizer_index_cost_adj, параметр значения в разных версиях Oracle, 510 optimizer_index_cost_adj, параметр курсора, 512 optimizer_mode, параметр значения в разных версиях Oracle, 507, 510 установка режима оптимизатора, 24 optimizer_mode, параметр курсора, 512 optimizer_mode_hinted, параметр значения в разных версиях Oracle, 510 optimizer_percent_parallel, параметр, 56 OR использование с битовыми индексами, 227 ord_pred.sql, тестовый сценарий, 246 ordered, подсказка, 283, 284 ordered .sql, тестовый сценарий, 283 ordered_predicates, подсказка, 50, 246 при оценке стоимости ресурсов ЦП, 49 Outer table, элемент трассировки 10053, 375 over(), выражение, 179, 182 Р Р_А_Т, и разделяемые серверы, 403 parallel.sql, тестовый сценарий, 54 parallel_2.sql, тестовый сценарий, 54 parallel_ddl_forced_degree, параметр значения в разных версиях Oracle, 510 parallel_ddl_forced_instances, параметр значения в разных версиях Oracle, 510 parallel_ddl_mode, параметр значения в разных версиях Oracle, 510 parallel_ddl_mode, параметр курсора, 512 parallel_dml_forced_dop, параметр значения в разных версиях Oracle, 510 parallel_dml_mode, параметр значения в разных версиях Oracle, 510 parallel_dml_mode, параметр курсора, 512 parallel_execution_enabled, параметр значения в разных версиях Oracle, 510 parallel_execution_enabled, параметр курсора, 511 parallel_query_forced_dop, параметр значения в разных версиях Oracle, 511 parallel_query_mode, параметр значения в разных версиях Oracle, 510 parallel_query_mode, параметр курсора, 512 parallel_threads_per_cpu, параметр значения в разных версиях Oracle, 511 parallel_threads_per_cpu, параметр курсора, 512 Partition Exchange Loading, 65 partition.sql, тестовый сценарий, 61 patcpujhamess.sql, тестовый сценарий, 379, 418 pat_dump.sql, тестовый сценарий, 380, 417 pat_nocpu_hamess.sql, тестовый сценарий, 379, 418 PCTFREE, установка для блоков ASSM, 125 pga_aggregate_target, параметр значения в разных версиях Oracle, 511 pga_aggregate_target, параметр курсора, 512 PL/SQL, и параметр cursor_sharing, 186 plan_run81.sql, тестовый сценарий, 34 plan_run92.sql, тестовый сценарий, 34, 43 ppasses, элемент трассировки 10053, 376 prefetch._test.sql, тестовый сценарий, 343 prefetcb._test_01.sql, тестовый сценарий, 343 prefetch_test_02.sql, тестовый сценарий, 342, 346 probefrag, элемент трассировки 10053, 376 push_pred.sql, тестовый сценарий, 264 push_subq.sql, тестовый сценарий, 243 pv.sql, тестовый сценарий, 76 Q qb_name, подсказка, 454 query_rewrite_enabled, параметр значения в разных версиях Oracle, 507, 511 и переходные замкнутые выражения, 172 query_rewrite_enabled, параметр курсора, 512 query_rewrite_integrity, параметр значения в разных версиях Oracle, 511
Алфавитный указатель 519 query_rewrite_mtegrity, параметр курсора, 512 R RAC, уменьшение конкуренции при доступе к таблице, 128 ranges.sql, тестовый сценарий, 11, 83 ranges_02.sql, тестовый сценарий, 83 ranges_10g.sql, тестовый сценарий, 82 rebuild_test.sql, тестовый сценарий, 99 resc, элемент трассировки 10053, 375 resp, элемент трассировки 10053, 375 reverse.sql, тестовый сценарий, 122 reversed_ind.sql, тестовый сценарий, 123 rule, значение параметра optimizer_mode, 25 S sas_cpu_hamess.sql, тестовый сценарий, 418 sas_dump.sql, тестовый сценарий, 417 sas_nocpu_hamess.sql, тестовый сценарий, 418 selectivity_one.sql, тестовый сценарий, 79 semi_01.sql, тестовый сценарий, 275 set_column_stats, процедура, 194, 195 set_index_stats, процедура, 136 set_ops.sql, тестовый сценарий, 437, 440 set_system_stats.sql, тестовый сценарий, 42 set_xxx_stats(), процедуры, 195 short sort.sql, тестовый сценарий, 442 similar.sql, тестовый сценарий, 187 skip_unusable_indexes, параметр значения в разных версиях Oracle, 507, 511 skip_unusable_indexes, параметр курсора, 512 snap_myst.sql, тестовый сценарий, 394, 395 snap_ts.sql, тестовый сценарий, 394 sort (aggregate) в запросе, 38 sort_area_retained_size, параметр значения в разных версиях Oracle, 511 sort_area_retained_size, параметр курсора, 512 sort_area_size, параметр значения в разных версиях Oracle, 511 sort_area_size, параметр курсора, 512 sort_demo_01.sql, тестовый сценарий, 393, 404 sort_demo_01a.sql, тестовый сценарий, 397, 401, 404 sort_demo_01b.sql, тестовый сценарий, 406, 416 SQL Tuning Advisor, совет по использованию профиля, 168 SQL, преобразование в эквивалентные операторы, 29 sqlstat_enabled, параметр значения в разных версиях Oracle, 511 sreadtim, установка, 45 star_Join.sql, тестовый сценарий, 291 star_trans.sql, тестовый сценарий, 285 star_transformation_enabled, параметр, 289 значения в разных версиях Oracle, 511 star_transformation_enabled, параметр курсора, 512 statistics_level, параметр значения в разных версиях Oracle, 511 statisticsjevel, параметр курсора, 512 swap_join_inputs(), подсказка, 387 sys_op_countchg(), функции, 135 sysdate, псевдостолбец, 159, 160 sysdate_01.sql, тестовый сценарий, 159 т tl_i2_bad, индекс, 131 tablescan_01.sql, тестовый сценарий, 35 tablescan_01a.sql, тестовый сценарий, 39 tablescan_01b.sql, тестовый сценарий, 39 tablescan_02.sql, тестовый сценарий, 43 tablescan_03.sql, тестовый сценарий, 45 tablescan_04.sql, тестовый сценарий, 46 trans_close_01.sql, тестовый сценарий, 169 trans_close_02.sql, тестовый сценарий, 170, 172 trans_close_03.sql, тестовый сценарий, 171 treble_hash_auto.sql, тестовый сценарий, 386 two_predicate_01.sql, тестовый сценарий, 84 type_demo.sql, тестовый сценарий, 328 и unnest_cost_01.sql, тестовый сценарий, 270 unnest_cost_01a.sql, тестовый сценарий, 272, 273 unnest_cost_02.sql, тестовый сценарий, 271
520 Алфавитный указатель user_tab_histograms, представление, 69,182, 183, 184, 190, 191, 198, 204 появление пробелов, 200 V v$mystat, представление, 395 v$segstat, представление, 51 v$sessstat, представление, 395 v$sql_plan_statistics, представление, 245, 426 view_merge_01.sql, тестовый сценарий, 29, 30, 262 W workarea_size_policy, параметр значения в разных версиях Oracle, 511 workarea_size_policy, параметр курсора, 512 А автоматическое управление пространством сегментов, 124 автономный оптимизатор, 27 автотрассировка использование в случае коррелированных столбцов, 163 использование с гистограммами, 202 недостатки, 229 при оценке стоимости индексного доступа, 93 агрегирование, 432 анализ и оптимизация, 185 антисоединения, 268, 278, 280 аномалии, 281 и значения Null и Not In, 281 и подзапросы, 278 ошибки в версии 9i, 281 Б базовая формула оценки стоимости индексного доступа, 90 бессмысленные идентификаторы ошибки, связанные с типами данных, 150 битовые индексы и DML, 214 и индексы со структурой бинарного дерева, различия, 212, 213, 215, 217, 229, 230 и соединение типа «звезда», 291 комбинации, 219 битовые индексы {продолжение) невысокая кардинальность, 221 столбцы со значениями NULL, 225 компонент индекса, 215, 216 листовые блоки, 213 оценка стоимости ресурсов процессора, 228, 229, 230 построенные на нескольких столбцах, 231 распространенная ошибка, 222 расчет стоимости операций ввода-вывода, 230 стоимость, 216, 217, 234, 236 стоимость трансформаций, 234, 236 стратегия оценки стоимости, 216 табличный компонент, 217 трансформации, 233 битовые индексы соединения, 231, 232 блоки ASSM, 125 запроса, 454 при многоблочном чтении, 46 буферный кэш, проблемы, 51 быстрое полное индексное сканирование, 34, 58, 59 и табличное сканирование, 58 В ввод-вывод расчет стоимости для битовых индексов, 230 табличного сканирования, оценка стоимости операций, 44 веб-сайт автора, 19 с тестовыми примерами к книге, 19 веб-сайт Тома Кайта (AskTom), 235 внешние таблицы, 341 внутренние таблицы, 341 выборка из таблицы, упреждающая, 342 вынос подзапроса, 239, 255 использование в hist_intro.sql, 179 выражения, использующие sysdate, 160 вычисление влияние обновления версии Oracle, 73 кардинальности и стоимости, 167 селективности, 80, 81 вычисления, предположение о диапазонных предикатах, 80
Алфавитный указатель 521 Г гистограммы, 178, 183, 184 в 8i, 315 выбор количества групп, 314, 316 и переменные связывания, 185 и плотность, 205 и проблемы со значениями по умолчанию, 206, 208 и проблемы, связанные с типами данных, 203, 204, 205, 206 и распределенные запросы, 188 и соединения, 188 кумулятивные частотные, 184 решение проблем неверных типов данных, 149 сбалансированные по высоте, 183, 184, 198, 199 расчеты, 200 случаи их игнорирования, 187 соединений, 188 созданные по столбцам, 70 частотные, 184, 190, 191, 192 и пакет dbmsjstats, 315 и переменные связывания, 194 изменение, 194, 195 трудности с созданием, 208 группы в сценарии c_skew_ht_01.sql, 197 выбор количества при создании гистограммы, 314,316 и проблемы, связанные с типами данных, 203 расчеты, 200, 201 частотной гистограммы в 10g, 193 группы списков свободных блоков главный недостаток, 130 повторная балансировка, 129 создание таблицы, 128 д детерминистическая функция, 252 дизъюнкторы, влияние на ошибки списков значений, 73 динамическая выборка, 175 в случае коррелированных столбцов, 164 3 запросы обработка Перед оптимизацией, 31 параллельные, 54 запросы (продолжение) с битовым индексом стоимость, 229 трансформация, 238 значения по умолчанию, 206 проблем с помощью гистограмм, 206 И идентификатор записи расширенный, 119 идентификаторы бессмысленные ошибки, связанные с типами данных, 150 индексы важность столбцов, 140, 141 выбор порядка столбцов, 130 на базе функций, 149 по инвертированному ключу, 123 и сканирование диапазонов, 124 построенные на нескольких столбцах и битовые индексы, 231 со структурой бинарного дерева и битовые индексы, различия, 212, 213, 215, 217, 229, 230 и предикаты, сгенерированные из ограничений, 172 конвертация в битовый индекс, 233, 234 установка pctfree, 131 хорошие и плохие, 131 штраф за использование неправильного индекса, 133 кардинальность, 68 в 10.1.0.4 при выходе за границы диапазона, 158 в плане выполнения, 38 в примере bitmap_or.sql, 228 в примере discrete_02.sql, 155 в строке с индексом, 100,101 важность правильного расчета для плана выполнения, 181 вычисление, 167, 200, 201 для group by, 435 для операций над множествами, 439, 440 для селективностей индексов на основе В-дерева, 107 для соединений слияния, 425, 429
522 Алфавитный указатель кардинальность (продолжение) и комбинации битовых индексов, 221 и лидирующие нули, 151 и секционирование, 61, 63 и селективность, 68, 84, 491 и типы данных, 148, 149 изменения, 83 изменения для диапазонных предикатов, 77 нулевая, 193 округление, 100 ошибки, 79, 88 из-за маленьких списков значений, 74 планов выполнения проблемы со значениями по умолчанию, 153 различия оценок в версиях 9i и 10g, 166 соединения, 302, 305, 307 для трех таблиц, 321, 324 обработка значений null, 325 по диапазону, 308, 309 при пересечении диапазонов значений, 312, 313, 314 расчет, 299 с трансформацией типа «звезда», 290 столбцов, 155 ключи первичные и внешние, использование в гистограммах, 188 синтетические, ошибки, связанные с типами данных, 150 суррогатные, ошибки типов данных, 150 код оптимизатора, эволюция, 31 количество коллизий с ASSM и списками свободных блоков, 126 константы и переменные связывания, 62 и связанные переменные, 64 контроль ошибок в 10g, 305, 319 коэффициент селективности переменных связывания, 311 кумулятивная частотная гистограмма, 184 Л лидирующие нули и расчет кардинальности, 151 листовые блоки, 58 расщепление 50/50, 129 листовые блоки (продолжение) и быстрое полное индексное сканирование, 59 освобождение диапазона, 60 уменьшение конкуренции при доступе к таблице, 122 логические чтения и физические, 52 наименьшая стоимость, пример, 223 неограниченный закрытый диапазон, пример, 80 неограниченный открытый диапазон, пример, 80 неопределенные значения и селективность одной таблицы, 71 неравенства, 309 нулевая кардинальность, 193 О обновление версий Oracle, изменения временные таблицы, 504 выполнение параллельных запросов, 503 выход за границы диапазона, 501 группировка, 500 динамическая выборка, 503 индексирование значений null, 499 индексное соединение хэширования, 497 использование and_equal, 496 использование значений null в соединениях, 495 исправление ошибок при обработке входного списка, 497 конвертация индексов на основе В-дерева в битовые, 495 контроль ошибок, 500 неправильные типы данных, 501 округление, 493 оценка стоимости использования ресурсов процессора, 493 параметр optimizer_mode, 501 параметр pga_aggregate_target, 499 переходное замкнутое выражение, 498 подзапросы фильтрации, 503 поиск в индексе с пропусками, 495 расчет стоимости сортировки, 500 расчеты, связанные с sysdate, 498
Алфавитный указатель 523 обновление версий Oracle (продолжение) скалярные подзапросы, 503 слияние комплексных представлений, 502 статистика в словаре данных, 504 считывание значений переменных связывания, 494 уменьшение уровней вложенности запроса, 502 упорядоченные по убыванию индексы, 502 частотные гистограммы, 493 ограничение уровня таблицы, пример, 175 ограничения, 175 откладываемые, 174 отложенные, 112 округление символьных значений, 146 стоимости, 47 онлайн-хранилище кода, 19 операторы анализ стоимостным оптимизатором, 24 повторная оптимизация, 185 преобразование, 29 операции управляющие, 267 оптимизатор, 12, 24 влияние фактора кластеризации, 141 выбор плана выполнения, 30 дефекты, 41, 51 ошибки, 26 режимы, 24 сложность, 32 сравнение расчетов с оценкой человека, 192 стратегии стоимостной оптимизации, 33 оптимизация в автономном режиме, 168 и анализ, 185 откладываемые ограничения, 174 отложенные ограничения, 112 отметка максимального уровня заполнения и ASSM, 37 и списки свободных блоков, 120 оценка стоимости в 10g, 46 и преобразования, 29 процессорных ресурсов, 26 и битовые индексы, 228, 229, 230 ошибки в вычислениях в случае переменных связывания, 78 в кардинальности, 74, 79 ошибки (продолжение) в коррелированных столбцах, 164 генерируемые оптимизатором, 26 при использовании списка значений, 73 связанные с типами данных, 144, 147, 148, 150 п параллельное выполнение, 54 параллельные сканирования и чтения в режиме прямого доступа, 57 оценка стоимости, 57 параллельный запрос, 54 переменные связывания, 62 и гистограммы, 185 и диапазоны, 80 и селективность, 85 и частотные гистограммы, 194 подстановка вместо констант, 186 считывание значений, 82, 185 побочный эффект, 185 чрезмерное использование, 64 переходное замкнутое выражение, 169, 318 и селективность, 169 несогласованность, 171, 172 трюк на основе правил, 171 планы выполнения в dmbs_xplan 9.2.0.6, 63 выбор оптимизатором, 30 вынос подзапроса, 256 для bitmap_cost_04.sql, 225 для битовых индексов соединения, 232 для коррелированных столбцов, 163 для переходного замкнутого выражения, 170 для соединения с трансформацией типа «звезда», 287 для соединения типа «звезда», 292 для сценария dist_hist.sql, 189 для табличного сканирования, 36 и быстрое полное индексное сканирование, 58 и кардинальность соединения, 305 и трассировка 10053, 448, 459 и фильтрация, 304 кардинальность, 38 пример, 60
524 Алфавитный указатель планы выполнения (продолжение) сценарии для стандартизации вывода, 34 функциональность, 32 плотность, 184, 185, 196 и гистограммы, 205 подзапросы, 265 агрегирующие, 269 параметры, 267, 268 скалярные, 250 сокращающие, 356 типы, 269 подсказки /*+ cursor_sharing_exact */, 186 dynamic_sampling, 164, 165, 166 inline, 256, 257 materialize, 62, 256 no_merge, 248, 250, 251, 258, 263, 270, 271 no_unnest, 240, 243, 251, 272, 276 ordered, 283, 284 ordered_predicates, 49, 50, 246 qb_name, 454 swap_join_inputs(), 387 использование с индексами, 101 с полусоединениями, 276 с соединениями, 310 подставляемое представление, 180 полусоединения, 268, 276, 278 порядок предикатов, влияние на стоимость, 49 столбцов, влияние на фактор кластеризации, 130 правила селективности, 331 предикаты вычисление комбинаций предикатов, 84, 85 два предиката для одной таблицы, 83 вычисление селективности, 84 для соединения слияния, 426, 427 и селективность, 160, 161 на основе диапазонов селективность, 143 со странным неверным шаблоном, 156 ограничения и динамическая выборка, 175 проблемы с несколькими предикатами, 85 сгенерированные из ограничений, 173, 175 фильтрующие, 65 и секционирование, 65 представления v$mystat, 395 v$segstat, 51 v$sessstat, 395 v$sql_optimizer_env, 451, 511 v$sql_plan_statistics, 245, 426 агрегатные, 29, 263 создание результирующего набора данных, 262 подставляемые, 180 слияние, 262 приближение 80/20 при трансформации битовых индексов, 234 применение к битовым индексам, 218, 219, 220, 221 к битовым индексам соединения, 232 к индексам, построенным на нескольких столбцах, 231 производительность соединения хэширования, 374 профиль оптимизатора, 168 псевдонимы, использование в блоках запросов, 454 Р разделяемые серверы и Р_А_Т, 403 размеры блоков использование для настройки, 40 эффекты в 91, 39 распределенные запросы и гистограммы, 188 расширенный идентификатор записи, 119 С сбалансированная по высоте гистограмма, 183, 184 секционирование, 61 селективность, 68, 149 и значения типа «дата», 144 и индексы на базе функций, 162 и кардинальность, 491 и кардинальность соединения, 302, 305, 306, 307, 308 и кардинальность соединения по диапазону, 308, 309 и коррелированные столбцы, 162, 165 и лидирующие нули, 152
Алфавитный указатель 525 селективность (продолжение) и неверные типы данных, 147 и опасность использования дискретных значений, 154 и переходное замкнутое выражение, 169 и предикаты на основе диапазонов, 143 и предикаты, сгенерированные из ограничений, 173 и символьные значения, 144 и столбец sysdate, 159, 160 и числовые типы данных, 148 изменения в 10g, 76 объединенная, 86 одной таблицы, 68 и два предиката, 84 и диапазонные предикаты, 77, 80 и неопределенные значения, 71 и проблемы с несколькими предикатами, 86 и связанные переменные, 85 использование списков значений, 72 правила, 331 проблемы со значениями по умолчанию, 152 результаты использования функций, 161 сканирований по диапазону, 152 соединения, 303 при пересечении диапазонов значений, 313 расчет, 299 фиксированные проценты символьных выражений, 161 эффективная, 91,94, 95, 97, 102 символьные выражения фиксированные проценты селективности, 161 символьные значения, 144 синтетические ключи ошибки, связанные с типами данных, 150 сканирования по диапазону, 201 пример, 202 селективность, 152 слияние комплексных представлений в 8i, 31 словарь данных взлом, 137 проверка статистики, 71 событие 10032, 395 10033, 395, 397 событие (продолжение) 10046, 40, 395, 409 10053, 375, 395, 446, 507 10104, 363, 373 10132, установка вместе с событием 10053, 448 соединение слияния без первой сортировки, 427 варианты, 422 с декартовым произведением, 429, 430, 431, 444 для переходного замкнутого выражения, 170 значение для порядка соединения 9, 476 Порядок соединения 13, 482 порядок соединения 15, 484 порядок соединения 16, 484 порядок соединения 17, 485 порядок соединения 2, 466 пример, 292 соединение хэширования аномалии, 376 в несколько проходов, 367, 369 в один проход, 360, 362, 363 использование с трансформациями битовых индексов, 235 оптимальное, 358 производительность, 374 расчет размера записи, 357 соединения с трансформацией типа «звезда», 285, 291 сортировки/слияния стоимость, 422 типа «звезда», 291 сокращающий подзапрос, 356 сортировка буфера стоимость для переходного замкнутого выражения, 170 списки свободных блоков, 119 выбор, 121 достижение минимальной конкуренции, 127 и отметка максимального уровня заполнения, 120 создание таблицы, 128 управление, 120
526 Алфавитный указатель статистика без рабочей нагрузки, 45, 46 времени выполнения, 27 генерация для таблиц, 59 для столбца SYS_NC00005$, 162 из примера dependent.sql, 164 корректировка для индексов, 236 корректировка для фактора кластеризации, 136, 137, 140, 141 проверка в словаре данных, 71 сбор в 10g, 151 уровня секции, 63 проблемы, 65 стоимость, 24, 26, 28 ввода-вывода расчет для битовых индексов, 230 вычисление, 167 для различных выражений сортировки, 431 единицы измерения времени, 28 индексного доступа, базовая формула, 90 метод расчета метод произведения значений селективности, 351 метод хранимых результатов, 351 оценка в битовых индексах, 216, 217 соединения сортировки/слияния, 422 стоимость равна времени, доказательство, 27 столбцы в примере bitmap_cost_01.sql, 212 кардинальность, 155 коррелированные, 162 и динамическая выборка, 164, 165 селективность, 165 со значениями not null, 226 со значениями NULL, 225, 227 строка массива, превращение в запись таблицы, 233 суррогатные ключи — ошибки типов данных, 150 считывание значений переменных связывания, 82, 185 т таблицы внешние, 341 внутренние, 341 сжатие, 141 уменьшение конкуренции при доступе, 118, 120, 122, 124, 128, 129 управляющие, 341 тип данных «дата», селективность, 147, 148 типы данных кардинальность, 148 решение проблем при помощи гистограмм, 149 связанные с ними проблемы, 203 трассировка 10032, 406, 410 трассировка 10033, 401, 406, 411 трассировка 10046, 410 трассировка 10053, 372, 375, 446 блоки запроса, 454 включение, 446 для group by и сортировки, 433 для операции select distinct, 434 для операций над множествами, 440 для операций сравнения при сортировке, 418 для соединения слияния без первой сортировки, 427 запросы, 447 и 10104, 378, 384 и контроль ошибок, 458 и планы выполнения, 448 и системная статистика, 449 и соединения, 460, 463, 464 и стоимость сортировки, 412, 416 настройки параметров оптимизатора, 450 общие планы выполнения, 459 одиночные таблицы, 456 описание элементов соединения хэширования, 375 остановка, 446 параметры оптимизатора, 507 порядок соединения 1,459 10, 476 11,477 12, 480 13, 482 14, 483 15, 484 16, 484 17, 485 18, 487
Алфавитный указатель 527 трассировка 100532, порядок соединения (продолжение) 3,467 4,467 5,467 6, 471 7,471 8, 474 9, 476 при трансформации битовых индексов, 236 проверка правильности кардинальности, 310 результаты оценки соединений, 488 состав файла, 450, 454, 456 хранимая статистика, 454 трассировка 10104, 365, 370, 372, 373, 383, 385, 389 и 10053, 378, 384 состав файла, 373, 374 трассировка SQL-кода при работе с dbms_stats, 182 трассировочные файлы расширенные, 40 трюк на основе правил использование с переходным замкнутым выражением, 171 У управление пространством автоматическое (ASSM), 124 с использованием списков свободных блоков, 124 управляющие операции, 267 таблицы, 341 упреждающая выборка из таблицы, 342 Ф фактор кластеризации, 95, 115 влияние на оптимизатор, 141 фактор кластеризации (продолжение) вычисление, 137, 138, 139, 140 дефект производной, 132 и дополнительные столбцы, 133 и порядок столбцов, 130 и списки свободных блоков, 120 корректировка статистики, 136, 137, 140, 141 уменьшение конкуренции при доступе к таблице ASSM, 124 группы списков свободных блоков, 128, 129 индексы по инвертированному ключу, 122 несколько списков свободных блоков, 118, 120 физические чтения и логические, 52 фильтрация, 243, 303 оптимизация, 246, 247, 248, 251 фильтрующий предикат, 65 формула В. Брейтлинга, 90 функция детерминистическая, 252 X хэш-секция, 362 и соединение хэширования в один проход, 363, 365 проблемы при изменении размера, 382 ц циклическая ссылка на базу данных, 189 ч часто встречающееся значение, 199 пример, 202 частотная гистограмма, 184 чтение в режиме прямого доступа, 56 и параллельное сканирование, 57
Джонатан Льюис Oracle. Основы стоимостной оптимизации Перевели с английского В. Голубев, В. Щербинин Заведующий редакцией Руководитель проекта Научный редактор Литературный редактор Иллюстрации Художник Корректоры Верстка А. Кривцов П. Маннинен В. Щербинин Е. Бочкарева С. Романов Л. Аду веская А. Моносов, И. Смирнова Л. Харитонов Подписано в печать 25.10.06. Формат 70x100/16. Усл. п. л. 42,57. Тираж 3000. Заказ 3048 ООО «Питер Пресс», 198206, Санкт-Петербург, Петергофское шоссе, 73, лит. А29. Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 95 3005 — литература учебная. Отпечатано по технологии CtP в ОАО «Печатный двор» им. А. М. Горького. 197110, Санкт-Петербург, Чкаловский пр., 15.