Причины неэффективности SQL-запросов в Oracle. Оптимизация производительности SQL-запросов. Советы по оптимизации SQL запросов

Порыскав на досуге по тырнету, удивился, что специальных статей-руководств по оптимизации SQL-запросов нет. Перелистав различную информацию и книги, я постараюсь дать некоторое руководство к действию, которое поможет научиться писать правильные запросы.

  1. Оптимизация таблиц . Необходима, когда было произведено много изменений в таблице: либо удалена большая часть данных, либо много изменений со строками переменной длины - text, varchar, blob. Дело в том, что удалённые записи продолжают поддерживаться в индексном файле, и при последующей вставке новых записей используются позиции старых записей. Чтобы дефрагментировать файл с данными, используюется команда OPTIMIZE.

    OPTIMIZE TABLE `table1`, `table2`…

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

  2. Перестройка данных в таблице . После частых изменений в таблице, данная команда может повысить производительность работы с данными. Она перестраивает их в таблице и сортирует по определённому полю.

    ALTER TABLE `table1` ORDER BY `id`

    Тип данных . Лучше не индексировать поля, имеющие строковый тип, особенно поля типа TEXT. Для таблиц, данные которых часто изменяются, желательно избегать использования полей типа VARCHAR и BLOB, так как данный тип создаёт динамическую длину строки, тем самым увеличивая время доступа к данным. При этом советуют использовать поле VARCHAR вместо TEXT, так как с ним работа происходит быстрее.

    NOT NULL и поле по умолчанию . Лучше всего помечать поля как NOT NULL, так как они немного экономят место и исключают лишние проверки. При этом стоит задавать значение полей по умолчанию и новые данные вставлять только в том случае, если они от него отличаются. Это ускорит добавление данных и снизит время на анализ таблиц. И стоит помнить, что типы полей BLOB и TEXT не могут содержать значения по умолчанию.

    Постоянное соединение с сервером БД . Позволяет избежать потерь времени на повторное соединение. Однако стоит помнить, что у сервера может быть ограничение на количество соединений, и в том случае, если посещаемость сайта очень высокая, то постоянное соединение может сыграть злую шутку.

    Разделение данных. Длинные не ключевые поля советуют выделить в отдельную таблицу в том случае, если по исходной таблице происходит постоянная выборка данных и которая часто изменяется. Данный метод позволит сократить размер изменяемой части таблицы, что приведёт к сокращению поиска информации.
    Особенно это актуально в тех случаях, когда часть информации в таблице предназначена только для чтения, а другая часть - не только для чтения, но и для модификации (не забываем, что при записи информации блокируется вся таблица). Яркий пример - счётчик посещений.
    Есть таблица (имя first) с полями id, content, shows. Первое ключевое с auto_increment, второе - текстовое, а третье числовое - считает количество показов. Каждый раз загружая страницу, к последнему полю прибавляется +1. Отделим последнее поле во вторую таблицу. Итак, первая таблица (first) будет с полями id, content, а вторая (second) с полями shows и first_id. Первое поле понятно, второе думаю тоже - отсыл к ключевому полю id из первой таблицы.
    Теперь постоянные обновления будут происходить во второй таблице. При этом изменять количество посещений лучше не программно, а через запрос:

    А выборка будет происходить усложнённым запросом, но одним, двух не нужно:

    SELECT first.id, first.content, second.first_id, second.shows FROM second INNER JOIN first ON (first.id = second.first_id)

    Стоит помнить, что всё это не актуально для сайтов с малой посещаемостью и малым количеством информации.

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

    Требовать меньше данных . При возможности избегать запросов типа:

    SELECT * FROM `table1`

    Запрос не эффективен, так как скорее всего возвращает больше данных, чем необходимо для работы. Вариантом лучше будет конструкция:

    SELECT id, name FROM table1 ORDER BY id LIMIT 25

    Тут же сделаю добавление о желательности использования LIMIT. Данная команда ограничивает количество строк, возвращаемых запросом. То есть запрос становится "легче" и производительнее.
    Если стоит LIMIT 10, то после получения десяти строк запрос прерывается.
    Если в запросе применяется сортировка ORDER BY, то она происходит не по всей таблице, а только по выборке.
    Если использовать LIMIT совместно с DISTINCT, то запрос прервётся после того, как будет найдено указанное количество уникальных строк.
    Если использовать LIMIT 0, то возвращено будет пустое значение (иногда нужно для определения типа поля или просто проверки работы запроса).

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

    Ограничить использование SELECT для постоянно изменяющихся таблиц .

  3. Не забывайте про временные таблицы типа HEAP . Несмотря на то, что таблица имеет ограничения, в ней удобно хранить промежуточные данные, особенно когда требуется сделать ещё одну выборку из таблицы без повторного обращения. Дело в том, что эта таблица хранится в памяти и поэтому доступ к ней очень быстрый.
  4. Поиск по шаблону . Зависит от размера поля и если уменьшить размер с 400 байтов до 300, то время поиска сократиться на 25%.

