Максимальная производительность: архитектурные подходы к оптимизации запросов в PostgreSQL 9785002027040

В книге описываются ключевые принципы и архитектурные подходы к оптимизации запросов в PostgreSQL. Особое внимание уделе

142 21 1001KB

Russian (Old) Pages 79 [82] Year 2024

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Максимальная производительность: архитектурные подходы к оптимизации запросов в PostgreSQL
 9785002027040

Table of contents :
Содержание
Введение
Этапы выполнения запроса
Разбор
Трансформация
Планирование
Перебор планов
Выбор оптимального плана
Общая схема оценки стоимости плана
Оценка кардинальности
Физические операции
Чтение данных
Seq Sсаn
lndex Scan
lndex Only Scan
Bitmap Scan
Относительная эффективность операций чтения
Объединение данных
Nested Loop
Merge Join
Hash Join
Относительная эффективность операций объединения
EXPLAIN и EXPLAIN ANALYZE
EXPLAIN
EXPLAIN ANALYZE
Статистика
Средний размер поля
Неопределенные значения
Уникальные значения
Корреляция
Наиболее частые значения
Гистограмма
Статистика по выражению
Статистика для индекса по выражению
Многовариантная статистика
Работа со статистикой
Оптимизация
Короткие запросы
Селективность в коротких запросах
Мультиколоночные индексы
Работа с оператором LIKE
Избыточные фильтры
Добавление индексов
Длинные запросы
Работа с представлениями
Материализованные представления
Common ТаЫе Expressions

Citation preview

Дарья Золотухина

МАКСИМАf1ЬНАА ПРОИЗВО.QИ­ ТЕf1ЬНОСТЬ: АРХИТЕКТ!::IРН ЫЕ по.ахо.аы К ОПТИМИЗАЦИИ ЗАПРОСОВ В POSTGRESQL

2024

УДК 004.4 ББК 32.973.4 З-81

З-81 Золотухина, Д.Ю. Максимальная производительность: архитектурные подходы к оптимизации запросов в PostgreSQL / Д.Ю. Золотухина. Воронеж, 2024.-79 с.

ISBN 978-5-00202-704-0

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

ISBN 978-5-00202-704-0

©

Золотухина Д. Ю., 2024

СО.QЕРЖАНИЕ

3 5 5

8 9 12 16 17 17 20 20 20 22 25 26 30 31 32 35 37 41 43 43 44 47

Введение Этапы выполнения запроса Разбор Трансформация Планирование Перебор планов Выбор оптимального плана Общая схема оценки стоимости плана Оценка кардинальности Физические операции Чтение данных Seq Sсаn lndex Sсаn lndex Оnlу Sсаn Bitmap Sсап Относительная эффективность операций чтения Объединение данных Nested Loop Merge Joiп Hash Joiп Относительная эффективность операций объединения EXPLAIN и EXPLAIN ANALYZE EXPLAIN EXPLAIN ANALVZE

Статистика

49 50 52 54 54 56 60 60 61 63 64 68 69 69 69 71 71 72 73 77 77 78

Средний размер поля Неопределенные значения Уникальные значения Корреляция Наиболее частые значения Гистограмма Статистика по выражению Расширенная статистика по выражению Статистика для индекса по выражению Многовариантная статистика Работа со статистикой Оптимизация Короткие запросы Селективность в коротких запросах Мультиколоночные индексы Работа с оператором LIKE Избыточные фильтры Добавление индексов Длинные запросы Работа с представлениями Материализованные представления Commoп ТаЫе Expressioпs

3

ВВЕДЕНИЕ

SQL - это декларативный язык программирования. Вы формулируете запрос, описывая то, что хотите получить, а база данных решает, как именно достичь результата. В этом и заключается особенность: вы говорите базе данных, что вам нужно, а PostgreSQL определяет как это сделать. Это включает выбор индексов, порядок объединения таблиц, проверку условий фильтрации и другие аспекты выполнения запроса. В стандартной конфигурации PostgreSQL не допу­ скает вмешательства в этот процесс на уровне пользователя, но он предоставляет возможность увидеть, каким образом выполняется запрос через команды, такие как EXPLAIN и EXPLAIN ANALYZE. Однако, несмотря на высокий уровень автоматизации, про­ цесс оптимизации запросов не всегда идеален. Сложные запросы, большие объемы данных и специфические струк­ туры таблиц могут привести к неоптимальным решениям, что негативно сказывается на производительности. Для того чтобы помочь PostgreSQL сделать правильный выбор, необходимо глубоко понимать его внутреннее устройство, алгоритмы, которые он использует для выполнения запро­ сов, и типичные ошибки, которые могут возникать. Когда вы занимаетесь оптимизацией запросов, важно знать, как PostgreSQL анализирует данные, как строит планы выполнения, и на основе каких метрик принимает решения. Например, в каких случаях база данных решит использовать индекс, а когда предпочтёт последовательное сканирование таблицы. Также важно уметь распознавать ситуации, когда PostgreSQL делает неэффективный выбор,

4

который замедляет выполнение запроса, и как можно этому помешать. Оптимизация запросов - это искусство, которое требует понимания не только SQL-синтаксиса, но и глубокой инте­ грации с внутренними механизмами PostgreSQL. В процессе оптимизации вы становитесь не только автором запросов, но и партнёром планировщика запросов, помогая ему при­ нимать более точные и взвешенные решения. Эта книга познакомит вас с инструментами и техниками, которые позволят вам эффективно оптимизировать запросы в PostgreSQL, улучшить производительность приложений и глубже понять, как работает одна из самых мощных СУБД.

5

ЭТАПЫ ВЫПОf1НЕНИЯ ЗАПРОСА

Чтобы эффективно работать с PostgreSQL, важно глубже изучить его внутреннюю архитектуру и понять, как система функционирует «Под капотом». Начнем с основного - обра­ ботки и выполнения запросов в PostgreSQL. Простой вариант клиент-серверного протокола PostgreSQL позволяет вы- полнить запрос SQL, отправляя серверу его текст и получая в ответ полный результат выполнения, сколько бы строк он ни содержал. Запрос, поступа- ющий серверу, проходит несколько этапов: он разбирается, транс­ формиру- ется, планируется и, наконец, исполняется.

Разбор

Трансформэ1.tия

Ппан роеание

Выnоnнение

Разбор

Прежде всего, запрос необходимо разобрать, чтобы система смогла точно определить, что требуется выполнить. Этот процесс включает два ключевых этапа: лексический и син­ таксический анализы. Лексический анализатор разделяет запрос на отдельные элементы - лексемы (ключевые слова, строки, числовые значения и т.д.), а синтаксический анали­ затор проверяет, соответствует ли полученный набор лексем

6

правилам грамматики SQL. В результате успешного разбора запрос преобразуется в памяти сервера в абстрактное син­ таксическое дерево, которое представляет его структуру для дальнейшей обработки. Например, рассмотрим запрос:

1

SELECT schemaname, taЫename FROM pg_taЫes WHERE taЫeowner = 'postgres' ORDER ВУ taЫename;

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

SELECT

�DER BV L>blon•mo

pg 18111!!

\'IНERE 18111e.:...ner • �!n!Jres'

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

7

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

в

Трансформация

Далее запрос может подвергаться трансформациям. Эти преобразования выполняются ядром системы и преследуют несколько целей. Одна из них - замена имени представле­ ния на поддерево, которое соответствует запросу, лежащему в основе этого представления. На этом этапе также происходит обработка конструкций, таких как SEARC H и CYC LE, которые используются для рекурсивных запросов. В приведённом примере pg_taЫes является представлением. Подставив его определение непосредственно в запрос, мы получили бы следующее: SELECT schemaпame, tаЫепаmе FROM ( - pg_taЫes SELECT п.пsрпаmе AS schemaпame, c.relпame AS tаЫепаmе, pg_get_userbyid(c.relowпer) AS taЫeowпer, FROM pg_class с LEFT JOIN pg_пamespace п ON п.oid

=

c.relпamespace

LEFT JOIN pg_taЫespace t ON t.oid = c.reltaЫespace WHERE c.relkiпd = ANY (ARRAY['r':: char, 'р':: char] ) ) WHERE taЫeowпer = 'postgres' ORDER ВУ tаЫепаmе;

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

9

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

Планирование

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

10

Объединение наборов строк всегда происходит попарно, что создает множество вариантов с разным порядком соединений. Более того, существует несколько алгоритмов для этих соединений: можно последовательно перебирать строки одного набора и искать соответствия в другом, или же предварительно отсортировать оба набора и выполнить их слияние. Каждый алгоритм работает лучше в определённых условиях, и выбор неправильного способа может значи­ тельно замедлить выполнение запроса. Разница между неоптимальным и оптимальным планами может быть ко­ лоссальной - в тысячи или даже миллионы раз. Поэтому планировщик запросов, который отвечает за оптимизацию, является одним из самых сложных компонентов системы. План выполнения запроса также представлен в виде дерева, но его узлы описывают уже не логические, а физические операции над данными. На практике для отображения тексто­ вого представления плана используется команда EXPLAIN. На рисунке ниже выделены ключевые узлы дерева плана. Эти узлы отражают выполнение запроса и показаны в вы­ воде команды EXPLAIN. Узлы SeqScaп в плане запроса соответствуют последовательному чтению таблиц, а узел Nestedloop - операции соединения.

11

