13 Занятие 13: Оптимизация запросов / Профилирование
Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: qpt_13_optim_profiling.html Archive: QPT-handouts-16.zip Страниц PDF: 20
Page 1: Оптимизация запросов / Профилирование
Авторские права
© Postgres Professional, 2019–2024 Авторы: Егор Рогов, Павел Лузанов, Павел Толмачев, Илья Баштанов Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
Использование материалов курса
Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.
Page 2: Темы
- Профилирование как инструмент для поиска узких мест
- Выбор подзадачи для профилирования
- Средства построения профиля
Page 3: Инструмент
Профилирование
- выделение подзадач
- продолжительность
- количество выполнений
- Что оптимизировать?
- чем больше доля подзадачи в общем времени выполнения,
- тем больше потенциальный выигрыш
- необходимо учитывать затраты на оптимизацию
- полезно взглянуть на задачу шире
Комментарии преподавателя
В предыдущих темах мы разобрались с тем, как работают запросы, из каких «кирпичиков» строится план выполнения и что влияет на выбор того или иного плана. Это самое сложное и важное. Поняв механизмы, с помощью логики и здравого смысла можно разобраться в любой возникшей ситуации и понять, эффективно ли выполняется запрос и что можно сделать, чтобы улучшить показатели.
Но как найти тот запрос, который имеет смысл оптимизировать?
В принципе, решение любой задачи оптимизации (не только в контексте СУБД) начинается с профилирования, хоть этот термин и не всегда употребляется явно. Мы должны разбить задачу, вызывающую нарекания, на подзадачи и измерить, какую часть общего времени они занимают. Также полезна информация о числе выполнений каждой из подзадач.
Чем больше доля подзадачи в общем времени, тем больше выигрыш от оптимизации именно этой подзадачи. На практике приходится учитывать и ожидаемые затраты на оптимизацию: получить потенциальный выигрыш может оказаться нелегко.
Бывает так, что подзадача выполняется быстро, но часто (это, например, характерно для запросов, генерируемых ORM-ами). Может оказаться невозможным ускорить выполнение отдельных запросов, но стоит задаться вопросом: должна ли подзадача выполняться так часто? Такой вопрос может привести к необходимости изменения архитектуры, что всегда непросто, но в итоге дать существенный выигрыш.
Page 4: Что профилировать
Всю активность системы
- полезно администратору для поиска ресурсоемких задач
- мониторинг, расширение pg_profile
- Отдельную задачу, вызывающую нарекания
- полезно для решения конкретной проблемы
- чем точнее, тем лучше: широкий охват размывает картину
Комментарии преподавателя
Профиль общей активности может многое сказать администратору, который занят не решением конкретной проблемы, а поиском наиболее ресурсоемких задач, оптимизация которых, вероятно, снизит нагрузку на систему.
Такой профиль должна показывать система мониторинга.
Можно также воспользоваться расширением pg_profile. Оно основано на статистических представлениях PostgreSQL, информация из которых сохраняется в хранилище снимков активности. Изучая и сравнивая между собой снимки, можно выявлять проблемы и причины их возникновения. Автор расширения — Андрей Зубков (https://github.com/zubkov-andrei/pg\_profile).
Детальное обсуждение того, как именно строить систему мониторинга, какие инструменты для этого использовать и какие метрики собирать выходит за рамки курса, но порекомендуем книгу Алексея Лесовского «Мониторинг PostgreSQL»:
https://postgrespro.ru/education/books/monitoring
Для решения конкретной проблемы необходимо строить профиль так, чтобы измерения затрагивали только действия, необходимые для воспроизведения именно этой проблемы. Если, например, пользователь жалуется на то, что «окно открывается целую минуту», бессмысленно смотреть на всю активность в базе данных за эту минуту: в эти цифры попадут действия, не имеющие никакого отношения к открытию окна.
Page 5: Что измерять
Время выполнения
- имеет смысл для пользователя
- крайне нестабильный показатель
- Страничный ввод-вывод
- стабилен по отношению ко внешним факторам
- мало что говорит пользователям
Комментарии преподавателя
В каких единицах измерять ресурсы? Самая осмысленная для конечного пользователя характеристика — это время отклика: сколько прошло времени «от нажатия на кнопку» до «получения результата».
Однако с технической точки зрения смотреть на время не всегда удобно. Оно сильно зависит от массы внешних факторов: от наполненности кеша, от текущей загруженности сервера. Если поиском проблемы занимаются не на основном, а на тестовом сервере с иными характеристиками, то добавляется и разница в аппаратном обеспечении, в настройках, в профиле нагрузки.
В этом смысле может оказаться удобнее смотреть на ввод-вывод, то есть на число прочитанных и записанных страниц. Этот показатель более стабилен. Он не зависит от характеристик «железа» и от нагрузки, то есть будет совпадать на разных серверах с одинаковыми данными и равными значениями параметров конфигурации.
Поэтому очень важно анализировать проблемы на полном наборе данных. Для этого, например, можно использовать Database Lab Engine — средство быстрого создания тонких клонов, разработанное Postgres.ai: https://github.com/postgres-ai/database-lab-engine.
Ввод-вывод, как правило, адекватно отражает объем работы, необходимый для выполнения запроса, поскольку основное время уходит на чтение и обработку страниц с данными. Хотя такой показатель не имеет смысла для конечного пользователя.
Page 6: Профиль приложения
Подзадачи
- клиентская часть
- сервер приложений
- сервер баз данных
- ← проблема часто, но не всегда, именно здесь
- сеть
- Как профилировать
- технически трудно, нужны разнообразные средства мониторинга
- подтвердить предположение обычно несложно
Комментарии преподавателя
Для пользователя имеет смысл время отклика. Это означает, что в профиль, вообще говоря, должна входить не только СУБД, но и клиентская часть, и сервер приложений, и передача данных по сети.
Часто проблемы с производительностью кроются именно в СУБД, поскольку один неадекватно построенный план запроса может увеличивать время на порядки. Но это не всегда так. Проблема может оказаться в том, что у клиента медленное соединение с сервером, что клиентская часть долго отрисовывает полученные данные и т. п.
К сожалению, получить такой полный профиль достаточно трудно. Для этого все компоненты информационной системы должны быть снабжены подсистемами мониторинга и трассировки, учитывающими особенности именно этой системы. Но обычно несложно измерить общее время отклика (хотя бы и секундомером) и сравнить с общим временем работы СУБД; убедиться в отсутствии существенных сетевых задержек и т. п. Если же этого не проделать, то вполне может оказаться, что мы будем искать ключи там, где светлее, а не там, где их потеряли. Собственно, весь смысл профилирования и состоит в том, чтобы выяснить, что именно нуждается в оптимизации.
Далее мы будем считать установленным, что проблема именно в СУБД.
Page 7: Профиль PL/pgSQL
Расширение PL Profiler
- стороннее расширение
- предназначено только для функций на PL/pgSQL
- профилирование отдельного скрипта или сеанса
- отчет о работе в html, включая flame graph
- Расширение plpgsql_check
- стороннее расширение
- проверяет PL/pgSQL и встроенный SQL
- позволяет находить ошибки компиляции
- поиск зависимых объектов в функциях
- возможность автоматического профилирования функций
Комментарии преподавателя
При выполнении пользователем некоторого действия, выполняется обычно не один, а несколько запросов. Как определить тот запрос, который имеет смысл оптимизировать? Для этого нужен профиль, детализированный до запросов.
Однако серверная часть приложения, выполняющаяся в СУБД, может состоять не только из SQL-запросов, но и содержать процедурный код. Если код написан на языке PL/pgSQL, то для его профилирования можно воспользоваться сторонним расширением PL Profiler (основной автор — Ян Вик: https://github.com/bigsql/plprofiler).
Расширение позволяет профилировать как отдельно выполняемые скрипты, так и снимать профиль работающего сеанса. Результат работы формируется как html-файл, содержащий необходимую информацию. В том числе он включает изображение вызовов в виде «языков пламени» (flame graph).
Еще можно порекомендовать расширение plpgsql_check (основной автор — Павел Стехуле: https://github.com/okbob/plpgsql\_check). Это расширение проверяет достоверность идентификаторов SQL, используемых в коде PL/pgSQL, пытается выявить проблемы с производительностью, имеет встроенный профилировщик кода.
Поскольку курс посвящен оптимизации запросов SQL, мы не будем рассматривать эти расширения, но упоминаем их для полноты картины.
Page 8: Профиль запросов
Журнал сообщений сервера
- включается конфигурационными параметрами:
- log_min_duration_statement = 0
- время и текст всех запросов
- log_line_prefix
- идентифицирующая информация
- сложно включить для отдельного сеанса
- большой объем; при увеличении порога времени теряем информацию
- не отслеживаются вложенные запросы
- (можно использовать расширение auto_explain)
- анализ внешними средствами, такими, как pgBadger
Комментарии преподавателя
Для получения профиля по выполняемым SQL-запросам есть два основных средства, встроенных в PostgreSQL: журнал сообщений сервера и статистика.
В журнале сообщений с помощью конфигурационных параметров можно включить вывод информации о запросах и времени их выполнения. Обычно для этого используется параметр log_min_duration_statement, хотя есть и другие.
Как локализовать в журнале именно те запросы, которые относятся к действиям пользователя? Имело бы смысл включать и выключать параметр для конкретного сеанса, но штатных средств для этого не предусмотрено. Можно фильтровать общий поток сообщений, выделяя только нужные; для этого удобно в параметр log_line_prefix вывести дополнительную идентифицирующую информацию. Использование пула соединений еще больше усложняет ситуацию.
Для анализа вложенных запросов (когда в запросе вызывается функция, содержащая другие запросы) потребуется модуль auto_explain (https://postgrespro.ru/docs/postgresql/16/auto-explain).
Следующая проблема — анализ журнала. Для этого приходится применять внешние средства, из которых стандартом де-факто является расширение pgBadger (https://github.com/darold/pgbadger).
Разумеется, можно выводить в журнал и собственные сообщения, если они могут помочь делу.
Page 9: Профиль запросов
Расширение pg\_stat\_statements
- подробная информация о запросах в представлении
- (в том числе о вложенных)
- ограниченный размер хранилища
- запросы считаются одинаковыми «с точностью до констант»,
- даже если имеют разные планы выполнения
- идентификация только по имени пользователя и базе данных
- унифицированный идентификатор запроса (compute_query_id = auto)
Комментарии преподавателя
Второй способ — статистика, а именно расширение pg_stat_statements ( https://postgrespro.ru/docs/postgresql/16/pgstatstatements).
Расширение собирает достаточно подробную информацию о выполняемых запросах (в том числе в терминах ввода-вывода страниц) и отображает ее в представлении pg_stat_statements.
Поскольку число различных запросов может быть очень большим, размер хранилища ограничен конфигурационным параметром; в нем остаются наиболее часто используемые запросы.
При этом «одинаковыми» считаются запросы, имеющие одинаковое (с точностью до констант) дерево разбора. Надо иметь в виду, что такие запросы могли иметь разные планы выполнения и выполняться разное время.
К сожалению, есть сложности и с идентификацией: запросы можно отнести к конкретному пользователю и базе данных, но не к сеансу.
При установке конфигурационного параметра compute_query_id в значение auto или on ядро СУБД будет вычислять единый идентификатор запроса. Его можно выводить в журнал сообщений, настроив параметр log_line_prefix, а также использовать для соединения информации из ядра (колонка pg_stat_activity.query_id), pg_stat_statements и других расширений.
https://postgrespro.ru/docs/postgresql/16/runtime-config-statistics#GUC-CO MPUTE-QUERY-ID
Page 10: Расширение pg_stat_statements
Подключим расширение pg_stat_statements, с помощью которого будем строить профиль:
Подключение библиотеки требует перезагрузки.
Настроим расширение так, чтобы собиралась информация о всех запросах, в том числе вложенных:
Создадим представление, чтобы смотреть на собранную статистику выполнения операторов:
Здесь мы для простоты показываем только часть полей и выводим только общее число страниц кеша.
Профиль выполнения
Рассмотрим задачу. Требуется построить отчет, выводящий сводную таблицу количества перевезенных пассажиров: строками отчета должны быть модели самолетов, а столбцами — категории обслуживания. Сначала создадим функцию, возвращающую число пассажиров для заданной модели и категории обслуживания:
Для отчета создадим функцию, возвращающую набор строк:
Page 11: Page 11
Теперь исследуем работу предложенной реализации. Сбросим статистику:
Дата последнего сброса статистики видна в представлении pg_stat_statements_info:
Выполним функцию report():
Посмотрим, какую статистику мы получили:
Page 12: Page 12
Первым идет основной запрос, который детализируется ниже. Toplevel — признак выполнения запроса на верхнем уровне. Вторым идет запрос из функции qty — он вызывался 27 раз. Третий — запрос, по которому работает цикл. Обратите внимание, что идентификатор запроса в pg_stat_statements совпадает с системным благодаря настройке compute_query_id:
Page 13: Профиль одного запроса
EXPLAIN ANALYZE
- подзадачи — узлы плана
- продолжительность — actual time
- или страничный ввод-вывод — buffers
- количество выполнений — loops
- Особенности
- помимо наиболее ресурсоемких узлов, кандидаты на оптимизацию —
- узлы с большой ошибкой прогноза кардинальности
- любое вмешательство может привести к полной перестройке плана
- иногда приходится довольствоваться простым EXPLAIN
Комментарии преподавателя
Так или иначе, среди выполненных запросов мы находим тот, который будем оптимизировать. Как работать с самим запросом? Тут тоже поможет профиль, который выдает команда EXPLAIN ANALYZE.
Подзадачами такого профиля являются узлы плана (план — не плоский список, а дерево). Продолжительность выполнения узла и число его повторений покажут time и loops из блока значений actual. С помощью параметра buffers можно получить объем ввода-вывода (и время, потраченное на операции ввода-вывода, если включен параметр track_io_timing).
План выполнения содержит также крайне важную информацию об ожидании оптимизатора относительно кардинальности каждого шага. Как правило, если нет серьезной ошибки в прогнозе кардинальности, то и план будет построен адекватный (если нет, то надо менять глобальные настройки). Поэтому стоит обращать внимание не только на наиболее ресурсоемкие узлы, но и на узлы с большим (на порядок или выше) расхождением между rows и actual rows. Смотреть надо на наиболее вложенный проблемный узел, поскольку ошибка будет распространяться от него выше по дереву.
Бывают ситуации, когда запрос выполняется так долго, что выполнить EXPLAIN ANALYZE не представляется возможным. В таком случае придется довольствоваться простым EXPLAIN и пытаться разобраться в причине неэффективности без полной информации.
Работать с большими планами в текстовом виде не всегда удобно. Для лучшей наглядности можно пользоваться сторонними инструментами, из которых отметим https://explain.tensor.ru/about.
Page 14: Профиль одного запроса
Попробуем теперь вывести тот же отчет одним запросом и посмотрим на его выполнение командой EXPLAIN с параметрами analyze и buffers, чтобы в плане отображалось количество фактически прочитанных и записанных страниц файлов данных и временных файлов:
Page 15: Page 15
Обратите внимание, как уменьшилось время выполнения запроса и насколько меньше страниц пришлось прочитать благодаря устранению избыточных чтений (верхняя строчка Buffers). Можно посчитать общее количество страниц во всех задействованных таблицах:
Это число может служить грубой оценкой сверху для запроса, которому требуются все строки: обработка существенно большего числа страниц может говорить о том, что данные перебираются по нескольку раз. Видно, что в данном случае план близок к оптимальному. Его тоже можно улучшить, но уже не так радикально (очевидный момент — недостаток оперативной памяти для хеш-соединений).
Page 16: Итоги
Для поиска задач, нуждающихся в оптимизации, используется профилирование
- Средства профилирования зависят от задачи:
- журнал сообщений сервера и pg_stat_statements
- EXPLAIN ANALYZE
Page 17: Практика
- Выполните первую версию отчета, показанного в демонстрации, включив вывод текста и времени выполнения запросов в журнал сообщений. 2. Посмотрите, какая информация попала в журнал сообщений. 3. Повторите предыдущие пункты, включив расширение auto_explain с выводом вложенных запросов.
Page 18: 1. Отчет
Включаем вывод операторов и времени их выполнения в журнал:
2. Сообщения в журнале
Вложенные SQL-операторы не выводятся.
3. Расширение auto\_explain
Page 19: Page 19
Выведем несколько последних строк журнала сообщений:
Page 20: Page 20
В журнал попадают вложенные запросы и планы выполнения — собственно, это и есть основная функция расширения. Помимо этого, с указанными настройками в журнал попадают параметры запроса (строка Query Parameters) и идентификатор запроса (строка Query Identifier).