Когда пользователь начинает операцию по извлечению данных, SQL-оператор этого пользователя проходит несколько последовательных этапов, которые все вместе называются обработкой запроса. Одно из главных преимуществ языка SQL состоит в том, что он не является процедурным, и потому в нем не нужно перечислять шаги, которые должны выполняться для достижения поставленной перед оператором цели. Другими словами, в SQL не нужно описывать, как что-то должно быть сделано, вместо этого в нем достаточно описывать только то, что требуется получить от базы данных.

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

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

Синтаксический анализ SQL-запросов

Этап синтаксического анализа (parsing) главным образом состоит в выполнении проверки синтаксиса и семантики SQL-операторов. В конце этого этапа создается дерево синтаксического разбора (parse tree), отражающее структуру запроса.

В частности, во время этого этапа SQL-оператор преобразуется в запрос реляционной алгебры, который подвергается анализу для выяснения того, является ли он корректным с синтаксической точки зрения. Далее этот запрос подвергается проверке на предмет корректности с семантической точки зрения, во время которой с помощью словаря данных проверяется, чтобы все упоминаемые в запросе таблицы и отдельные столбцы, равно как и все необходимые объектные привилегии, действительно существовали. Вдобавок проверяются типы столбцов для получения уверенности в том, что данные соответствуют определениям столбцов. Потом оператор нормализуется для того, чтобы его можно было обработать более эффективным образом. В случае если запрос сформулирован неправильно, он отклоняется. После того, как дерево синтаксического разбора проходит все синтаксические и семантические проверки, оно признается действительным и отправляется на этап генерации логического плана запроса. Все эти операции происходят в области SGA, представляющей библиотечный кэш части.

Оптимизация SQL запросов

На этапе оптимизации Oracle применяет свой оптимизатор, который называется оптимизатором по стоимости (Cost-Base Optimizer - CBO), для выбора наилучшего метода доступа для извлечения данных из присутствующих в запросе таблиц и индексов. За счет использования предоставляемых статистических данных и любых указываемых в SQL-запросах подсказок, CBO генерирует для SQL-оператора оптимальный план выполнения.

В общем случае этап оптимизации можно поделить на два отдельных подэтапа: перезапись запроса и генерация физического плана выполнения запроса. Давайте рассмотрим эти два отдельных подэтапа оптимизации более подробно.

Этап перезаписи запроса

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

Этап генерации плана выполнения

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

В общем, при генерации физического плана или плана выполнения запроса оптимизатор принимает во внимание следующие факторы:

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

Оптимизатор может генерировать несколько действительных физических планов запроса, которые являются потенциальными планами выполнения. Затем оптимизатор делает выбор между ними путем оценки стоимости каждого возможного физического плана на основании доступных ему статистических данных по таблицам и индексам и выбора того плана, подсчитанная стоимость которого оказывается наименьшей. Этот процесс оценки стоимости возможных физических планов запроса называется оптимизацией запроса по стоимости (cost-based optimization). Стоимость выполнения плана напрямую зависит от того, сколько ресурсов (ввода-вывода, памяти и ЦП) для него требуется. Потом оптимизатор передает выбранный самый низкий по стоимости физический план запроса механизму выполнения запросов Oracle. В следующем разделе рассматривается простой пример, чтобы можно было лучше разобраться в том, что собой представляет процесс оптимизации процесса по стоимости.

Пример оптимизации запроса по стоимости

Давайте предположим, что требуется выполнить показанный ниже запрос, предусматривающий поиск информации обо всех руководителях (supervisor), которые работают в Далласе (Dallas):

SQL> SELECT * FROM employee e, dept d WHERE e.dept_no = d.dept_no AND(e.job = "SUPERVISOR" AND d.city = "DALLAS"); SQL>

Получить список руководителей можно тремя способами. Давайте рассмотрим три этих способа и вычислим стоимость получения результатов в случае применения каждого из них.

Для произведения вычислений по стоимости давайте исходить из следующих предположений:

  • считывать и записывать данные можно только по одной строке за раз (в реальности операции ввода-вывода выполняются обычно на уровне блоков, а не на уровне строк);
  • база данных записывает каждый промежуточный шаг на диск (опять-таки, в реальном мире такого может и не быть);
  • с таблицами не ассоциированы никакие индексы;
  • в таблице employee содержится 2000 строк;
  • в таблице dept содержится 40 строк и руководителей тоже 40 (по одному на каждое отделение);
  • в Далласе всего функционирует десять отделений.

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

Запрос 1: декартово соединение

В случае применения этого запроса сначала получается декартово произведение таблиц employee и dept , а затем проверяться, какие из строк в нем удовлетворяют требованию:

WHERE e.job=supervisor AND d.dept=operations AND e.dept_no=d.dept_no

  • декартово произведение таблиц employee и dept потребует считывания обеих таблиц, т.е. 2000 + 40 = 2040 операций чтения;
  • создание декартова произведения - 2000 * 40 = 80000 операций записи;
  • считывание результата декартова произведения для его сравнения с условием выбора строк - 2000 * 40 = 80000 операций чтения;
  • итого общая стоимость ввода-вывода составит: 2040 + 80000 + 80000 = 162040.

Запрос 2: соединение двух таблиц