=> EXPLAIN SELECT schemaпame, tаЫепаmе FROM pg_taЫes WHERE taЫeowпer = 'postgres' ORDER ВУ tаЫепаmе; QUERY PLAN Sort (cost=22.28 .. 22.28 rows=1 width=1 28) Sort Кеу: c.relпame - > Nested Loop Left Joiп (cost=0.00 .. 22.27 rows=1 width=1 28) Joiп Filter: (п.oid = c.relпamespace) -> Seq Sсап оп pg_class с (cost=0.00 .. 21 .1 8 rows=1 width=72) Filter: ((relkiпd = ANY ('{r, p}'::»char» [] )) AND (pg_g ... -> Seq Sсап оп pg_пamespace п (cost=0.00 .. 1 .04 rows=4 wid ... (7 rows)

L.ANNEDSТМT

TARGEТENТRY

r(Jfl EXPLAIN SELECT schemaпame, tаЫепаmе FROM pg_taЫes WHERE tаЫеоwпег = 'postgres' ORDER ВУ tаЫепаmе; QUERY PLAN Sort (cost=22.28 .. 22.28 rows=1 width=1 28) Sort Кеу: c.relпame -> Nested Loop Left Joiп (cost=0.00 .. 22.27 rows=1 width=1 28) Joiп Filter: (п.oid = c.relпamespace) -> Seq Sсап оп pg_class с (cost=0.00 .. 21 .1 8 rows=1 width=72) Filter: ((relkiпd = ANY ('{г, p}'::»char» [] )) AND (pg_g ... -> Seq Sсап оп pg_пamespace п (cost=0.00 .. 1 .04 rows=4 wid ... (7 rows)

Первая компонента (начальная стоимость, startup cost) отра­ жает затраты на подготовку к выполнению узла, в то время как вторая компонента (полная стоимость, total cost) опре­ деляет общие затраты на извлечение всех результирующих строк.

17 О бщая схема оценки стоимости плана

Для того чтобы получить полную оценку плана, необходимо проанализировать каждый из его узлов. Стоимость узла зависит как от его типа (например, стоимость чтения данных из таблицы и сортировки различаются), так и от объема обрабатываемых данных (обычно меньший объем означа­ ет меньшие затраты). Тип узла известен, а для прогнози­ рования объема данных нужно оценить кардинальность входных наборов (количество строк, поступающих на вход узла) и селективность узла (долю строк, которые останутся на выходе). Для этого требуется статистическая информация о данных, включая размер таблиц и распределение данных по столбцам. Таким образом, оптимизация зависит от корректной статистики, собираемой и обновляемой процессом автоанализа. Если кардинальность оценена правильно для каждого узла плана, то стоимость будет адекватно отражать реальные затраты. Основные ошибки планировщика, как правило, свя­ заны с неверной оценкой кардинальности и селективности, что может происходить из-за устаревшей или некорректной статистики, невозможности её использования или, реже, из-за несовершенства моделей, лежащих в основе работы планировщика. Оценка кардинальности

Оценка кардинальности - это рекурсивный про­ цесс. Ч тобы определить кардинальность узла плана, необходимо: 1. Оценить кардинальность каждого из дочерних узлов и получить количество строк, поступающих на вход узла. 2. Оценить селективность самого узла, то есть долю входя­ щих строк, которая останется на выходе. Умножение этих двух значений даст кардинальность узла.

18

Селективность представляется в виде числа от О до 1. Селек­ тивность, близкая к нулю, обычно обозначается как высокая, а близкая к единице - как низкая. Это может показаться нелогичным, однако селективность здесь интерпретируется как избирательность: условие, выбирающее малую долю строк, имеет высокую селективность (избирательность), тогда как условие, оставляющее почти все строки, харак­ теризуется низкой селективностью. Сначала рассчитываются кардинальности листовых уз­ лов, где расположены методы доступа к данным. Для этого используется статистика, в частности, общий размер таблицы. Селективность условий, наложенных на таблицу, зависит от их типа. В простейшем случае можно принять за селек­ тивность некую константу, Для оценки кардинальности соединений вычисляется кар­ динальность декартова произведения, которая равна про­ изведению кардинальностей двух наборов данных, а также оценивается селективность условий соединения, что в свою очередь зависит от типа условий. Аналогичным образом кардинальность рассчитывается и для других узлов, таких как сортировка и агрегация. Важно подчеркнуть, что ошибка в расчете кардинальности, возникающая на нижних уровнях плана, может распростра­ няться вверх, что в конечном итоге приводит к неправильной оценке и выбору неэффективного плана. Этот неприятный эффект усугубляется тем, что планировщик имеет статисти­ ческую информацию только о таблицах, но не о результатах соединений. Оценка стоимости выполняется в рекурсивном порядке. Для вычисления стоимости поддерева плана необходимо

19

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

20

ФИЗИЧЕСКИЕ ОПЕРАЦИИ

Теперь, когда мы разобрались с принципами работы плани­ ровщика PostgreSQL, пришло время углубиться в физические операции, которые он применяет при построении плана выполнения запросов. Эти операции являются основой процесса оптимизации, и правильный выбор таких опера­ ций напрямую влияет на производительность и скорость обработки данных. В PostgreSQL физические операции, которые можно увидеть в выводе команды EXPLAIN, представляют собой конкретные шаги выполнения запроса. Эти операции можно разделить на две большие категории: чтение данных и объединение данных. Давайте рассмотрим основные из них подробнее. Чтение данных

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

21

версии создаётся новая версия с изменёнными данными, и обе версии - старая и новая - продолжают существовать одновременно. В итоге одна и та же строка может быть представлена в нескольких версиях, каждая из которых отражает состояние данных в разное время. Если данные становятся слишком большими, они могут быть сжаты и оптимизированы. На уровне операционной системы та­ блицы хранятся в виде файлов, и каждая таблица состоит из одного или нескольких файлов по 1 гигабайту каждый. Когда один файл заполняется, PostgreSQL автоматически создаёт следующий файл. Файлы можно увидеть в файло­ вой системе, и их имена будут идентичны за исключением различий в расширениях. Seq Sсап предполагает последовательное чтение всех этих файлов. PostgreSQL проходит через каждую страницу данных, независимо от условий фильтрации. Последовательное сканирование эффективно, если фильтры не избирательны, то есть селективность является низкой, или если нужно обработать большую часть данных таблицы. Поскольку страницы читаются последовательно, без необхо­ димости дополнительных операций поиска, стоимость такого сканирования минимальна. По умолчанию планировщик присваивает ей базовую стоимость, равную единице. Эта «единичка» указывает на то, что Seq Sсап выполняется с ми­ нимальными затратами ресурсов по сравнению с другими, более сложными методами доступа к данным, такими как индексное сканирование.

22

Пример работы Seq Sсап: EXPLAI N ANALYZE SELECT * FROM employees WHERE departmeпt = 'IТ'; Seq Sсап оп employees (cost=0.00 .. 1 25.00 rows=50 width=1 00) Filter: (departmeпt = 'IТ') Rows Removed Ьу Filter: 950 Actual time=0.01 5 .. 0.1 28 rows=50 loops= 1

В данном примере мы выбираем все записи из таблицы employees, где значение столбца departmeпt равно 'IТ'. Если для столбца departmeпt нет индекса, PostgreSQL выполнит полное сканирование таблицы. Seq Sсап эффективен, когда: • Таблица небольшая, и затраты на индексацию выше, чем

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

Здесь PostgreSQL использует индекс для поиска нужных строк. Вместо чтения всей таблицы система обращается к индексу для быстрого поиска интересующих строк. Эта операция полезна, если запрос содержит условия, соответ­ ствующие полям индекса. lпdex Sсап использует внутри себя 8-tгее индекс. Индексы 8-tгее - это наиболее распространенный и универсальный тип индексов в PostgreSQL, который используется для уско­ рения операций поиска данных. 8-tгее индекс организует

23

данные в виде сбалансированного дерева, где каждая опе­ рация поиска, вставки или удаления может выполняться за логарифмическое время относительно размера данных. Индекс В-дерева состоит из нескольких уровней. На верх­ нем уровне (корневом) хранятся ссылки на промежуточные узлы, а на нижнем уровне (листьях) - указатели на строки таблицы. Когда PostgreSQL использует B-tree индекс, он начинает с корневого узла и, шаг за шагом, переходит к со­ ответствующим листовым узлам, где и находятся ссылки на необходимые строки таблицы. Каждый узел В-дерева отсортирован, что позволяет значительно ускорить поиск. B-tree индекс эффективен для диапазонных запросов (на­ пример, WHERE age BETWEEN 20 AND 30), а также для точных сравнений (например, WHERE id 5). Важно, что В-дерево автоматически поддерживает балансировку - то есть все пути от корня к листам имеют одинаковую длину. Это га­ рантирует, что операция поиска будет одинаково быстрой для всех данных, что делает B-tree надежным выбором для большинства типов запросов. =

Когда запрос оптимизирован и планировщик выбирает использование индекса, PostgreSQL выполняет так назы­ ваемый «lпdex Sсап». В этом случае он обращается к B-tree индексу, чтобы быстро найти нужные строки. Если индекс полностью удовлетворяет запрос (например, запрос на по­ иск по ключу), данные могут быть найдены сразу через индекс. В противном случае, PostgreSQL использует индекс для получения ссылок на строки и затем извлекает полные данные из таблицы. Преимущества и ограничения B-tree индексов: • Преимущества: B-tree индекс универсален и работает прак­

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

24 • Ограничения: 8-tгее менее эффективен для неупорядочен­

ных или редко встречающихся значений, и его использование может стать избыточным при таблицах с очень редкими или уникальными значениями в столбцах. Этот тип индекса - основной инструмент для ускорения поиска в PostgreSQL, и знание того, как и когда использовать 8-tгее индексы, может существенно повысить производи­ тельность запросов. Пример выполнения запроса с использованием индексного сканирования (lпdex Sсап) в PostgreSQL: Создаем индекс на столбец department CREATE I NDEX idx_department ON employees(department); - Выполняем запрос с фильтрацией по индексированному столбцу EXPLAI N ANALYZE SELECT * FROM employees WHERE department = 'IT';

8 этом примере сначала создается индекс на столбце de­ partmeпt в таблице employees. После этого выполняется запрос, который использует данный индекс для фильтра­ ции записей, где departmeпt 'IT'. PostgreSQL, при наличии индекса, выбирает индексное сканирование, если это более эффективно. =

lndex Scan using idx_department оп employees (cost=0.42 .. 1 5.37 rows= 50 width=1 00) lndex Cond: (department = 'IТ') Actual time=0.01 5 .. 0.045 rows=50 loops= 1

lпdex Sсап эффективен, когда: • Фильтруется небольшое количество строк из большого объема данных.

25

·Существует подходящий индекс по столбцу, используемому в условии WHERE. • Запросы требуют выборки данных из определенного диа­ пазона значений (например, диапазон дат или чисел). • Таблица велика, а выборка - небольшая, так как индекс позволяет значительно сократить число просматриваемых строк. Индексное сканирование обычно быстрее последователь­ ного сканирования, если результатом запроса является не­ большая часть данных, благодаря тому, что поиск в индексе обходится дешевле, чем полное сканирование таблицы. lndex Only Scan

lпdex Опlу Sсап (IOS) - это метод извлечения данных из та­ блицы, который использует только индекс без обращения к самой таблице. Этот подход может значительно ускорить выполнение запросов, особенно если таблица содержит много строк и полей, но есть определенные условия, при которых его использование будет эффективным. Пример выполнения запроса с использованием lпdex Опlу Sсап в PostgreSQL: Создаем индекс на столбец department CREATE I NDEX idx_department_name ON employees(department); Выполняем запрос, который может использовать lndex Only Scan -

EXPLAI N ANALYZE SELECT department FROM employees WHERE department = 'IТ';

В этом примере создается индекс на столбце departmeпt в таблице employees. Затем выполняется запрос, который извлекает только данные из индексированного столбца departmeпt. Это важный момент, поскольку PostgreSQL может выполнить lпdex Опlу Sсап только в том случае, если

26

все необходимые данные для запроса находятся в самом индексе и не требуется обращаться к таблице. lndex Only Scan using idx_department_name оп employees (cost=0.42 .. 1 2.37 rows=50 width=1 О) lndex Cond: (department = 'IТ') Неар Fetches: О Actual time=0.01 5 .. 0.035 rows=50 loops= 1

lпdex Опlу Sсап эффективен, когда: • Запрос использует только столбцы, покрытые индексом, и не требует дополнительных данных из таблицы. • Результат запроса можно полностью получить из индекса, что исключает необходимость чтения данных непосред­ ственно из таблицы, ускоряя выполнение запроса. • Особенно полезен для запросов, которые часто читают данные, но изменения в таблице происходят не так часто. lпdex Опlу Sсап работает более эффективно, когда данные в индексе актуальны и не требуют дополнительных проверок с основного хранилища. Таким образом, lпdex Опlу Sсап - это мощный инструмент для повышения производительности выборок в PostgreSQL, особенно в случаях, когда структура таблицы и запросы позволяют использовать все преимущества индексов. Этот вид сканирования становится возможным только при опре­ деленных условиях, например, когда индексы полностью покрывают запрос и данные индекса актуальны. Bitmap Scan

Bitmap Sсап - это метод извлечения данных в PostgreSQL, который позволяет эффективно обрабатывать запросы с использованием индексов, особенно когда необходимо об­ рабатывать большие объемы данных. Он работает, создавая битовую карту, которая указывает, какие строки в таблице соответствуют условиям запроса. Давайте рассмотрим

27

подробнее, как именно работает Bitmap Sсап, его этапы и преимущества. Этапы работы Bitmap Sсап: 1. Создание битовой карты. Когда выполняется запрос, который может воспользоваться индексами, PostgreSQL ана­ лизирует условия фильтрации и определяет, какие индексы могут быть использованы. Затем для каждого подходящего индекса создается битовая карта (Ьitmap), где каждый бит соответствует строке в таблице. Если строка удовлетворяет условиям запроса, соответствующий бит устанавливается в 1, в противном случае - в О. 2. Объединение битовых карт. Если запрос включает не­ сколько условий, PostgreSQL может использовать несколько индексов. В этом случае он создает отдельные битовые карты для каждого индекса и объединяет их с помощью ло­ гических операций (AND, OR). Например, если используются два условия с оператором AND, PostgreSQL выполнит поби­ товую операцию И (AND) между двумя битовыми картами, чтобы получить окончательную битовую карту. 3. Сканирование таблицы. После создания объединенной битовой карты PostgreSQL использует ее для извлечения данных. Он обращается к таблице и получает строки, со­ ответствующие установленным битам в битовой карте. Это позволяет минимизировать количество считываемых страниц, так как PostgreSQL извлекает только те строки, которые необходимы, а не все строки таблицы. 4. Извлечение данных. На этом этапе система обращается к таблице для извлечения строк, отмеченных в битовой карте. Если строки находятся на одной и той же странице, это значительно ускоряет процесс, так как уменьшается количество операций ввода-вывода.

28

Пример использования Bitmap lпdex Sсап в PostgreSQL: Создаем индексы на столбцы salary и departmeпt CREATE I NDEX idx_salary ON employees (salary); CREATE I NDEX idx_departmeпt ON employees (departmeпt); - Выполняем запрос с двумя условиями, которые могут использовать Bitmap Sсап EXPLAI N ANALYZE SELECT employee_id, salary, departmeпt FROM employees WHERE salary > 50000 AND departmeпt = 'IТ';

В данном примере создаются индексы на столбцы salary и departmeпt в таблице employees. Затем выполняется за­ прос с двумя условиями (salary > 50000 и departmeпt 'IТ'), которые могут быть обработаны с помощью Bitmap lпdex Sсап. PostgreSQL может использовать Bitmap Sсап, когда запрос включает несколько условий, которые должны быть объединены логически (например, через AND). =

Bitmap Неар Sсап оп employees (cost=1 2.45 .. 1 34.75 rows=25 width=32) Recheck Сопd: ((salary > 50000) AND (departmeпt = 'IТ')) Bitmap lпdex Sсап оп idx_salary (cost=0.00 . .4.34 rows= 50 width=O)

->

lпdex Сопd: (salary > 50000) -> Bitmap lпdex Sсап оп idx_departmeпt (cost=0.00 .. 3.45 rows=25 width=O) lпdex Сопd: (departmeпt = 'IТ') Actual time=0.045 .. 0.095 rows=20 loops= 1

Bitmap Sсап эффективен в следующих случаях: • Запросы с несколькими условиями: если запрос содержит

множество условий фильтрации (например, несколько столб­ цов в операторе WHERE), Bitmap Sсап может использовать

29

несколько индексов. Он создает битовую карту, где уста­ навливается бит для каждой строки, соответствующей условиям, что позволяет эффективно фильтровать данные. ·Большие объемы данных: Ьitmap Sсап эффективен, когда нужно обрабатывать большие объемы данных, которые не помещаются в память. Он позволяет избегать полного сканирования таблицы, вместо этого обращаясь к соот­ ветствующим страницам данных, что может значительно снизить время выполнения. • Комбинация условий с использованием логических опера­ ций: если условия выбора содержат логические операторы (AND, OR), Bitmap Sсап может комбинировать результаты из нескольких индексов. Это позволяет ему создавать более точную битовую карту для извлечения данных. • Высокая селективность условий: если условия запроса от­ фильтровывают значительную часть данных, использование Bitmap Sсап может быть особенно эффективным. Высокая селективность условий приводит к созданию небольшой битовой карты, что уменьшает объем данных, которые не­ обходимо извлечь. • Индексы на разных полях: если у вас есть индексы на раз­ ных полях, которые вы используете в запросе, Bitmap Sсап может эффективно объединять их. Это особенно полезно в сложных запросах, где используются несколько индексов. Таким образом, Bitmap lпdex Sсап используется для объе­ динения результатов нескольких индексов, позволяя Post­ greSQL более эффективно обрабатывать запросы с множе­ ственными условиями.

30 Относительная эффективность операци й чтения

Завершая обсуждение физических операций доступа к дан­ ным в PostgreSQL, стоит взглянуть на их относительную эффективность, учитывая такие параметры, как стоимость, скорость и селективность запроса.

о

На приведённом графике можно увидеть, что наилучшую производительность обеспечивает lпdex Опlу Sсап, посколь­ ку он позволяет извлечь данные напрямую из индекса, без обращения к самим страницам таблиц. Однако, несмотря на его высокую эффективность, lndex Only Scan не всегда при­ меним, так как это зависит от состояния видимости данных (например, если требуется проверка на актуальность строк). Следующей по эффективности операцией является lndex Scan, который использует индекс для поиска нужных строк, но в отличие от lndex Only Scan, требует обращения к самим страницам данных для проверки условий видимости. Bitmap lndex Scan также находится в середине по производитель­ ности. Он особенно полезен при выборках, возвращающих

31

большое количество строк, так как использует промежуточ­ ные битовые карты для эффективного доступа к данным, минимизируя обращения к страницам. Sequeпtial Sсап, или последовательный обход всех строк таблицы, становится выгоден только на крупных объемах данных или при низкой селективности запросов. Он при­ меняется, когда выборка охватывает значительную часть таблицы, либо когда использование индексов неэффективно. Важно понимать, что точные цифры и результаты могут сильно варьироваться в зависимости от характеристик ваших данных и их распределения. Этот график скорее представляет общий порядок предпочтений операций, чем абсолютные данные о производительности, поэтому точные сценарии нужно оценивать на практике. О бъединение данных

Операции объединения используются, когда PostgreSQL нужно объединить данные из нескольких источников (та­ блиц). Рассмотрим ключевые из них.

32 Nested Loop

Nested Loop - это один из классических и наиболее простых алгоритмов соединения Uoiп) в PostgreSQL. Его суть состоит в том, что для каждой строки из одной таблицы (внешняя таблица) база данных выполняет поиск всех строк из другой таблицы (внутренняя таблица), соответствующих условию соединения.

Алгоритм Nested Loop реализован следующим образом: 1. Чтение внешней таблицы. PostgreSQL выбирает строку из первой (внешней) таблицы. 2. Поиск соответствий. Для каждой строки внешней таблицы PostgreSQL проходит через вторую (внутреннюю) таблицу, находя все строки, которые соответствуют условию соеди­ нения (например, WHERE t1.id t2.id). 3. Повторение. Этот процесс повторяется для каждой строки из внешней таблицы до тех пор, пока все строки не будут обработаны. Nested Loop - мощный инструмент, когда объемы данных относительно малы или когда одна из таблиц хорошо ин­ дексирована. Однако для больших наборов данных или при отсутствии индексов этот метод может стать узким местом, и его лучше заменить другими алгоритмами соединений. =

Пример работы Nested Loop:

33

Предположим, у нас есть две таблицы: employees (сотрудники): id (уникальный идентификатор сотрудника) - name (имя сотрудника) - department_id (идентификатор отдела) departments (отделы): id (уникальный идентификатор отдела) - department_name (название отдела)

34

Теперь мы хотим выполнить запрос, который вернет имена всех сотрудников и названия их отделов. Такой запрос мо­ жет быть реализован с использованием вложенного цикла (Nested Loop), если одна из таблиц небольшая и PostgreSQL считает этот метод оптимальным: EXPLAI N ANALYZE SELECT е.паmе, d.departmeпt_пame FROM employees е JOIN departmeпts d ON e.departmeпt_id = d.id;

Результат: Nested Loop (cost=0.29 .. 8.31 rows= 1 0 width=64) (actual time=0.035 .. 0.053 rows=1 О loops=1 ) Seq Sсап оп departmeпts d (cost=0.00 .. 1 .01 rows=5 width=32) (actual time=0.005 . .0.006 rows=5 loops=1 )

->

lпdex Sсап usiпg employees_departmeпt_id_idx оп employees е (cost=0.29 .. 1 .03 rows=2 width=32) (actual time=0.004 .. 0.005 rows=2 loops=5)

->

lпdex Сопd: (departmeпt_id = d.id) Рlаппiпg Time: 0.068 ms Executioп Time: 0.077 ms

Nested Loop показывает отличную производительность в следующих ситуациях: • Небольшой объем данных. Алгоритм хорошо работает,

когда объем данных одной или обеих таблиц мал. Напри­ мер, если одна таблица содержит десятки или сотни строк, а другая - тысячи, Nested Loop будет выполняться быстро, поскольку обход большого количества строк не потребуется. • Использование индексов. Когда одна из таблиц (чаще вну­ тренняя) индексирована по полю, участвующему в условии соединения, Nested Loop будет работать гораздо быстрее.

35 PostgreSQL будет использовать индекс для быстрого поиска

соответствующих строк во внутренней таблице. ·Ситуации с селективными запросами. Если запрос воз­ вращает небольшое количество строк, а одна из таблиц существенно меньше, Nested Loop будет более эффективным по сравнению с другими типами соединений (например, с Hash Joiп). Это связано с тем, что обход меньших объемов данных не требует значительных ресурсов. MergeJoin Merge Joiп

- это алгоритм соединения Qoiп) в PostgreSQL, ко­ торый используется для объединения двух отсортированных наборов данных. Его основной принцип заключается в том, что обе таблицы должны быть отсортированы по ключу соединения, что позволяет PostgreSQL последовательно проходить по строкам каждой таблицы и эффективно объ­ единять их. Алгоритм Merge Joiп выполняется следующим образом: 1. Предварительная сортировка. Обе таблицы, участвую­ щие в соединении, должны быть отсортированы по полю (или полям), используемому для соединения. Если данные уже отсортированы (например, через индекс), сортировка не требуется. 2. Пос трочный обход. PostgreSQL начинает с первой строки каждой таблицы. Если значения ключа соединения в обеих таблицах совпадают, эти строки объединяются и добавля­ ются в результат. 3. Продвижение по строкам. Если значение ключа соединения в одной таблице меньше, чем в другой, PostgreSQL переходит к следующей строке меньшей таблицы. Этот процесс продол­ жается, пока все строки обеих таблиц не будут обработаны. Пример работы Merge Joiп: Предположим, у нас есть две таблицы:

36

orders (заказы): - id (уникальный идентификатор заказа) - customer_id (идентификатор клиента) customers (клиенты): - id (уникальный идентификатор клиента) - паmе (имя клиента)

Обе таблицы содержат индексы по столбцу id, и мы хотим выполнить запрос, который соединяет заказы с клиентами, чтобы отобразить имена клиентов для каждого заказа. Если обе таблицы отсортированы по полю соединения (например, по customer_id в таблице заказов и id в таблице клиентов), PostgreSQL может выбрать использование Merge Joiп: EXPLAI N ANALYZE SELECT o.id AS order_id, с.пате AS customer_пame FROM orders о JOIN customers с ON o.customer_id = c.id;

Вывод: Merge Joiп (cost=0.43 .. 1 .44 rows=1 О width=64) (actual time=0.004 .. 0.008 rows=1 О loops=1 ) lпdex Sсап usiпg orders_customer_id_idx оп orders о (cost=0.1 5 .. 0.24 rows=5 width=32) (actual time=0.002 .. 0.004 rows=5 loops=1 )

->

lпdex Sсап usiпg customers_pkey оп customers с (cost=0.1 5 .. 0.24 rows=5 width=32) (actual time=0.002 .. 0.003 rows=5 loops=1 )

->

Рlаппiпg Time: 0.085 ms Executioп Time: 0.01 2 ms

Merge Joiп эффективен в следующих ситуациях: • Данные отсортированы или могут быть быстро отсортиро­ ваны. Merge Joiп работает быстрее, если обе таблицы уже отсортированы по ключу соединения. Это часто случается,

37

когда данные сортируются индексами. В таких случаях PostgreSQL может пропустить этап сортировки и напрямую выполнять соединение. • Большие объемы данных . Merge Joiп хорошо подходит для обработки больших наборов данных. В отличие от Nested Loop, который может быть медленным для больших таблиц, Merge Join использует линейное сравнение строк, что делает его более масштабируемым и эффективным при больших объемах данных. • Равномерные наборы данных. Если обе таблицы содержат сравнимое количество строк и соединение основано на рав­ номерно распределенных значениях ключа соединения, Merge Join может значительно опередить другие алгоритмы за счет своей линейной природы. ·Сортировка обходится дешевле. В случае, если требуется сортировка данных (например, из-за отсутствия индексов), PostgreSQL оценивает стоимость сортировки для обоих наборов данных. Если сортировка обходится дешевле, чем альтернативы (Hash Join или Nested Loop), Merge Join будет выбран как оптимальный метод. Merge Join это мощный и эффективный алгоритм для со­ единения данных в PostgreSQL, особенно когда данные уже отсортированы или могут быть эффективно отсортированы. Он превосходит по производительности такие алгоритмы, как Nested Loop, при работе с большими объемами данных и может быть оптимальным решением для запросов с рав­ номерно распределенными ключами. Однако он становится менее эффективным при работе с несортированными дан­ ными или небольшими наборами данных. -

Hash Join

Hash Join это один из наиболее популярных и эффективных алгоритмов соединения Uoin) в PostgreSQL. Он особенно по­ лезен для обработки больших наборов данных, когда ключи соединения не отсортированы и индексы отсутствуют. Этот метод использует хэш-таблицы для сопоставления строк из двух таблиц, что делает его мощным инструментом для -

ЭВ

объединения данных с произвольным распределением ключей. Hash Join выполняется в два основных этапа: 1. Создание хэш-таблицы (build phase). PostgreSQL выбирает меньшую из двух таблиц (или nодзаnросов) и строит хэш-та­ блицу по ключу соединения. Каждая строка этой таблицы хэшируется по значению ключа соединения и добавляется в хэш-таблицу. 2. Поиск по хэш-таблице (ргоЬе phase). После создания хэш-та­ блицы PostgreSQL начинает проходить по строкам второй (большей) таблицы и использует хэш-функцию для поиска соответствий в хэш-таблице. Если найдены совпадения, соответствующие строки объединяются и добавляются в результат. Пример работы Hash Join: Предположим, у нас есть две таблицы: employees, содер­ жащая информацию о сотрудниках, и departments, храня­ щая данные о отделах. Мы хотим объединить эти таблицы, чтобы получить имена сотрудников вместе с названиями их отделов. Для выполнения такого запроса можно использовать Hash Joiп, который эффективно объединяет данные из обеих таблиц, создавая хеш-таблицу на основе одной из них. EXPLAI N ANALYZE SELECT e.name, d.department_name FROM employees е JOIN departments d ON e.department_id

=

d.id;

39

Результат: Hash Joiп (cost=1 6.00 .. 34.00 rows=1 ООО width=48) (actual time=0.1 00 .. 0.250 rows=1 ООО loops=1 ) Hash Сопd: (e.departmeпt_id = d.id) -> Seq Sсап оп employees е (cost=0.00 .. 1 8.00 rows=1 500 width=32) (actual time=0.01 0 .. 0.1 50 rows= 1 500 loops=1 ) -> Hash (cost= 1 2.00 .. 1 2.00 rows= 500 width=1 6) (actual time=0.050 .. 0.050 rows=500 loops=1 ) -> Seq Sсап оп departmeпts d (cost=0.00 .. 1 2.00 rows=500 width= 1 6) (actual time=0.020 .. 0.030 rows= 500 loops=1 )

Рlаппiпg Time: 0.1 00 ms Executioп Time: 0.350 ms Hash Joiп особенно эффективен в следующих сценариях: ·Большие наборы данных. Hash Joiп превосходно справля­ ется с большими таблицами, так как его время выполнения практически линейно и не зависит от наличия индексов. Это особенно полезно, если одна или обе таблицы содержат боль­ шой объем данных, а ключи соединения не отсортированы. ·Отсутствие индексов. В ситуациях, когда таблицы не име­ ют индексов по ключам соединения, Hash Joiп становится предпочтительным выбором, так как построение и исполь­ зование хэш-таблицы обходится дешевле, чем сортировка данных для Merge Joiп или многократное сканирование таблиц для Nested Loop. • Неупоря доченные данные. Если данные распределены ха­ отично, и сортировка была бы слишком дорогой операцией, Hash Joiп может обойти необходимость предварительной сортировки, что делает его более экономичным по ресурсам. • Равномерное распределение ключей. Hash Joiп работает особенно эффективно, когда значения ключа соединения равномерно распределены, так как это минимизирует раз­ мер хэш-таблицы и увеличивает вероятность быстрого нахождения совпадений. Hash Joiп становится неэффективен, когда имеются:

40

·Маленькие таблицы. Для небольших таблиц, где операции соединения можно выполнить быстро и без создания хэш-та­ блиц, использование Hash Joiп может быть избыточным. В таких случаях PostgreSQL может выбрать более простой и быстрый алгоритм, например, Nested Loop. • Ограничения по памяти. Hash Joiп требует выделения па­ мяти для создания хэш-таблицы. Если таблицы слишком велики для размещения хэш-таблицы в оперативной памяти, PostgreSQL может начать использовать временные файлы на диске, что значительно замедляет работу. Это может быть критично в средах с ограниченной памятью. • Неравномерное распределение данных. Если ключ соеди­ нения распределен неравномерно, например, несколько ключей сильно преобладают, это может привести к созда­ нию очень больших сегментов хэш-таблицы. Это снижает эффективность алгоритма и приводит к высокому потре­ блению ресурсов. Hash Joiп это мощный инструмент для выполнения соеди­ нений в PostgreSQL, особенно когда работа идет с большими и неупорядоченными наборами данных. Его способность быстро находить совпадения без необходимости сортировки или индексов делает его идеальным выбором в случаях, когда таблицы велики или ключи соединения распределены произвольно. Однако при работе с небольшими таблицами или в условиях ограниченной памяти следует учитывать, что другие алгоритмы могут быть более эффективными. -

41 Относительная эффективность операци й о бъединения

о

На данном графике наглядно показана связь между эффек­ тивностью различных методов соединения таблиц и селек­ тивностью данных. Nested Loop как алгоритм соединения особенно хорош при малом количестве возвращаемых строк и работает эффек­ тивно для небольших наборов данных или при наличии индексов на одну из соединяемых таблиц. Его стоимость возрастает при увеличении объема данных, так как он вы­ полняет повторные вложенные циклы для каждой строки первой таблицы, чтобы найти соответствующие строки во второй таблице. Hash Joiп начинает демонстрировать свою эффективность при работе с большими объемами данных и средней се­ лективности. Этот метод создает хэш-таблицу по одной из соединяемых таблиц, что позволяет быстрее находить соответствующие строки во второй таблице. Стоимость этого метода более стабильна и растет медленнее, чем

42

у Nested Loop, что делает его предпочтительным для боль­ ших наборов данных. Merge Joiп с индексом или сортировкой особенно эффекти­ вен, когда данные уже отсортированы. Он работает, сопостав­ ляя отсортированные строки из двух таблиц, что позволяет минимизировать затраты на вычисления. Однако если данные не отсортированы, потребуется предварительная сортировка, что увеличивает стоимость операции. Сортиров­ ка увеличивает стоимость операции, однако при больших объемах данных Merge Joiп с индексом или сортировкой становится более эффективным по сравнению с Nested Loop и Hash Joiп. Таким образом, выбор физической операции соединения в PostgreSQL напрямую зависит от объема данных и струк­ туры запроса, и график наглядно демонстрирует, как про­ изводительность этих методов меняется в зависимости от селективности.

43

EXPLAI N И EXPLAI N ANALYZE

В этой главе мы сосредоточимся на командах EXPLAIN и EXPLAIN ANALYZE, которая является важным инструментом для анализа и оптимизации запросов в PostgreSQL. EXPLAIN

Команда EXPLAIN помогает понять, как PostgreSQL собира­ ется выполнить запрос, не выполняя его фактически. Она показывает план, который выбирает планировщик для вы­ полнения: как будут использованы индексы, какие операции сканирования (например, последовательное сканирование или индексное) будут применены и в каком порядке будут объединяться таблицы. Этот этап позволяет выявить потен­ циальные проблемы с производительностью, прежде чем запрос будет запущен на больших объемах данных. Используя EXPLAIN, можно выявить, какой вид сканирова­ ния применяется и как PostgreSQL планирует организовать работу с данными. Однако важно помнить, что это теорети­ ческая оценка: на практике результаты могут отличаться. Пример работы команды EXPLAIN:

1

1

EXPLAI N SELECT * FROM orders WHERE customer_id = 42;

1

Результат:

Seq Scan оп orders (cost=0.00 .. 35.50 rows=5 width=1 00) Filter: (customer_id = 42)

44

Разберем параметры, которые мы видим в результате запроса: • Seq Sсап: Вид операции, в данном случае последовательное сканирование таблицы. • Cost: Стоимость выполнения операции, выраженная в двух значениях: начальная стоимость (0.00) и стоимость завер­ шения операции (35.50). Это ключевой параметр, кото­ рый показывает, насколько "дорогим" будет выполнение операции. Первое значение - это начальная стоимость (например, время на инициализацию), а второе - общая стоимость операции. Эти значения относительны и зависят от настроек кластера. • Rows: Ожидаемое количество строк, которое вернет опе­ рация (5). Оптимизатор делает оценку, сколько строк будет возвращено на этапе выполнения запроса. • Width: Средний размер строки в байтах (100 байт). Оцен­ ка размера каждой возвращаемой строки, что важно для планирования ресурсов. EXPLAIN ANALYZE

Команда EXPLAIN ANALYZE расширяет возможности EXPLAIN за счет реального выполнения запроса. Она предоставляет не только предполагаемый план, но и фактические данные, включая время выполнения, количество обработанных строк и другие метрики. Это позволяет измерить реальные временные показатели, количество строк, обработанных на каждом этапе, и выявить различия между теоретическим и фактическим исполнением. Важные параметры, которые добавляются в результате выполнения EXPLAIN ANALYZE, включают фактическое время выполнения операций (Actual Time), количество итераций (Loops), объем данных и скорость обработки на каждом этапе. Важность связки с оптимизацией заключается в том, что эти данные дают возможность разработчику не просто предположить, как запрос будет выполняться, а увидеть

45

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

1

EXPLAI N ANALYZE SELECT * FROM orders WHERE customer_id = 42;

1

Результат:

Seq Scan оп orders (cost=0.00 .. 35.50 rows=5 width=1 00) (actual time=0.020 .. 0.050 rows=2 loops= 1 ) Filter: (customer_id = 42) Rows Removed Ьу Filter: 48 Planning Time: 0.1 00 ms Execution Time: 0.1 50 ms

Разберем новые параметры, которые мы можем увидеть в результате работы EXPLAIN ANALYZE: • Actual Time: Фактическое время выполнения операции в миллисекундах (включает начальное и конечное время). • Rows Removed Ьу Filter: Количество строк, которые были отброшены фильтром (48). • Loops: Сколько раз операция была выполнена (обычно больше 1 для вложенных циклов). • Рlаппiпg Time: Время, потраченное на планирование за­ проса. • Executioп Time: Общее время выполнения запроса. Эти параметры позволяют более точно оценить производи­ тельность запросов и выявить узкие места в их выполнении.

46

Так как в EXPLAIN ANALYZE предоставляет не только план, который PostgreSQL предполагает для выполнения запроса, но и реальные данные по его исполнению, появляется воз­ можность сравнить количество строк, которое оптимизатор ожидал получить, с фактическими результатами. Напри­ мер, Postgres мог прогнозировать, что вернется 90% строк, но на самом деле в при выполнении запроса вернулись только 2%. Это может привести к неэффективному выбору физической операции, например, использованию Seq Sсап вместо lпdex Sсап. Анализируя эти данные, можно понять, почему Postgres повел себя не так, как ожидалось, и какие действия можно предпринять для того, чтобы помочь Post­ gres выбрать более оптимальный план выполнения, таким образом оптимизируя выполнение запроса.

47

СТАТИСТИКА В этой главе мы подробно разберем, на чем основыва­ ется PostgreSQL при расчете плана выполнения запроса. Если EXPLAIN ANALYZE фактически выполняет запрос, как EXPLAIN мог заранее знать количество строк и другие параметры? Ответ заключается в том, что планировщик PostgreSQL использует статистическую информацию о та­ блицах и индексах, чтобы предсказать, как будет выполнен запрос.

И хотя фактические результаты могут варьироваться, пла­ нировщик делает предположения на основе собранной статистики, которая хранится в системе. Таким образом, статистика служит основой для вычислений, позволяя планировщику оценить, как лучше всего выполнить запрос. Статистика - это данные, которые собираются системой для оценки количества строк, селективности условий и других характеристик таблиц. Эти данные помогают оптимизатору запросов выбирать наиболее эффективные планы выпол­ нения запросов, снижая затраты на их выполнение. Рассмотрим ее внутреннее строение. Основная статистика на уровне таблицы хранится в систем­ ном каталоге в таблице pg_class. К этим данным относятся: • reltuples - количество строк в таблице; • relpages - размер таблицы в страницах; • relallvisiЫe - количество страниц, полностью видимых

(отмеченных в карте видимости).

48

Для примера, посмотрим на эти значения для таблицы ftights: =>

SELECT

reltuples, relpages, relallvisiЫe pg_class WHERE relпame = 'ftights'; reltuples 1 relpages 1 relallvisiЫe FROM

-------

+- - - - - - -+- - - - - - - - - -

21 4867 1 2624 1 2624 (1 row)

Значение reltuples - это оценочное количество строк в табли­ це. Это значение используется для расчета потенциального размера результата запроса. Например, если планиров­ щик считает, что в таблице orders содержится 10,000 строк, но фактически их 50,000, это может привести к выбору неэффективного плана. relpages указывает общее количество страниц, занимае­ мых таблицей. Каждая страница содержит фиксированное количество строк (обычно 8 КБ). Этот параметр помогает определить, сколько страниц планировщику необходимо просмотреть при выполнении запроса. Если у таблицы много страниц, планировщик может решить использовать последовательный скан вместо индексного. relallvisiЫe - это количество страниц, на которых все строки видимы для текущей транзакции. Этот параметр важен для оптимизации операций, таких как Bitmap Неар Sсап. Если страницы содержат невидимые строки, их обход может быть неэффективным. Статистика в PostgreSQL собирается в процессе анализа данных, который может быть запущен вручную или автома­ тически. Однако базовая статистика обновляется не только во время анализа, но и при выполнении таких операций, как VACUUM FU LL, C LUSTER, CREATE INDEX и REINDEX, а также уточняется при очистке.

49

Во время анализа случайным образом выбираются стро­ ки из таблицы - по 300 строк на каждый установленный параметр default_statistics_target. Размер таблицы при этом не влияет на количество строк, необходимых для сбора статистики, поскольку выборка, достаточная для построения статистики с заданной точностью, не зависит от общего объема данных. Строки выбираются из такого же количества случайных страниц таблицы. В небольших таблицах количество прочитанных страниц и строк может быть меньше, но для крупных таблиц выборка всегда ограничена, что может привести к небольшим расхожде­ ниям в оценках. Тем не менее, даже приблизительная статистика обычно оказывается достаточно точной для выбора оптимального плана выполнения запроса. С редни й размер поля

Параметр avg_width в PostgreSQ L представляет собой среднюю ширину (в байтах) всех значений в столбце опре­ делённой таблицы. Этот параметр играет важную роль в планировании запросов, так как помогает планировщику оценить, сколько памяти потребуется для хранения данных, и оптимизировать операции, связанные с выборкой и обра­ боткой этих данных. Понимание средней ширины столбца позволяет PostgreSQ L более точно планировать операции, такие как SELECT, JOIN и ORDER ВУ. Например, если у вас есть столбец с перемен­ ной длиной (например, VARCHAR), планировщик может использовать avg_width для более эффективного выбора методов доступа и вычисления памяти, необходимой для хранения результатов. Предположим, у нас есть таблица employees с полем паmе, в котором хранятся имена сотрудников. Мы хотим узнать, какая средняя ширина этого поля.

50

Запрос для получения avg_width: SELECT attname, avg_width FROM pg_stats WHERE taЫename = 'employees' AND attname

=

'name';

Результат: attname 1 avg_width - - ---

+ - - - - --

name 1 20 (1 row)

В этом примере avg_width для столбца паmе составляет 20 байт. Это означает, что в среднем каждое имя занимает 20 байт в памяти. Если значение avg_width не актуально или не соответствует реальному распределению данных, планировщик может вы­ бирать менее эффективные планы выполнения. Например, если фактическая ширина данных значительно меньше или больше, чем avg_width, это может привести к недостаточно­ му выделению памяти или, наоборот, к излишнему расходу ресурсов. Также эта статистика применяется для оценки объема памяти, необходимой для выполнения определенных операций, таких как сортировка или хеширование. Неопределенные значения

В PostgreSQ L параметр статистики пull_frac указывает на долю значений в столбце, которые являются NU LL. Этот параметр помогает планировщику запросов оценить, как много значений в таблице не имеют данных, что влияет на выбор методов доступа к данным и производительность выполнения запросов. Знание процента NU LL-значений полезно для оптимизации запросов, содержащих условия вроде WHERE columп IS

51

NOT NU LL. Если планировщик знает, что столбец содержит большое количество NU LL, он может предпочесть более эф­ фективные методы доступа для таких запросов. Например, если большая часть значений в столбце отсутствует (NU LL), использование индекса может оказаться менее выгодным. Рассмотрим таблицу orders, где столбец shipped_date может содержать пустые (NU LL) значения, если заказ ещё не был отправлен. Запрос для получения пull_frac: SELECT attname, null_frac FROM pg_stats WHERE taЫename = 'orders' AND attname

1

=

'shipped_date';

Результат:

attname 1 null_frac ------

+------

shipped_date 1 0.75 (1 row)

Это означает, что 75% строк в столбце shipped_date содержат значение NU LL. Планировщик учтёт это при построении пла­ на запроса, где может использоваться фильтр на наличие данных в этом столбце. Если статистика неактуальна или не отражает текущего состояния данных, это может привести к выбору неэффек­ тивных планов. Например, если планировщик ожидает больше NU LL-значений, чем есть на самом деле, он может выбрать неподходящую стратегию, что снизит производи­ тельность запроса.

52 Уникальн ые значения

Поле п_distiпct в представлении pg_stats отображает количе­ ство уникальных значений в столбце. Если значение п_distiпct отрицательное, его абсолютное значение интерпретируется как доля уникальных значений. Например, значение -1 ука­ зывает на то, что все значения в столбце уникальны, тогда как 3 говорит о том, что в среднем каждое значение встре­ чается в трех строках. Анализатор применяет доли, когда вычисленное количество уникальных значений превышает 10% от общего числа строк; в таких случаях предполагается, что пропорция останется стабильной и при дальнейших изменениях данных. -

частоrа



r1u11-rrac

.

.

"

.

.

.

.

.

.

. .

.

.

.

.

.

.

.

.

.

.

.

"

.

"

.

"

.

.

.

.

.

.

.

.

.

;.

.

.

.

. .

.

.

•·

.

disti ct

-

1

значения

Количество уникальных значений играет ключевую роль в ситуациях, предполагающих равномерное распределение

53

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

Рассмотрим таблицу orders с полем customer_id. Выполним запрос для получения значения п_distiпct: SELECT attname, n_distinct FROM pg_stats WHERE taЫename = 'orders' AND attname

1

=

'customer_id';

Результат:

attname 1 n_distinct - - - - --

+- - - - - -

customer_id l -0.5 (1 row)

Значение п_distiпct -0.5 означает, что PostgreSQL предпо­ лагает, что уникальные значения составляют 50% от общего количества строк в таблице. Отрицательное значение ука­ зывает на пропорциональную оценку, относительно общего числа строк в таблице. =

Иногда полезно вручную задать точное значение п_distiпct для более точной оптимизации запросов:

1

ALTER TABLE orders ALTER COLUMN customer_id SET (n_distinct = 1 00);

Это указывает, что в столбце customer_id ожидается ровно 100 уникальных значений

54 Корреляция

Поле correlatioп в представлении pg_stats отображает степень корреляции между физическим расположением данных и их логическим порядком, имеющим значение для операций сравнения. Если данные хранятся в строгом порядке воз­ растания, корреляция будет стремиться к единице; в случае убывания - к минус единице. Чем более хаотично располо­ жены данные на диске, тем ближе значение к нулю. Рассмотрим таблицу orders с полем order_date: SELECT attпame, correlatioп FROM pg_stats WHERE tаЫепаmе = 'orders' AND attпame

=

'order_date';

Результат: attпame 1 correlatioп ------

+ - - - - - --

order_date 1 0.85 (1 row)

Значение 0.85 показывает, что данные в столбце order_date довольно сильно упорядочены, что позволяет PostgreSQL эффективно использовать индекс для обработки диапазон­ ных запросов по этому столбцу. Высокая корреляция улучшает производительность запросов с диапазонными фильтрами, поскольку PostgreSQL может обрабатывать меньшее количество страниц, извлекая зна­ чения из индекса. Наи более частые значения

Для более точной оценки в условиях неравномерного распределения собирается статистика по наиболее часто встречающимся значениям (most commoп values, MCV) и ча­ стоте их появления. В представлении pg_stats эти данные

55

отображаются в двух соответствующих столбцах: most_com­ moп_vals и most_commoп_freqs.

частота mos _common_vals

mos _common_frвqs n111_1

ас

-

-

-

------

Пример: Для таблицы orders с полем status: SELECT most_commoп_vals, most_commoп_freqs FROM pg_stats WHERE tаЫепаmе = 'orders' AND attпame = 'status';

1

Результат:

mosLcommoп_vals 1 most_commoп_freqs ---------

+- - - - - - - - -

{completed, peпdiпg, shipped} 1 {0.4, 0.3, 0.2}

Здесь видно, что статус «completed» встречается в 40% за­ писей, «peпdiпg» - в 30%, а «shipped» - в 20%. Для оценки селективности условия «столбец зна­ чение» достаточно найти нужное значение в массиве =

56

most_commoп_vals и использовать соответствующую ча­ стоту из массива most_commoп_freq s с тем же индексом. Список наиболее часто встречающихся значений также служит для оценки селективности условий с неравенствами. Например, для условия типа «столбец < значение» необхо­ димо найти в массиве most_commoп_vals все значения, меньшие искомого, и сложить соответствующие частоты из массива most_commoп_freq s. Статистика частых значений показывает свою эффектив­ ность, когда разнообразие значений ограничено. Максималь­ ный размер массивов определяется тем же параметром default_statistics_target, который регулирует объем случай ной выборки строк для анализа. Гистограмма

Когда количество различных значений становится слиш­ ком велико для их записи в массив, на помощь приходит гистограмма. Гистограмма состоит из нескольких корзин, в которые распределяются значения, и их количество опре­ деляется тем же параметром default_statistics_target.

57

Ш ирина каждой корзины выбирается так, чтобы в неё поме­ щалось примерно равное количество значений. Это свой­ ство отображается в виде одинаковой площади больших заштрихованных прямоугольников на рисунке. При этом учитываются только те значения, которые не были включены в список наиболее частых. В результате суммарная частота значений в любой корзине равняется 1/число корзин.

частоrа mcv

/

mcf

/ " " ,

"

/

/

/

/

/

"

/

/

/

/

/

/

1

/

/

, ,

1 1

/

, /

nu11-rrac

.

.

.

. .

. .

.,

/ /

,

/ .

.

. . .

.

·•

.

.

.

/

3начения

his1ogram_tюunds

Гистограмма хранится в поле histogram_bounds представле­ ния pg_stats в виде массива значений, которые определяют границы корзин. left(histogram_bounds:: text,60) 1 1 ' ..: AS hist_bounds pg_stats s WHERE s.taЫename = 'boarding_passes' AND s.attname = 'seat_no'; =>

SELECT

FROM

hist_bounds {1 ОА,1 ОА,1 0С,1 ОС,1 OF,1 1 В,1 1 H,1 2A,1 2B,1 3B,1 3H,1 4G,1 5G ... (1 row)

58

Гистограм ма служит, в частности, для оценки селективно­ сти операторов «больше» и «меньше», а также в сочетании со сп иском наиболее частых значен ий. Рассмотрим п ример, иллюстрирующий количество посадоч ных талонов, выдан­ ных на дальние ряды : => EXPLAIN SELECT * FROM boarding_passes WHERE seat_no > '30А'; QUERY PLAN Seq Scan оп boarding_passes (cost=0.00 .. 1 57353.30 rows=3036992 ... Filter: ((seat_no):: text > '30А':: text) (2 rows) Номер места был выбран так, чтобы он точно находился на границе корзин гистограммы. Оценка селективности дан­ ного условия будет равна N, где N это количество корзин, значения в которых соответствуют условию, то есть распо­ лагаются справа от заданного значения. П р и этом следует учитывать, что наиболее частые значения не уч итываются в гистограмме. -

59

Хотя неопределенные значения, как правило, также не вхо­ дят в гистограмму, в столбце seat_пo их быть не должно. = > SELECT s.null_frac FROM pg_stats s WHERE s.taЫename = 'boarding_passes' AND s.attname = 'seat_no';

null_frac о

(1 row)

частоrа ,

; т--: "

" "

, -

,

, _" /

, 1

null_frac





.







.





. .,

;

;

;

/

,/

/

;

/

;

,

х

/ ;

; ; ;

� /

;

"

; ,_ , /

,

,

"

"

,

/

;

/

, , , , значениR

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

60

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

Обычно статистика по столбцу может быть применена только в том случае, если в операции сравнения с одной из сторон оператора фигурирует сам столбец, а не вы ражение. Напри­ м ер, план и ровщик не может п редсказать, как измен ится статистика после при менения функци и к столбцу, поэтому для условия вида «вызов функции константа» он всегда использует фиксированную оценку в 0,5%. =

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

Первый способ заключается в использовании расширен­ ной статистики для вы ражения. Однако такая статистика не собирается автоматически, и для ее создания необходи­ мо вручную выполнить команду C REATE STATISTICS, чтобы создать соответствующий объект в базе данных. CREATE STATISTICS flights_expr ON (extract( month FROM scheduled_departure АТ TIME ZONE 'Europe/Moscow'

=>

)) FROM flights;

61

После сбора статистики оцен ка корректи руется следующим образом: => ANALYZE flights; => EXPLAIN SELECT * FROM flights WHERE extract( month FROM scheduled_departure АТ TIME ZONE 'Europe/Moscow') = 1; QUERY PLAN Seq Scan оп flights (cost=0.00 .. 6384.1 7 rows=1 6509 width=63) Filter: (EXTRACT(month FROM (scheduled_departure АТ TIME ZONE (2 rows) Для того чтобы собран н а я статистика и с п ол ьзовалась, вы ражение в условии запроса должно совпадать с тем, как оно было записано в команде CREATE STATISTICS. Размер собираемой р а с ш и р е н н о й статистики можно и з менить с помощью команды ALTER STATISTICS, например:

1

=> ALTER STAТISТICS flights_expr SET STATISTICS 42; С татистика для индекса по выражени ю

Второй способ улучш ить оценки карди нальности закл ю­ ч ается в том, что п р и создан и и и ндекса по в ыр ажени ю собирается отдельная статистика, аналогично статистике для таблицы. Этот подход особенно полезен, если индекс действительно необходим. => DROP STATISTICS flights_expr; => CREATE I NDEX ON flights(extract( month FROM scheduled_departure АТ TI M E ZONE 'Eu rope/Moscow') ); => ANALYZE flights; => EXPLAIN SELECT * FROM flights WHERE extract( month FROM scheduled_departure АТ TIME ZONE 'Europe/Moscow') = 1;

62

QUERY PLAN Bitmap Неар Sсап оп flights (cost=31 8.86 .. 3237.40 rows= 1 6831 wi ... Recheck Сопd: (EXTRACT(moпth FROM (scheduled_departure АТ TIME ... -> Bitmap lпdex Sсап оп flights_extract_idx (cost=0.00 .. 31 4.6 ... lпdex Сопd: (EXTRACT(moпth FROM (scheduled_departure АТ TI ... (4 rows) Статистика для индексов по выражению сохраняется ана­ логично статистике для таблиц. Например, используя пред­ ставление pg_stats, можно получить количество уникальных значений, указав имя индекса в качестве tаЫепаmе. => SELECT п_distiпct FROM pg_stats WHERE tаЫепаmе = 'flights_extract_idx'; п_distiпct 12 (1 row) Для изменения точ ности статистики индекса можно вос­ пользоваться командой ALTER INDEX. Однако перед эти м может понадобиться узнать, как называется столбец, соот­ ветствующий заданному выражению. Например: => SELECT attпame FROM pg_attribute WHERE attrelid = 'flights_extract_idx':: regclass; attпame extract (1 row) => ALTER I NDEX flights_extract_idx ALTER COLUMN extract SET STATISTICS 42;

63 М ноговариантная статистика

М ноговариантная статистика в PostgreSQL позволяет учи­ тывать взаи мосвязь между несколькими столбцам и для повышения точности оценок в запросах, особенно для за­ просов, где фильтрация идет сразу по нескольким полям . В обычной статистике PostgreSQL учиты вает независи мость полей, что может приводить к неточн ы м планам, особенно есл и столбцы и ме ют коррел я ц и ю . Для этого и с п ол ьзу­ ется расширение C REATE STATISTICS, которое п о м о гает учесть корреляцию между столбцам и или их совместную селективность. П ример: Допустим, у нас есть таблица orders со столбцами custom­ er_id и amouпt. CREATE STATISTICS staLorders ON customer_id, amount FROM orders; ANALYZE orders; Теперь PostgreSQL будет и спользовать м ноговариантную статистику для этих двух столбцов, улуч шая планирование запросов, которые включают оба поля. П ример запроса: EXPLAI N ANALYZE SELECT * FROM orders WHERE customer_id = 42 AND amount > 1 00;

1

Результат без м ноговариантной статистики:

Seq Scan оп orders (cost=0.00 .. 35.60 rows=1 width=24) (actual time=0.043 . .0.046 rows=O loops= 1 ) Filter: ((customer_id = 42) AND (amount > 1 00)) Rows Removed Ьу Filter: 1 О

64

1

Рlаппiпg Time: 0.064 ms Executioп Time: 0.087 ms Резул ьтат с м ноговариантной статистикой: Bitmap Неар Sсап оп orders (cost=4.35 .. 1 8.80 rows=2 width=24) (actual time=0.021 .. 0.042 rows=2 loops=1 ) Recheck Сопd: ((customer_id = 42) AND (amouпt > 1 00)) Неар Blocks: exact=2 - > Bitmap lпdex Sсап оп idx_orders (cost=0.00 .. 4.34 rows=2 width=O) (actual time=0.01 7 .. 0.01 8 rows=2 loops=1 ) lпdex Сопd: ((customer_id = 42) AND (amouпt > 1 00)) Рlаппiпg Time: 0.070 ms Executioп Time: 0.1 00 ms

С многовариантной статистикой планировщик может более точно оценить селективность фильтра по двум столбцам и выбрать более эффективный план вы полнен ия, напри­ мер, с использованием индекса, что снижает общее время выполнения запроса. Ра бота со статистикой

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

Неверная или устаревшая статисти ка может возни кнуть в результате: • Изменений в данных. Если данные в таблице знач итель­

но изменил ись, старая статистика может уже не отражать текущее состояние.

65 • Неудачной выборки. PostgreSQL использует случай ную

выборку данных для сбора статистики. Если выборка была недостаточно репрезентативной, это также может привести к ошибкам. Ч тобы корректировать статистику в PostgreSQL, можно использовать следующие подходы: Анализ таблицы

Команда ANALYZE используется для обновления статистики для одной или нескольких таблиц. Это приводит к пересборке статистики на основе текущих данных. Пример:

1

ANALYZE orders;

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

При выполнении ANALYZE можно использовать параметры, чтобы управлять объемом данных, которые будут проана­ лизированы. Например, параметр sample_size позволяет указать количество строк для выборки. Пример:

1

ANALYZE orders (sample_size

=>

1 000);

Это позволит выполнить анализ на основе 1ООО строк, что может быть полезно, если данные сильно изменились. Принудительный анализ

66

Если необходимо принудительно обновить статистику, можно использовать опцию VERBOSE, которая даст больше инфор­ мации о процессе анализа.

1

П ример: ANALVZE VERBOSE orders; Эта команда не только обновит статистику, но и предоставит информацию о том, какие строки были проанализирова н ы . Частота обновления статистики Рекомендуется регулярно обновлять статистику, особенно для таблиц, которые часто обновляются. Установка пара­ метра autovacuum позволяет PostgreSQL автоматически об­ новлять статистику при каждом изменен ии данных. Однако, если у вас есть особые требования, вы можете настроить график обновления в зависимости от конкретных условий работы вашего приложения. Проверка статистики Для проверки текущей статистики можно использовать си­ стемные представления, такие как pg_statistic или pg_stats, чтобы видеть текущее состоя ние статистики для табл и ц и индексов.

1

П ример: SELECT * FROM pg_stats WHERE taЫename

=

'orders';

Эта команда покажет текущую стати стику для табли ц ы orders, что позволит в а м оценить, актуальна ли она. Коррекция статистики - это важны й аспект оптимизации зап росов в PostgreSQL. П равильная настройка и регулярное

67

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

68

ОПТИМИЗАЦИЯ

Теперь, обладая этими знаниями, можно перейти к рекомен­ дациям по оптимизации запросов, учитывая два основных типа запросов и соответствующие им стратегии. П е р в ы й тип - короткие запросы, ч асто встреча ющиеся в приложениях. Это выборки по идентификатору или дан­ н ы е, относящиеся к конкретному пол ьзователю. Обычно такие зап росы возвращают небол ьшое количество записей, и их эффективная обработка возможна за счет индексного сканирования. Второй тип - длинные запросы, которые формируют отчеты или выполняют агрегатны е операции, такие как подсчет значен и й . Эти зап росы, как п равило, требуют последова­ тельного сканирования таблицы (tаЫе sсап) из-за объема данных, котор ы й необходимо обработать. Н ап ри мер, при вы полнении запроса COUNT(*) избежать последовательного ска н и рования не п редставляется возможн ы м, поскольку требуется пройти по всем строкам таблицы. Для каждого из этих типов запросов существуют свои под­ ходы к оптимизации. Короткие запросы могут быть опти­ м и з и рованы за счет п равильного и спол ьзования индек­ сов. Длинные запросы, в свою очередь, можно улучшить с помощью кэширования результатов, грамотной работы со статистикой, хотя полностью исключ ить последователь­ ное сканирование в таких случаях невозможно.

69 Короткие запросы С електи вность в коротких запросах

Рассмотрим короткие запросы и их селективность на при­ мере статусов тикетов - «новый», «В работе», и «завершён­ н ы й». Ч асто возникает вопрос: нужно ли создавать индекс для адм и нистративной панели, которая фильтрует тикеты по статусу? На первый взгляд, индекс кажется лишним. Как правило, при низкой селективности (например, когда м ного записей имеют одинаковый статус) индекс не приносит пользы, так как Postgres просто не будет его использовать. В подобных случаях создание индекса не оправдано. Однако если мы обратим внимание на тот факт, что в системе может быть огромное количество завершённых тикетов, а но­ вые появляются реже, возникает другая картина. Зап росы на новые тикеты могут встречаться намного чаще. В таких ситуациях полезно создать частичный индекс, охватываю­ щий только новые тикеты . Это существенно сократит его размер и повысит эффективность запросов к таблице, так как индекс будет точечно п рименим к наиболее частому сценарию использования.

1

CREATE I NDEX tickets_status ON tickets(status) WHERE status 'new';

=

М ультиколоночные индексы

Рассмотрим использование нескольких индексов п ротив мультиколоночных индексов. Как уже упоминалось, битовые карты (Ьitmap) позволяют эффективно применять несколько индексов одновременно. Однако с ними могут возникнуть проблемы : битовые карты могут стать слишком большими и ресурсоёмкими. Например, если у нас есть зап рос на выбор проектов, форкнутых от оп ределённого проекта, и сортиров­ ка по времени обновления (updated_at), то Postgres нач нёт

70

строить огром ную битовую карту для всех п роектов, что приведёт к существенному снижению производительности. EXPLAI N SELECT * FROM projects WHERE forked_from_id = 1 ORDER ВУ updated_at DESC LIMIT 1 О Limit (cost=281 6580.03 .. 281 6580.05 rows=1 О width=1 507) -> Sort (cost=281 6580.03 .. 2820760.49 rows=1 6721 85 width= 1 507) Sort Кеу: updated_at DESC -> Bitmap Неар Sсап оп projects (cost=91 1 51 .99 .. 2780444.71 rows=1 6721 85 width= 1 507) Recheck Сопd: (forked_from_id = 1 ) -> Bitmap lпdex Sсап оп iпdex_projects_oп_forked_from_id (cost=0.00 .. 90733. 95 rows=1 6721 85 width=O) lпdex Сопd: (forked_from_id = 1 ) Для таких случаев целесообразно создать мультиколоноч­ ный индекс, объеди няющий оба поля (например, forked_from и u pdated_at). Это п оз вол ило б ы знач ител ь н о ускор ить в ы полнение з а проса, так как и ндекс охваты вал б ы оба критерия фильтрации и сортировки. CREATE I NDEX ON projects(forked_from_id, updated_at DESC); Limit (cost=40.08 .. 79.73 rows=1 О width=1 507) -> lпdex Sсап usiпg projects_forked_from_id_updated_at_idx оп projects (cost=0.43 .. 6629764.44 rows=1 6721 85 width= 1 507) lпdex Сопd: (forked_from_id = 1 ) Тем н е менее, мультиколоночные индексы и меют свои ми­ нусы. Они занимают больше места, поскольку необходимо хранить несколько значен и й для каждой записи. В физи­ чески отсортированной структуре индекса будут храниться сразу несколько полей, что делает его более «тяжёл ым» для чтения. Важно помнить, что индекс по полям 'х, у, z' можно использовать для запросов, вкл ючающих тол ько 'х', 'х, у' или 'х, у, z', но не для запросов, где участвуют только 'у' или 'у, z' .

71

П ри п роектирова н и и индексов полезно думать о них как о отсортированных сп исках. Это помогает понимать, в каких случаях их можно применять, а когда они окажутся неэф­ фекти в н ы м и . Правил ь н ы й подход к и ндексам, особен но мультиколоночн ы м, и м еет ключевое значение для опти­ мизации производительности запросов. Ра бота с оператором LI KE

Разберем работу индексов с оператором LIKE в Postgres. Хотя Postgres может использовать В-tгее индекс для опти­ мизации запросов с LIKE, это возможно не всегда. Например, если шаблон содержит постфикс (символы в конце строки, нап ример, '%аЬс'), то индекс использоваться не будет. Точно так же, если и спользуется функция в условии, например, lower(userпame) like 'аЬс%', то опти м и зация через и ндекс также не произойдёт. P o stg res оцени в ает такие з а п р о с ы как и м ею щ и е н из­ кую сел ект и в н ость - около 0,5%, что дел ает их м ал о­ п р и годн ы м и для и ндекс а ц и и . Одна ко, если в шаблоне есть п реф и кс ( н а п р и мер, userпame LIKE 'аЬс%'), и н декс будет п ри м енён, и это знач ител ьно ускорит вы пол н е н и е з а п роса. Та к и м образом, при и спользова н и и и н декса ц и и с о п е­ р а то р о м LIKE н у ж н о у ч и т ы вать структуру ш а бл о н а . Есл и это п рефиксное с р а в н е н ие, и ндекс будет п оле­ зен. В остал ь н ы х случ аях Postg res м ожет отказаться от испол ьзова н ия и ндекса, что важно учиты вать п р и опти м и з а ц и и з а п росов. И збыточные фильтры

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

72

П редположим, у нас есть адрес - Тим и рязевский проспект, 2. Этот адрес уникален, и в запросе можно было бы найти его без дополнительных критериев. Однако, если мы добавляем условие поиска по городу, например, «Москва», то хотя это условие и кажется избыточным, оно может помочь оптим и­ зировать запрос за счёт улучшения статистических оценок, ускоряя выборку данных.

1

SELECT * FROM users WHERE address = 'Timiryazevsky prospect, 2' AND city = «Moscow»; Такой подход может помочь Postgres более эффекти вно обработать запрос, особенно если данные сильно фрагмен­ тированы или содержат множественные записи с похожими полям и . Тем не менее, добавляя дополнительные поля в фильтры, стоит помнить, что это может не только улуч шить, но и ухуд­ ш ить п роизводител ьность, особенно есл и между полям и есть функциональные зависимости. В таких случаях Postgres может ошибаться в оценке кардинальности, что приведёт к неоптимальному плану выполнения запроса. Чтобы решить эту п роблему, можно воспол ьзоваться ком андой CREATE STATISTICS для сбора расширенной статистики и улуч шения оценки запросов. До бавление индексов

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

73

Во-вто р ы х, необход и м о регул я р н о п роверять и с п ол ьзо­ в а н и е и ндексов. И н декс, кото р ы й а ктивно п р и менялся сегодня, может оказаться невостребованн ы м завтра. Для этого существует специальный запрос к системной таблице pg_stat_all_iпdexes, который п озволяет п роверить, какие и ндексы активно используются, а какие м ожно удалить, чтобы ускорить базу данн ых, особен но на операциях записи и обновления.

1

CREATE I NDEX idx_sale_id ON sales(id); select * from pg_stat_all_indexes where indexrelname='idx_sale_id'; Также помн ите, что большее количество индексов увели­ ч ивает время вы полнения операций обновления и вставки данных. И ндексы, которые содержат несколько колонок, могут занимать значител ьный объём памяти, что замедляет процесс их чтения. Важно уч итывать, что PostgreSQL само­ стоятел ьно оценивает затраты на чтение индекса и может решить не использовать его, если затраты окажутся сл иш­ ком высокими. Таким образом, решение о создании индекса всегда должно быть взвешенн ы м и основываться на текущем состоянии данных и реальных потребностях в производительности. Длинные запросы

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

74

ситуация, когда PostgreSQL начинает использовать диск для свопинга, что знач ительно снижает производительность. Во-вторых, при работе с джойнам и стоит сначала вы пол­ н ить те из н их, которые сокращают количество дан н ы х, а не увел ичивают. В этом помогают такие тип ы джойнов, как semi-joiп (EXISTS / IN / ЕХСЕРТ) и aпti-joiп (NOT EXISTS / NOT IN / INTERSECT). Эти соединения используются для филь­ трации данных, не добавляя дополнительную информацию в результирующий набор, что позволяет оптимизировать выполнение запроса и сократить объем обрабатываемых данных. Оптим изация джойнов и выбор подходящего типа соедине­ ний могут существенно улуч шить производительность за­ п росов, особенно при работе с большими наборами данных. Вот интерес н ы й факт: PostgreSQL а ктивно опти м из и рует операции соединения (джойны). При формировании запроса вы можете заметить, что PostgreSQL может выпол н ить его не в том порядке, в котором вы его написали, сначала обраба­ тывая один джойн, затем другой. Однако такая опти мизация п рекращается, когда п ревы шается п редел сворачивания джойнов Uoiп collapse limit), который по умолчанию равен 8. Рассмотрим небольшой запрос с двумя джойнами. После выполнения команды EXPLAIN мы увидим, что использует­ ся эффективный метод соединения, такой как вложенн ы й цикл (пested loop).

75

EXPLAI N SELECT * FROM projects р JOIN project_dependencies pd ON p.id = pd.project_id WHERE p.id IN (SELECT project_id FROM collection_projects WHERE collection_id = 42) Nested Loop (cost=0.87 .. 1 8.67 rows=1 width=1 91 1 )

Теперь установим значение JoiпCollapselimit равным еди­ ни це, чтобы смоделировать поведение PostgreSQL, и посмо­ трим на результаты. SET join_collapse_limit = 1 ; Hash Join (cost=8521 04.51 . . 5676891 .77 rows=1 width= 1 91 1 )

В этом случае PostgreSQL будет вы полнять операции в строго заданном порядке: сначала вы полняется джойн по всем про­ ектам, а затем происходит фильтрация по идентификатору. Это может привести к нежелател ьным последствиям, если п редел будет п ревы шен, что может негати вно сказаться на производительности запросов. Существует н ес колько м етодов, кото р ы е м о гут п о м оч ь PostgreSQL лучше понимать, где искать статистику, такие как UN ION, INTERSECT и ЕХСЕРТ. Эти операции позволяют и звлекать дан н ы е более эффекти вно, заменяя сложные усл о в ия с O R н а отдель н ы е з а просы с испол ьзован и е м U N ION. Аналогич но, испол ьзование EXISTS и N OT EXI STS может улуч шить производител ьность. П роблема заключается в том, что при наличии множества условий с 'OR' PostgreSQL может потерять эффективность, что п р иводит к н а коплению ошибок в оценках. Исполь­ зуя ' U N I O N', мы п риближаем дан н ы е к исходн ы м стати­ стическим значения м, что может существенно улучшить

76

производител ьность запроса. Поэтому, если вы замечаете, что запросы с подобными условиями работают неэффектив­ но, стоит рассмотреть возможность переработки структуры зап росов и испол ьзования этих операций для опти мизации. Теперь вы сможете увидеть, что такая оптимизация выпол­ няется не из-за отсутствия других вариантов, а потому что она действительно необходима. В этом контексте полезна команда EXPLAIN ANALYZE BUFFERS, которая п редоставляет и н формацию не только о времени выпол нения зап роса, но и о кол и честве п рочита н н ы х стран иц. Это п озволяет оценить, как ч асто и сколько стр а н и ц было обработа н о в процессе выполнения запроса. EXPLAI N (ANALYZE, BUFFERS, COSTS FALSE, TIMING FALSE, SUMMARY FALSE) SELECT * FROM projects WHERE name LIKE 'ivan%' LIMIT 20; Limit (actual rows=20 loops=1 ) Buffers: shared hit=1 5 read=3907 1/0 Timings: read=21 9.956 - > Seq Scan оп projects (actual rows=20 loops=1 ) Filter: ((name):: text -- 'ivan%':: text) Rows Removed Ьу Filter: 1 0033 Buffers: shared hit=1 5 read=3907 1/0 Timings: read=21 9.956 Кроме того, в п р иведенн о м п римере можно отключить несуществен н ы е элементы, что является еще одной по­ лезной функцией. Запрос информации о прочитанных бу­ ферах дает представление о количестве страниц, которые были загружены в память. Например, чтобы получить 20 строк данных, возможно, потребовалось проч итать значи­ тельное количество страниц, что объясняется тем, что мы использовали полное сканирование (seq ueпtial sсап), пока не завершили поиск.

77

Есл и же в вашем запросе несколько раз обра щается к одной и той же таблице, вы сможете выявить избыточные операции чтен ия. Это особенно полезно для анализа и оптимизации производительности запросов. Ра бота с представлениями

Рассмотрим представления (views) как важн ы й инструмент в работе с базами данных. П редставления п редставляют собой заранее подготовленн ы е и сохраненные зап росы, которые обладают высокой ч итабельностью и удобством и спользования. Одной из ключевых особенностей п ред­ ставл е н и й является их воз м ож ность встра и в а н и я . П р и оптимизации запросов планиров щ ик Postgres может инте­ гри ровать представление в основной зап рос, что открывает дополнительные возможности для повышения эффектив­ ности выполнения. Тем не менее, следует отметить, что, хотя Postgres может пе­ реписы вать запросы, разработчики не имеют возможности изменять внутреннее содержание представлений. Например, если необходимо добавить дополнительное условие, такое как 'WHERE id =?', это не удастся реал изовать, поскольку представление ограничивает доступ к своему коду. Таким образом, использование представлений может накладывать оп ределенные ограничения на разработчиков. Тем не менее, они остаются полезн ы м и н струменто м для обеспечения безопасности данных и структурирования запросов, что делает их цен н ы м элементом в разработке приложений. М атериализован н ы е представления

Материал изованные п редставления п редставляют собой более интересный инструмент по сравнению с обы ч н ы м и п редста вления м и, поскольку о н и кэшируют резул ьтаты запросов. Это означает, что резул ьтаты фактически хра­ нятся в базе данных, что позволяет и спользовать стати­ стику и и ндексы, тем сам ы м улуч шая производительность планиров щ ика.

78

Однако стоит уч итывать, что материал изова н н ы е п ред­ ставления не обновляются автоматически; их необходимо периодически обновлять вручную, повторно вы полняя весь запрос. Поэтому их применимость может быть ограничена. Есл и у вас есть данные, которые можно обновлять с оп реде­ ленной периодичностью, материализованные представления окажутся весьма полезны ми . Кроме того, материализованные представления обеспечи­ вают высокую степень корреляции (равную единице), так как при вы полнении запроса и его материализации данные з а п и с ы в а ются без п ро м ежутков и несоответст в и й . Это позволяет эффективно использовать различные индексы и, в целом, упрощает работу с дан н ы м и . Common Та Ы е Expressions

Теперь обратим внимание н а СТЕ (Commoп ТаЫе Expres­ sioпs), которые п редставляют собой удобочитаемые под­ запросы, расположенные перед основным запросом. СТЕ позволя ют задавать н а з в а н и е п одза п роса, к которому можно обращаться в дал ьнейшем. Разработчики особен­ но ценят эту возможность, поскол ьку она позволяет аб­ страгироваться от громоздких запросов и делает их более структурирован н ы м и . WITH sub_query A S ( SELECT * FROM blg_taЫe - ...

) SELECT * FROM sub_query WHERE key = 1 23; С выходом PostgreSQL 1 2 СТЕ стали более безопасн ыми, так как они начали вести себя как подзапросы, интегрируясь в основной зап рос. Ранее, до 1 2-й версии, существовала проблема с тем, что СТЕ выполнялись принудительно зара­ нее, что могло негативно сказаться на производительности .

Кроме того, в СТЕ есть ключевые слова MATERIALIZED и NOT MATERIALIZED, которые позволяют явно указать, следует ли выполнять подзапрос заранее или нет. Хотя возможности их применения могут быть ограничены, использование СТЕ дает разработчикам гибкость в управлении выполнением запросов, позволяя как п ринуждать к п редварительному выполнению, так и откладывать его. WITH sub_query AS NOT MATERIALIZED ( SELECT * FROM blg_taЫe - ...

) SELECT * FROM sub_query AS sq1 JOIN sub_query AS sq2 ON sq1 .key = sq2.ref WHERE sq2.key = 1 23;