Второй запрос подразумевает выполнение соединения таблиц employee и dept . В случае применения этого запроса сначала будет осуществляться соединение таблиц employee и dept по столбцу dept_no , а затем выборка из результатов этого соединения всех строк, которые удовлетворяют условию:

WHERE e.job=supervisor and city=Dallas

Общая стоимость выполнения этого запроса будет выглядеть так:

  • соединение таблиц employee и dep сначала потребует считывания всех строк из обеих таблиц, т.е. 2000 + 40 = 2.040 операций чтения;
  • создание соединения таблиц employee и dep - 2000 операций записи;
  • считывание результатов соединения будет стоить 2000 операций чтения;
  • итого общая стоимость ввода-вывода составит: 2040 + 2000 + 2000 = 6040.

Запрос 3: соединение сокращенных связей

Третий запрос тоже подразумевает выполнение соединения таблиц employee и dept , но с соединением не всех, а только выборочных строк из этих двух таблиц. В случае его применения необходимые данные будут извлекаться так, как описано далее. Сначала будет осуществляться считывание таблицы employee для получения всех строк со значением SUPERVISOR . Затем будет выполняться считывание таблицы dept для извлечения всех строк со значением DALLAS . И, наконец, напоследок будет осуществляться соединение тех строк, которые были извлечены из таблиц employee и dept .

Общая стоимость выполнения этого запроса будет выглядеть так:

  • считывание таблицы employee для извлечения строк со значением SUPERVISOR будет стоить 2000 операций чтения;
  • запись строк со значением SUPERVISOR , которые были извлечены на предыдущем шаге - 40 операций записи;
  • считывание таблицы dept для извлечения всех строк со значением DALLAS - 40 операций чтения;
  • запись строк со значением DALLAS , извлеченных на предыдущем шаге - 10 операций записи;
  • соединение строк со значением SUPERVISOR и со значением DALLAS , извлеченных на предыдущих шагах выполнения данного запроса - всего 40 + 10 = 50 операций записи;
  • считывание результата соединения, полученного на предыдущем шаге - 50 операций чтения;
  • итого всего стоимость ввода-вывода составит: 2000 + 2 (40) + 10 + 2 (50) = 2190.

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

Эвристические стратегии для обработки запросов

Применение методики оптимизации по стоимости не является единственным способом выполнения оптимизации запросов. Для обработки запросов в базе данных могут также применяться и менее систематичные методики, известные как эвристические стратегии (heuristic strategies). Операция соединения является бинарной, а операция вроде выбора - унарной. Успешная стратегия в целом заключается в выполнении унарной операции на раннем этапе, чтобы в более сложных и длительных по времени бинарных операциях далее использовались меньшие операнды. Выполнение в первую очередь как можно большего количества унарных операций сокращает источники строк в операциях соединения. Ниже перечислены некоторые наиболее типичные эвристические стратегии по обработке запросов.

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

Выполнение запросов

На последнем этапе процесса обработки запросов осуществляется выполнение оптимизированного запроса (физического плана запроса, который был выбран). Если он представляет собой оператор SELECT , тогда производится возврат соответствующих строк пользователю, а если оператор INSERT, UPDATE или DELETE , тогда - внесение в строки соответствующих изменений. Исполняющий механизм SQL берет план выполнения, полученный на этапе оптимизации, и выполняет его.

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

Пять простых оптимизаций, которые можно реализовать на основе одних лишь метаданных (т. е. ограничений) и самого запроса Предлагаем вам адаптацию статьи Лукаса Эдера, рассчитанную на тех, кто имеет общее представление о базах данных и SQL, а также небольшой практический опыт работы с СУБД. Стоимостная оптимизация – фактически стандартный способ оптимизации SQL-запросов в современных базах данных. Именно поэтому настолько сложно написать вручную сложный алгоритм на 3GL (языках программирования третьего поколения) , производительность которого превышала бы динамически рассчитываемый план выполнения, сгенерированный современным оптимизатором. Сегодня мы не будем обсуждать стоимостную оптимизацию, то есть оптимизацию на основе стоимостной модели базы данных. Мы рассмотрим гораздо более простые оптимизации. Те,которые можно реализовать на основе одних лишь метаданных (т. е. ограничений) и самого запроса. Обычно их реализация для базы данных – не бином Ньютона, поскольку, в данном случае, любая оптимизация приведет к лучшему плану выполнения, независимо от наличия индексов, объемов данных и асимметрии распределения данных. "Не бином Ньютона" не в смысле легкости реализации оптимизации, а в том, следует ли это делать. Эти оптимизации устраняют [для базы данных] ненужную, дополнительную работу ().

Для чего эти оптимизации применяются?

Большинство из них применяется для:
  • исправления ошибок в запросах;
  • обеспечения повторного использования представлений без фактического выполнения логики представления базой данных.
В первом случае, можно было бы заявить: "Ну и что, просто возьми, и исправь этот дурацкий SQL-запрос". Но пусть первым бросит в меня камень тот, кому не доводилось ошибаться. Второй случай особенно интересен: это дает нам возможность создания сложных библиотек представлений и табличных функций, допускающих многократное использование в нескольких слоях.

Используемые базы данных

В этой статье мы будет сравнивать 10 SQL-оптимизаций в пяти наиболее широко используемых СУБД (согласно рейтингу баз данных):
  • Oracle 12.2;
  • MySQL 8.0.2;
  • SQL Server 2014;
  • PostgreSQL 9.6;
  • DB2 LUW 10.5.
Другой почти вторит ему. Как обычно, в этой статье я буду выполнять запросы к базе данных Sakila. Вот список этих десяти разновидностей оптимизаций:
  1. транзитивное замыкание;
  2. невозможные предикаты и ненужные обращения к таблицам;
  3. устранение JOIN;
  4. устранение "бессмысленных" предикатов;
  5. проекции в подзапросах EXISTS;
  6. cлияние предикатов;
  7. доказуемо пустые множества;
  8. oграничения CHECK;
  9. ненужные рефлексивные соединения;
  10. Pushdown предикатов
Сегодня мы обсудим пп. 1-3, во второй части - 4 и 5, а в части 3 – 6-10.

1. Транзитивное замыкание

Начнем с чего-нибудь попроще: транзитивного замыкания . Это тривиальное понятие, применимое ко множеству математических операций, например, оператору равенства. Его можно сформулировать в этом случае следующим образом: если A = B и B = C, то A = C.

Несложно, правда? Но это влечет некоторые интересные последствия для оптимизаторов SQL. Рассмотрим пример. Извлечем все фильмы с ACTOR_ID = 1: SELECT first_name, last_name, film_id FROM actor a JOIN film_actor fa ON a. actor_id = fa. actor_id WHERE a. actor_id = 1 ; Результат следующий: FIRST_NAME LAST_NAME FILM_ID PENELOPE GUINESS 1 PENELOPE GUINESS 23 PENELOPE GUINESS 25 PENELOPE GUINESS 106 PENELOPE GUINESS 140 PENELOPE GUINESS 166 . . . Взглянем теперь на план выполнения этого запроса в случае СУБД Oracle: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- | Id | Operation | Name | Rows | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- | 0 | SELECT STATEMENT | | | | 1 | NESTED LOOPS | | 19 | | 2 | TABLE ACCESS BY INDEX ROWID| ACTOR | 1 | | * 3 | INDEX UNIQUE SCAN | PK_ACTOR | 1 | | * 4 | INDEX RANGE SCAN | PK_FILM_ACTOR | 19 | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- Predicate Information (identified by operation id) : -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - 3 - access ("A" . "ACTOR_ID" = 1 ) 4 - access ("FA" . "ACTOR_ID" = 1 ) Особенно тут интересен раздел предикатов. Предикат ACTOR_ID = 1, вследствие транзитивного замыкания применяется как к таблице ACTOR, так и таблице FILM_ACTOR. Если: A. ACTOR_ID = 1 (из предиката WHERE) и… A. ACTOR_ID = FA. ACTOR_ID (из предиката ON) То: FA. ACTOR_ID = 1 В случае более сложных запросов это приводит к некоторым весьма приятным результатам. В частности, точность оценок кардинальности существенно повышается, так как появляется возможность подбора оценок на основе конкретного константного значения предиката, а не, например, среднего числа фильмов по актерам, как в следующем запросе (возвращающем такой же результат): SELECT first_name, last_name, film_id FROM actor a JOIN film_actor fa ON a. actor_id = fa. actor_id WHERE first_name = "PENELOPE" AND last_name = "GUINESS" Его план: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- | Id | Operation | Name | Rows | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- | 0 | SELECT STATEMENT | | | | 1 | NESTED LOOPS | | 2 | | * 2 | TABLE ACCESS BY INDEX ROWID BATCHED| ACTOR | 1 | | * 3 | INDEX RANGE SCAN | IDX_ACTOR_LAST_NAME | 3 | | * 4 | INDEX RANGE SCAN | PK_FILM_ACTOR | 27 | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- Predicate Information (identified by operation id) : -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - 2 - filter ("A" . "FIRST_NAME" = "PENELOPE" ) 3 - access ("A" . "LAST_NAME" = "GUINESS" ) 4 - access ("A" . "ACTOR_ID" = "FA" . "ACTOR_ID" ) Как вы можете видеть, оценка числа строк таблицы FILM_ACTOR завышена, а оценка для вложенных циклов (NESTED LOOP) занижена. Вот пару интересных значений: SELECT count (* ) FROM film_actor WHERE actor_id = 1 ; SELECT avg (c) FROM ( SELECT count (* ) c FROM film_actor GROUP BY actor_id ) ; Результат: 19 27.315 Отсюда и получаются оценки. Если база данных знает, что речь идет о ACTOR_ID = 1, то может собрать статистику по количеству фильмов для этого конкретного актёра . Если же не знает (поскольку стандартный механизм сбора статистики не соотносит FIRST_NAME/LAST_NAME с ACTOR_ID), то мы получим среднее число фильмов для всех актеров . Простая, несущественная ошибка в данном конкретном случае, но в сложном запросе она может распространяться дальше, накапливаться и приводить дальше в запросе (выше в плане) к неправильному выбору JOIN. Так что всегда, когда только можете, проектируйте свои соединения и простые предикаты так, что воспользоваться преимуществами транзитивного замыкания. Какие еще базы данных поддерживают эту возможность?

DB2

Да! Explain Plan -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - ID | Operation | Rows | Cost 1 | RETURN | | 13 2 | NLJOIN | 27 of 1 | 13 3 | FETCH ACTOR | 1 of 1 (100.00 % ) | 6 4 | IXSCAN PK_ACTOR | 1 of 200 ( .50 % ) | 0 5 | IXSCAN PK_FILM_ACTOR | 27 of 5462 ( .49 % ) | 6 Predicate Information 4 - START (Q2. ACTOR_ID = 1 ) STOP (Q2. ACTOR_ID = 1 ) 5 - START (1 = Q1. ACTOR_ID) STOP (1 = Q1. ACTOR_ID) Кстати, если вам нравятся крутые планы выполнения вроде этого, воспользуйтесь сценарием Маркуса Винанда (Markus Winand) .

MySQL

К сожалению, планы выполнения MySQL плохо подходят для подобного анализа. В выводимой информации отсутствует сам предикат: ID SELECT TYPE TABLE TYPE REF ROWS -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 1 SIMPLE a const const 1 1 SIMPLE fa ref const 19 Но тот факт, что в столбце REF два раза указано const показывает, что в обеих таблицах идет поиск по константному значению. В то же время, план запроса с FIRST_NAME / LAST_NAME выглядит следующим образом: ID SELECT TYPE TABLE TYPE REF ROWS -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - 1 SIMPLE a ref const 3 1 SIMPLE fa ref a. actor_id 27 И, как вы можете видеть, в REF теперь указана ссылка на столбец из предиката JOIN. Оценка кардинальности практически такая же, как в Oracle. Так что да, MySQL тоже поддерживает транзитивное замыкание.

PostgreSQL

Да! QUERY PLAN -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- Nested Loop (cost= 4.49 . .40 .24 rows= 27 width= 15 ) - > Seq Scan on actor a (cost= 0.00 . .4 .50 rows= 1 width= 17 ) Filter: (actor_id = 1 ) - > Bitmap Heap Scan on film_actor fa (cost= 4.49 . .35 .47 rows= 27 width= 4 ) Recheck Cond: (actor_id = 1 ) - > Bitmap Index Scan on film_actor_pkey (cost= 0.00 . .4 .48 rows= 27 width= 0 ) Index Cond: (actor_id = 1 )

SQL Server

Да! | -- Nested Loops (Inner Join) | -- Nested Loops (Inner Join) | | -- Index Seek (SEEK: ([ a] . [ actor_id] = (1 ) ) ) | | -- RID Lookup | -- Index Seek (SEEK: ([ fa] . [ actor_id] = (1 ) ) )

Резюме

Все наши базы данных поддерживают транзитивное замыкание.
База данных Транзитивное замыкание
DB2 LUW 10.5 Да
MySQL 8.0.2 Да
Oracle 12.2.0.1 Да
PostgreSQL 9.6 Да
SQL Server 2014 Да
Однако дождитесь №6 в следующей части статьи. Существуют сложные случаи транзитивного замыкания, с которыми справляются не все базы данных.

2. Невозможные предикаты и ненужные обращения к таблицам

Эта совсем дурацкая оптимизация, но почему бы и нет? Если пользователи пишут невозможные предикаты, то зачем их вообще выполнять? Вот несколько примеров: -- "Очевидный" SELECT * FROM actor WHERE 1 = 0 -- "Хитрый" SELECT * FROM actor WHERE NULL = NULL Первый запрос, очевидно, никогда не вернет никаких результатов, но то же самое утверждение справедливо и относительно второго. Ведь хотя NULL IS NULL всегда TRUE, результат вычисления NULL = NULL равен NULL, что, согласно трёхзначной логике , эквивалентно FALSE. Это не требует особых пояснений, так что перейдем сразу к выяснению, какие из баз данных выполняют такую оптимизацию.

DB2

Да! Explain Plan -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - ID | Operation | Rows | Cost 1 | RETURN | | 0 2 | TBSCAN GENROW | 0 of 0 | 0 Как вы можете видеть, обращение к таблице ACTOR полностью исключено из плана. В нём присутствует только операция GENROW, генерирующая ноль строк. Идеально.

MySQL

Да! ID SELECT TYPE TABLE EXTRAS -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - 1 SIMPLE Impossible WHERE На этот раз, MySQL был столь любезен, что сообщил нам о невозможном предложении WHERE. Спасибо! Это сильно облегчает анализ, особенно по сравнению с другими базами данных.

Oracle

Да! -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | Id | Operation | Name | Starts | E- Rows | A- Rows | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | 0 | SELECT STATEMENT | | 1 | | 0 | | * 1 | FILTER | | 1 | | 0 | | 2 | TABLE ACCESS FULL| ACTOR | 0 | 200 | 0 | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - Predicate Information (identified by operation id) : -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - 1 - filter ( NULL IS NOT NULL) Видим, что в плане по-прежнему упоминается обращение к таблице ACTOR, причем ожидаемое число строк по-прежнему 200, но присутствует и операция фильтрации (FILTER) при Id=1, где никогда не будет TRUE. В силу нелюбви Oracle к стандартному булевому типу данных SQL , Oracle отображает в плане NULL IS NOT NULL, вместо простого FALSE. Ну что ж... Но если серьезно, следите за этим предикатом. Мне случалось отлаживать планы выполнения с поддеревьями в 1000 строк и крайне высокими значениями стоимости и лишь постфактум обнаруживать, что всё это поддерево "отсекалось" фильтром NULL IS NOT NULL. Немного обескураживающе, скажу я вам.

PostgreSQL

Да! QUERY PLAN -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - Result ( cost= 0.00 . .0 .00 rows= 0 width= 228 ) One- Time Filter: false Уже лучше. Никакого надоедливого обращения к таблице ACTOR и маленький аккуратный предикат FALSE.

SQL Server?

Да! | -- Constant Scan SQL Server называет это "константным просмотром", то есть просмотром, при котором ничего не происходит – аналогично DB2. Все наши базы данных умеют исключать невозможные предикаты:

3. Устранение JOIN

В предыдущем разделе мы наблюдали ненужные обращения к таблицам в однотабличных запросах. Но что произойдет, если в JOIN не требуется одно из нескольких обращений к таблицам? Я уже писал про устранение JOIN в предыдущем посте из моего блога . SQL-движок способен определить, на основе вида запроса, а также наличия первичных и внешних ключей, действительно ли конкретный JOIN необходим в данном запросе, или его устранение не повлияет на семантику запроса. Во всех следующих трёх примерах, JOIN не нужен. Внутреннее соединение типа "...-к-одному" можно устранить при наличии не допускающего неопределенного значения (NOT NULL) внешнего ключа Вместо вот этого: SELECT first_name, last_name FROM customer c JOIN address a ON c. address_id = a. address_id База данных может выполнить следующее: SELECT first_name, last_name FROM customer c Внутреннее соединение (INNER JOIN) типа "...-к-одному" можно заменить при наличии допускающего неопределенного значения внешнего ключа. Вышеприведенный запрос работает, если на внешний ключ наложено ограничение NOT NULL. Если же нет, например, как в этом запросе: SELECT title FROM film f JOIN language l ON f. original_language_id = l. language_id то JOIN все равно можно устранить, но придется добавить предикат NOT NULL, вот так: SELECT title FROM film WHERE original_language_id IS NOT NULL Внешнее соединение (OUTER JOIN) типа "...-к-одному" можно убрать при наличии уникального ключа. Вместо вот этого: SELECT first_name, last_name FROM customer c LEFT JOIN address a ON c. address_id = a. address_id База данных, опять же, может выполнить следующее: SELECT first_name, last_name FROM customer c ... даже если внешнего ключа по CUSTOMER.ADDRESS_ID нет. Уникальное внешнее соединение (DISTINCT OUTER JOIN) типа "...-ко-многим" можно убрать. Вместо вот этого: SELECT DISTINCT first_name, last_name FROM actor a LEFT JOIN film_actor fa ON a. actor_id = fa. actor_id База данных может выполнить следующее: SELECT DISTINCT first_name, last_name FROM actor a Все эти примеры были подробно изучены в предыдущей статье, так что я не буду повторяться, а лишь подытожу всё, что могут устранять различные базы данных:
База данных INNER JOIN: ...-к-одному (может быть NULL): ...-к-одному OUTER JOIN: ...-к-одному OUTER JOIN DISTINCT: ...-ко-многим
DB2 LUW 10.5 Да Да Да Да
MySQL 8.0.2 Нет Нет Нет Нет
Oracle 12.2.0.1 Да Да Да Нет
PostgreSQL 9.6 Нет Нет Да Нет
SQL Server 2014 Да Нет Да Да
К сожалению, не все базы данных могут устранять все виды соединений. DB2 и SQL Server тут – безусловные лидеры!

После написания программы и появления «живых» данных выясняется, что реакция программы на тестовые наборы, порой сильно отличается от работы с реальными данными. Программисты обычно мало внимания уделяют формированию тестовых наборов данных, что является серьезной ошибкой. Ставка делается на то, что используются современные «крутые» СУБД, которые сами себя настраивают. К сожалению это не совсем так, и работе с базой данных следует уделять пристальное внимание. В идеале, за обработку бизнес логики должны отвечать специалисты. Но и рядовым программистам полезно иметь навыки и знания по архитектуре СУБД и написанию SQL запросов.

Нередки случаи, когда используются генераторы скриптов и программного кода для доступа к данным. Авторы программ надеются на то, что современные технологические новинки сами выполнят за вас всю работу. В результате нередки случаи, когда несколько месяцев спустя после внедрения программы, пользователи начинают жаловаться, что программа «еле шевелится». Начинается всеобщая паника с привлечением дорогостоящих специалистов, которые смогут найти “бутылочное горлышко” тормозящее программу и спасти проект.

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

Существует ряд программ позволяющих автоматизировать и упростить эту задачу. Данный материал ориентирован на работу с сервером Oracle , но и для других баз данных есть аналогичные средства анализа и оптимизации «тюнинга». Первым нашим помощником станет программа мониторинга работы сервера Oracle с названием « Spotlight on Oracle » фирмы Quest software (http://www.quest.com). Это очень мощный инструмент, предназначенный для контроля функционирования вашего сервера. Данная программа выполнена в необычной цветовой палитре, что резко выделяет ее от других продуктов. После запуска данной программы необходимо создать учетную запись пользователя для чего потребуется учетная запись SYS или запись с системными привилегиями DBA. Помощник создания новой учетной записи вызывается из меню “ File > User Wizard ”.

После создания учетной записи пользователя и соединением с сервером Oracle нам представляется визуальная картинка, которая отображает компоненты и процессы работы сервера. Если один, или несколько компонентов сервера работает не оптимально или с перегрузкой, то его цвет изменяется от зеленого до красного, в зависимости от степени перегрузки. Возможен мониторинг сразу нескольких серверов, список которых отображается в левой панели и так же меняет цвет. Иконка в панели задач также меняет цвет синхронно с программой, что позволяет оперативно реагировать при “свернутом” в приложении. Пример мониторинга показан на рисунке 1.

Очень полезной особенностью данной программы является система авто-рекомендаций решения проблем. Достаточно кликнуть мышкой по красному участку изображения, чтобы получить развернутое описание проблемы и возможные методы ее устранения. Если же все нормально, то благодаря данной программе можно подстроить параметры запуска сервера для уменьшения используемых им системных ресурсов. Например, по рисунку 1 можно сделать вывод, что размер табличного пространства файла базы данных можно смело уменьшить в два раза, и желательно выделить дополнительную память под “ Shared Pool ”.

Но это все проблемы администратора базы данных. Разработчиков же больше волнует, как работают их творения и сколько ресурсов «кушают» запросы к базе данных. Для этого вызываем пункт меню “ Navigator > Top Sessions ”. После заполнения параметров фильтра отбора данных нам будет показан список текущих запросов к серверу базы данных. Предварительно отсортировав запросы по требованиям к ресурсам, можно выделить самые “прожорливые”. В этом же окне можно посмотреть план выполнения запроса, пример которого показан на рисунке 2. Причем план запросов можно представить в виде графа, дерева или словесного описания. Здесь так же используется цветовая маркировка проблемных участков.

После выявления проблемных SQL запросов настал черед их оптимизации. Для автоматизации этого процесса воспользуемся программой SQL Expert фирмы LECCO (http://www.leccotech.com). Вызываем окно SQL редактора и добавляем в него скрипт запроса. Здесь так же можно посмотреть план выполнения запроса. Но нас больше всего интересует функция меню “SQL-> Optimize ”, которая генерирует список альтернативных вариантов построения заданного SQL скрипта. А функция “SQL-> Butch Run ” позволяет проанализировать время выполнения всех запросов на “живых” данных и вывести результирующую таблицу, которую можно отсортировать по требуемому параметру. Выбрав наиболее оптимальный запрос, его можно сравнить с оригиналом и принять решение о возможности дальнейшего его использования в своем приложении. Пример работы по оптимизации запроса показан на рисунке 3.

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

Оптимизация неэффективного SQL, несомненно, важнейшее средство повышения производительности в арсенале аналитика по производи­тельности Oracle. Если вызов базы данных порождает более десятка вызовов LIO для каждой строки, возвращаемой из каждой таблицы фразы FROM соответствующей команды SQL, то, скорее всего, произво­дительность такой команды можно повысить. Например, операция со­единения трех таблиц, возвращающая две строки, вероятно, должна требовать не более 60 вызовов LIO.

Любая относительная оценка при определенных условиях мо­жет оказаться неверной. Одним из таких условий является фор­мирование результата запроса как итога агрегирования. Напри­мер, запрос, возвращающий сумму (одну строку) для таблицы из миллиона строк, вполне законно может требовать выполне­ния более десяти вызовов LIO.

Приложения, выполняющие код SQL, порождающий множество вызо­вов LIO, создает серьезные препятствия для масштабируемости систем с большим количеством пользователей. Лишние вызовы LIO не только отнимают мощность процессора, но и могут вызвать большое количе­ство событий latch free для защелок cache buffers chains . Получение и освобождение защелок само по себе может при­вести к избыточному потреблению мощности процессора, особенно в конфигурациях, где аналитики увеличили значение, заданное для _SPIN_COUNT по умолчанию (обычно не стоит этого делать).

В настоящее время есть ряд полезных ресурсов по оптимизации SQL: .1 Участники разнообразных списков рассылки, например Oracle-L ( http://www.cybcon.com/~jkstill), также делают замечательное дело, помогая друг другу писать эффективный код. Все эти ресурсы содер­жат хорошие советы о том, как писать эффективный SQL, применяя методы, часть из которых перечислена ниже:

Диагностика выполнения команд SQL посредством таких инстру­ментов, как tkprof, EXPLAIN PLAN, и отладочных событий 10032, 10033,10046,10079,10104 и 10241.

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

Работа с текстом SQL, направленная на использование более эффек­тивных планов выполнения.

Выбор эффективной стратегии индексирования с тем, чтобы обес­печить сокращение объема данных для запросов без создания до­полнительной нагрузки в операциях INSERT, UPDATE, MERGE и DELETE.

Применение хранимых планов выполнения с целью заставить оп­тимизатор запросов Oracle использовать выбранный вами план.

Создание приемлемой статистики для таблиц, индексов и базы дан­ных с тем, чтобы наилучшим образом информировать оптимизатор запросов Oracle о ваших данных.

Разработка таких физических моделей данных, которые облегчают хранение и выборку в контексте приложения.

Разработка логических моделей данных, облегчающих хранение и выборку в контексте приложения.

Оптимизация разбора

Избыточный разбор - это верный путь к невозможности обеспечить масштабируемость приложения для работы с большим количеством пользователей . Студенты обычно приходят к нам, считая, что полные разборы (hard parses) значительно замедля­ют обработку транзакций, а вот в частичном разборе (soft parse) нет ничего страшного. Более того, иногда люди считают, что полного раз­бора можно избежать, а частичного - нет. Оба мнения верны лишь на­половину. Полные разборы действительно так ужасны, как о них ду­мают, и их можно избежать, используя в коде SQL переменные связы­вания вместо литералов. Однако частичный разбор столь же ужасен, и часто без него тоже можно обойтись.

Многие авторы употребляют словосочетание «частичный разбор» (soft parse) как синоним для «вызова разбора» (parse call). Мне больше нра­вится термин «вызов разбора», т. к. он обращает наше внимание на приложение, внутри которого в действительности может быть пред­принято спасительное действие. Если же говорить о «частичном разбо­ре», то внимание акцентируется на базе данных, которая не является местом решения нашей проблемы. И вот почему. Каждый раз, когда серверный процесс Oracle получает вызов разбора от приложения, это­му процессу необходимо использовать процессор сервера базы данных. Если будет обнаружен подходящий для этого запроса разделяемый курсор в кэше курсоров сеанса или библиотечном кэше Oracle, то вызов разбора никогда не приведет к полному разбору, и окажется, что затра­ты на разбор совсем не так велики, как могли бы быть. Однако отсут­ствие разбора обходится еще дешевле, чем частичный разбор. Наилуч­шую масштабируемость для большого количества пользователей име­ют приложения, в которых разбор происходит как можно реже. Следу­ет по возможности избавиться от всех ненужных вызовов разбора.

Для обеспечения масштабируемости лучше всего, чтобы прило­жения осуществляли минимально возможное количество вызовов базы данных. Именно в этом направлении развивается Oracle Call Interface (OCI). Например, в версии 8 OCI предприняты меры для снижения количества пересылок между клиентом и сервером ( http://otn.oracle.com/tech/oci/htdocs/Developing_apps.html). Версия 9.2 OCI идет еще дальше, делая так, что многие вызовы базы данных приложения вообще не достигают базы дан ных ( http://otn.oracle.com/tech/oci/htdocs/oci9ir2_new_features).

В системах с высокой конкурентностью и неоправданно многочислен­ными вызовами разбора большое количество событий CPU service зачас­тую коррелирует с большим количеством событий ожидания latch free для библиотечного кэша, разделяемого пула и других защелок. Само по себе получение и освобождение защелок может привести к избыточно­му потреблению мощности процессора, особенно в конфигурациях, где аналитики увеличили значение, заданное для SPINCOUNT по умолчанию (опять-таки, обычно не стоит этого делать). Более того, избыточные вызовы разбора могут привести к ненужным задержкам SQL*Net message from client, способным добавить до нескольких секунд лишнего време­ ни отклика на каждую секунду реальной работы, выполняемой в базе данных. Наконец, вызовы разбора для длинных фрагментов SQL созда­ют ненужные задержки SQL*Net more data from client, которые также мо­гут внести существенный вклад в увеличение времени отклика.

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

Старайтесь обходиться без строковых литералов в инструкциях WHERE. Заменяйте их переменными связывания (заполнителями), особенно когда строковый литерал имеет высокую кардинальность (т. е. строка может иметь множество значений). Использование строковых литералов вместо переменных связывания приводит к затратам процессорного времени (CPU service), а также вызывает в системах с высокой конкурентностью ненужные события ожида­ния освобождения защелок для библиотечного кэша, разделяемого пула и объектов кэша строк.

Пример 11.2. Разбор внутри цикла серьезно препятствует масштабируемости

Отключите те функции интерфейсного драйвера, которые приводят к увеличению количества вызовов разбора по отношению к их чис­лу в исходном коде приложения. Например, Perl DBI содержит ат­рибут уровня prepare под названием ora_check_sql. Его значение по умолчанию равно 1, что означает два вызова разбора для каждого вызова функции Perl prepare. Первый вызов разбора выполняется с тем, чтобы помочь SQL-разработчику приложения быстрее отла­дить свой исходный код за счет предоставления более подробной диагностической информации для неудачных вызовов разбора. Од­ нако в промышленных системах такую функцию следует отключить, т. к. она приводит к излишним вызовам разбора.