Перейти к содержанию

БД:Теория:Глава 6: различия между версиями

Материал из Мадока ВТ Вики
Импорт
 
Статья переписана в соответствии с последней лекцией.
 
Строка 1: Строка 1:
<span id="глава-6-индексы-и-выполнение-запросов"></span>
== Глава 6: Индексы и Выполнение Запросов ==
В предыдущих главах мы научились создавать таблицы, наполнять их данными и извлекать эти данные с помощью SQL-запросов. Теперь разберемся, как СУБД выполняет эти запросы “под капотом” и как можно ускорить их выполнение с помощью '''индексов'''. Эта глава основана на материалах Лекций 6: “Индексы. Выполнение запросов” и Лекции 7: “Выполнение запросов”.
В предыдущих главах мы научились создавать таблицы, наполнять их данными и извлекать эти данные с помощью SQL-запросов. Теперь разберемся, как СУБД выполняет эти запросы “под капотом” и как можно ускорить их выполнение с помощью '''индексов'''. Эта глава основана на материалах Лекций 6: “Индексы. Выполнение запросов” и Лекции 7: “Выполнение запросов”.


Строка 8: Строка 11:
'''Способы повышения производительности:'''
'''Способы повышения производительности:'''


# '''Грамотное проектирование модели данных:''' Это основа. Если модель данных изначально плохо спроектирована (например, ненормализована, что приводит к избыточности, или наоборот, излишне нормализована, что требует множества сложных соединений для простых задач), то оптимизировать запросы будет очень сложно.
# '''Использование индексов:''' Создание дополнительных структур данных, которые позволяют СУБД быстро находить нужные строки по значениям в определенных столбцах, не просматривая всю таблицу. (Основная тема этой главы).
# '''Использование индексов:''' Создание дополнительных структур данных, которые позволяют СУБД быстро находить нужные строки по значениям в определенных столбцах, не просматривая всю таблицу. (Основная тема этой главы).
# '''Оптимизация запросов:''' Переписывание SQL-запросов так, чтобы они были более понятны оптимизатору СУБД и могли быть выполнены более эффективно (например, избегая ненужных соединений или используя <code>EXISTS</code> вместо <code>IN</code> в некоторых случаях).
# '''Оптимизация запросов:''' Переписывание SQL-запросов так, чтобы они были более понятны оптимизатору СУБД и могли быть выполнены более эффективно (например, избегая ненужных соединений или используя <code>EXISTS</code> вместо <code>IN</code> в некоторых случаях).
# '''Настройка физических параметров СУБД:''' Оптимизация конфигурации сервера PostgreSQL (выделение памяти, настройка параллелизма, параметры ввода-вывода и т.д.).
# '''Настройка физических параметров СУБД:''' Оптимизация конфигурации сервера PostgreSQL (выделение памяти, настройка параллелизма, параметры ввода-вывода и т.д.).
# '''Денормализация:''' (Рассматривали ранее) Иногда используется для ускорения чтения за счет избыточности.
# '''Партиционирование таблиц:''' Разделение очень больших таблиц на физически отдельные части (партиции) для ускорения доступа к конкретным диапазонам данных.


Мы сосредоточимся на '''индексах''' и понимании '''выполнения запросов'''.
Мы сосредоточимся на '''индексах''' и понимании '''выполнения запросов'''.
Строка 27: Строка 29:
* Это отдельная структура данных (обычно хранящаяся в отдельном файле), связанная с таблицей.
* Это отдельная структура данных (обычно хранящаяся в отдельном файле), связанная с таблицей.
* Он содержит '''копии значений''' из одного или нескольких столбцов таблицы.
* Он содержит '''копии значений''' из одного или нескольких столбцов таблицы.
* Эти значения в индексе '''упорядочены''' определенным образом (например, по алфавиту для строк, по возрастанию для чисел).
* Эти значения в индексе '''упорядочены''' определенным образом (например, по алфавиту для строк, по возрастанию для чисел, или организованы в специфическую структуру данных типа хеш-таблицы).
* Каждое значение в индексе имеет '''указатель''' (ссылку, адрес) на те строки в основной таблице, где это значение встречается.
* Каждое значение в индексе имеет '''указатель''' (ссылку, адрес, TID - Tuple Identifier) на те строки в основной таблице, где это значение встречается.


'''Как индекс помогает?'''
'''Как индекс помогает?'''


Рассмотрим запрос:
Рассмотрим таблицу <code>STUDENT</code>:


<syntaxhighlight lang="sql">SELECT * FROM student WHERE StudID = 18;</syntaxhighlight>
{| class="wikitable"
* '''Без индекса:''' СУБД читает ''каждую'' строку таблицы <code>student</code> и сравнивает значение в колонке <code>StudID</code> с числом 18. Если таблица большая, это долго.
|-
! style="text-align: left;"| StudID
! style="text-align: left;"| GroupID
! style="text-align: left;"| Name
|-
| style="text-align: left;"| 14
| style="text-align: left;"| 51
| style="text-align: left;"| Ivan Petrov
|-
| style="text-align: left;"| 23
| style="text-align: left;"| 53
| style="text-align: left;"| Vladimir Ivanov
|-
| style="text-align: left;"| 18
| style="text-align: left;"| 54
| style="text-align: left;"| Eugene Serov
|-
| style="text-align: left;"| 13
| style="text-align: left;"| 55
| style="text-align: left;"| Gennady Surikov
|-
| style="text-align: left;"| 25
| style="text-align: left;"| 56
| style="text-align: left;"| Nikolay Petrov
|-
| style="text-align: left;"| 25
| style="text-align: left;"| 58
| style="text-align: left;"| Svetlana Ivanova
|-
| style="text-align: left;"| 25
| style="text-align: left;"| 59
| style="text-align: left;"| Antony Fedorov
|-
| style="text-align: left;"| 18
| style="text-align: left;"| 63
| style="text-align: left;"| Petr Menchikov
|-
| style="text-align: left;"| 27
| style="text-align: left;"| 67
| style="text-align: left;"| Gennady Klokov
|-
| style="text-align: left;"| 23
| style="text-align: left;"| 69
| style="text-align: left;"| Serge Vidov
|-
| style="text-align: left;"| 18
| style="text-align: left;"| 72
| style="text-align: left;"| Roman Klever
|-
| style="text-align: left;"| 13
| style="text-align: left;"| 73
| style="text-align: left;"| Pavel Borisov
|}
 
И запрос:
 
<syntaxhighlight lang="sql">SELECT * FROM STUDENT WHERE StudID = 18;</syntaxhighlight>
* '''Без индекса:''' СУБД читает ''каждую'' строку таблицы <code>STUDENT</code> и сравнивает значение в колонке <code>StudID</code> с числом 18. Если таблица большая (миллионы строк), это займет много времени, так как СУБД должна прочитать все данные с диска в память и проверить каждую строку.
* '''С индексом по <code>StudID</code>:'''
* '''С индексом по <code>StudID</code>:'''
*# СУБД обращается к индексу по <code>StudID</code>.
*# СУБД обращается к структуре индекса по <code>StudID</code>.
*# Так как значения в индексе упорядочены, СУБД '''быстро''' находит значение 18 (например, используя бинарный поиск или структуру B-дерева).
*# Если индекс, например, B-дерево (значения отсортированы), СУБД '''быстро''' находит значение 18 (например, используя бинарный поиск или навигацию по дереву).
*# Из индекса СУБД получает указатели на строки в основной таблице, где <code>StudID = 18</code>.
*# Из индекса СУБД получает указатели (TID) на строки в основной таблице, где <code>StudID = 18</code>.
*# СУБД читает '''только''' эти конкретные строки из основной таблицы.
*# СУБД читает '''только''' эти конкретные строки из основной таблицы, используя полученные указатели. Это значительно быстрее, чем читать всю таблицу.
 
'''Пример (слайды 10-17, упрощенная иллюстрация):'''
 
Допустим, у нас есть упорядоченный список значений <code>StudID</code> (это и есть наш “игрушечный” индекс) и стрелки, указывающие на строки в основной таблице.
 
{| class="wikitable"
|-
! style="text-align: left;"| ''Индекс (упорядоченный StudID)''
! style="text-align: left;"| ''Таблица STUDENT (StudID, GroupID, Name)''
|-
| style="text-align: left;"| <code>13</code> →
| style="text-align: left;"| <code>14, 51, Ivan Petrov</code>
|-
| style="text-align: left;"| <code>13</code> →
| style="text-align: left;"| <code>23, 53, Vladimir Ivanov</code>
|-
| style="text-align: left;"| <code>14</code> →
| style="text-align: left;"| <code>18, 54, Eugene Serov</code> &lt;– ''Нужная строка''
|-
| style="text-align: left;"| <code>18</code> →
| style="text-align: left;"| <code>13, 55, Gennady Surikov</code>
|-
| style="text-align: left;"| <code>18</code> →
| style="text-align: left;"| <code>25, 56, Nikolay Petrov</code>
|-
| style="text-align: left;"| <code>18</code> →
| style="text-align: left;"| <code>25, 58, Svetlana Ivanova</code>
|-
| style="text-align: left;"| <code>23</code> →
| style="text-align: left;"| <code>25, 59, Antony Fedorov</code>
|-
| style="text-align: left;"| <code>23</code> →
| style="text-align: left;"| <code>18, 63, Petr Menchikov</code> &lt;– ''Нужная строка''
|-
| style="text-align: left;"| <code>25</code> →
| style="text-align: left;"| <code>27, 67, Gennady Klokov</code>
|-
| style="text-align: left;"| <code>25</code> →
| style="text-align: left;"| <code>23, 69, Serge Vidov</code>
|-
| style="text-align: left;"| <code>25</code> →
| style="text-align: left;"| <code>18, 72, Roman Klever</code> &lt;– ''Нужная строка''
|-
| style="text-align: left;"| <code>27</code> →
| style="text-align: left;"| <code>13, 73, Pavel Borisov</code>
|}


'''Пример (из слайдов 10-17):'''
# '''Запрос:''' <code>SELECT * FROM STUDENT WHERE StudID = 18;</code>
# СУБД смотрит в ''индекс''.
# Поскольку индекс отсортирован (в нашем примере это простой список, но представьте более сложную структуру), СУБД быстро находит первое вхождение <code>18</code>.
# Берет указатель и читает строку <code>18, 54, Eugene Serov</code>.
# Переходит к следующему значению <code>18</code> в индексе.
# Берет указатель и читает строку <code>18, 63, Petr Menchikov</code>.
# Переходит к следующему значению <code>18</code> в индексе.
# Берет указатель и читает строку <code>18, 72, Roman Klever</code>.
# Следующее значение в индексе уже <code>23</code> (или больше 18). Поиск завершен.


* Есть таблица <code>STUDENT</code>.
Вместо сканирования всех 12 строк таблицы, мы быстро нашли 3 нужные строки через индекс.
* Создан индекс по колонке <code>StudID</code>. Индекс содержит значения <code>StudID</code> и указатели на строки.
* Запрос: <code>SELECT * FROM STUDENT WHERE StudID = 18;</code>
* СУБД смотрит в индекс, быстро находит записи со значением 18.
* Получает из индекса указатели на строки с ID=18 (в примере это строки с Eugene Serov, Petr Menchikov, Roman Klever).
* Читает только эти три строки из таблицы <code>STUDENT</code>.


'''Важные моменты:'''
'''Важные моменты:'''


* Индексы работают '''неявно''': Вам не нужно указывать в <code>SELECT</code>-запросе, какой индекс использовать. Оптимизатор СУБД сам решает, использовать ли индекс и какой именно, на основе статистики данных и стоимости выполнения разных планов.
* Индексы работают '''неявно''': Вам не нужно указывать в <code>SELECT</code>-запросе, какой индекс использовать. Оптимизатор СУБД сам решает, использовать ли индекс и какой именно, на основе статистики данных и стоимости выполнения разных планов.
* Иногда СУБД может '''не использовать''' индекс, даже если он есть. Например, если запрос выбирает очень большую часть таблицы (проще прочитать всю таблицу), или если условия в <code>WHERE</code> таковы, что индекс неприменим (например, функция над индексированным столбцом).
* Иногда СУБД может '''не использовать''' индекс, даже если он есть. Например, если запрос выбирает очень большую часть таблицы (проще прочитать всю таблицу), или если условия в <code>WHERE</code> таковы, что индекс неприменим (например, функция над индексированным столбцом <code>WHERE upper(StudName) = 'ИВАНОВ'</code>).


'''Когда Индексы Полезны?'''
'''Когда Индексы Полезны?'''
Строка 62: Строка 170:
* '''Поиск строк по условиям (<code>WHERE</code>)''': Особенно для условий равенства (<code>=</code>), диапазонных запросов (<code>&gt;</code>, <code>&lt;</code>, <code>BETWEEN</code>) по индексированному столбцу.
* '''Поиск строк по условиям (<code>WHERE</code>)''': Особенно для условий равенства (<code>=</code>), диапазонных запросов (<code>&gt;</code>, <code>&lt;</code>, <code>BETWEEN</code>) по индексированному столбцу.
* '''Соединения таблиц (<code>JOIN</code>)''': Индексы по столбцам, участвующим в условии <code>ON</code>, значительно ускоряют поиск совпадающих строк в соединяемых таблицах. Первичные и уникальные ключи автоматически индексируются в PostgreSQL, что ускоряет соединения по ним.
* '''Соединения таблиц (<code>JOIN</code>)''': Индексы по столбцам, участвующим в условии <code>ON</code>, значительно ускоряют поиск совпадающих строк в соединяемых таблицах. Первичные и уникальные ключи автоматически индексируются в PostgreSQL, что ускоряет соединения по ним.
* '''Поиск минимального/максимального значения (<code>MIN()</code>, <code>MAX()</code>):''' Если столбец индексирован, СУБД может просто взять первое/последнее значение из индекса, не сканируя таблицу.
* '''Поиск минимального/максимального значения (<code>MIN()</code>, <code>MAX()</code>):''' Если столбец индексирован (например, B-деревом), СУБД может просто взять первое/последнее значение из индекса, не сканируя таблицу.
* '''Сортировку (<code>ORDER BY</code>)''': Если порядок сортировки совпадает с порядком индекса, СУБД может избежать фактической сортировки данных, просто прочитав строки в порядке индекса.
* '''Сортировку (<code>ORDER BY</code>)''': Если порядок сортировки совпадает с порядком индекса (или обратен ему, и индекс это поддерживает), СУБД может избежать фактической сортировки данных, просто прочитав строки в порядке индекса.
* '''Группировку (<code>GROUP BY</code>)''': Индексы могут помочь СУБД быстрее найти строки, относящиеся к одной группе.
* '''Группировку (<code>GROUP BY</code>)''': Индексы могут помочь СУБД быстрее найти строки, относящиеся к одной группе.


'''Недостатки Индексов:'''
'''Недостатки Индексов:'''


# '''Занимают место:''' Индекс — это дополнительная структура данных, которая хранится на диске и занимает место (иногда сравнимое с размером самой таблицы).
# '''Занимают место на диске и в памяти:''' Индекс — это дополнительная структура данных. Чем больше индексов, тем больше места они занимают.
# '''Замедляют операции записи (<code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code>):''' При изменении данных в индексированном столбце или добавлении/удалении строки СУБД должна обновить не только саму таблицу, но и '''все''' связанные с ней индексы. Это добавляет накладные расходы. Чем больше индексов на таблице, тем медленнее будут операции записи.
# '''Замедляют операции записи (<code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code>):''' При изменении данных в индексированном столбце или добавлении/удалении строки СУБД должна обновить не только саму таблицу, но и '''все''' связанные с ней индексы. Это добавляет накладные расходы. Чем больше индексов на таблице, тем медленнее будут операции записи.
# '''Неэффективны на маленьких таблицах:''' Если таблица настолько мала, что целиком помещается в память, СУБД прочитает ее полностью быстрее, чем будет обращаться к индексу.
#* ''Пример (слайд 23-24):'' <code>DELETE FROM STUDENT WHERE GroupID = 54;</code> Если по <code>GroupID</code> есть индекс, то СУБД должна не только удалить строку из таблицы <code>STUDENT</code>, но и удалить соответствующие записи из индекса по <code>GroupID</code>, а также, возможно, из индекса по <code>StudID</code> (если <code>StudID</code> был ключом этого индекса и строка удаляется).
# '''Неэффективны при выборке больших объемов данных:''' Если запрос выбирает значительную часть таблицы (например, <code>WHERE column &gt; 0</code>, где почти все значения больше нуля), СУБД, скорее всего, решит, что проще прочитать всю таблицу последовательно (<code>Seq Scan</code>), чем много раз обращаться к индексу и затем к разным частям таблицы за строками (<code>Index Scan</code> + много чтений с диска).
# '''Неэффективны на маленьких таблицах:''' Если таблица настолько мала, что целиком помещается в несколько блоков памяти, СУБД прочитает ее полностью быстрее, чем будет обращаться к индексу.
# '''Неэффективны при выборке больших объемов данных:''' Если запрос выбирает значительную часть таблицы (например, <code>WHERE column &gt; 0</code>, где почти все значения больше нуля), СУБД, скорее всего, решит, что проще прочитать всю таблицу последовательно (<code>Seq Scan</code>), чем много раз обращаться к индексу и затем к разным частям таблицы за строками (<code>Index Scan</code> + много случайных чтений с диска, что медленнее последовательного).


'''Стратегии Применения Индексов:'''
'''Стратегии Применения Индексов:'''


* '''Анализируйте запросы:''' Какие столбцы часто используются в <code>WHERE</code> и <code>JOIN</code>? Какие операции преобладают — чтение или запись?
* '''Анализируйте запросы:''' Какие столбцы часто используются в <code>WHERE</code> и <code>JOIN</code>? Какие операции преобладают — чтение или запись?
* '''Индексируйте столбцы в <code>WHERE</code>:''' Особенно те, по которым идет поиск на равенство или по диапазону и которые обладают хорошей '''селективностью''' (т.е. по значению в этом столбце можно отобрать небольшую долю строк таблицы). Индексировать столбец “пол” (М/Ж) обычно бессмысленно, так как каждое значение выбирает примерно половину таблицы.
* '''Индексируйте столбцы в <code>WHERE</code>:''' Особенно те, по которым идет поиск на равенство или по диапазону и которые обладают хорошей '''селективностью''' (т.е. по значению в этом столбце можно отобрать небольшую долю строк таблицы). Индексировать столбец “пол” (М/Ж), где всего два значения, обычно бессмысленно, так как каждое значение выбирает примерно половину таблицы.
* '''Индексируйте столбцы внешних ключей:''' Это критически важно для ускорения <code>JOIN</code>.
* '''Индексируйте столбцы внешних ключей:''' Это критически важно для ускорения <code>JOIN</code>. PostgreSQL ''не'' создает их автоматически для внешних ключей (в отличие от первичных/уникальных), поэтому это нужно делать вручную.
* '''Индексируйте столбцы в <code>ORDER BY</code>:''' Если часто нужна сортировка по определенному столбцу.
* '''Индексируйте столбцы в <code>ORDER BY</code> и <code>GROUP BY</code>:''' Если часто нужна сортировка или группировка по определенному столбцу.
* '''Используйте составные индексы:''' Если часто ищете по комбинации столбцов (<code>WHERE col1 = ? AND col2 = ?</code>), создайте индекс по <code>(col1, col2)</code>. Порядок столбцов в составном индексе важен! Индекс по <code>(col1, col2)</code> может использоваться для запросов по <code>col1</code> и для запросов по <code>col1</code> и <code>col2</code>, но ''не'' для запросов только по <code>col2</code>.
* '''Используйте составные индексы:''' Если часто ищете по комбинации столбцов (<code>WHERE col1 = ? AND col2 = ?</code>), создайте индекс по <code>(col1, col2)</code>. Порядок столбцов в составном индексе важен! Индекс по <code>(col1, col2)</code> может использоваться для запросов по <code>col1</code> и для запросов по <code>col1</code> И <code>col2</code>, но ''не'' для запросов только по <code>col2</code>.
* '''Не создавайте лишних индексов:''' Каждый индекс замедляет запись. Удаляйте неиспользуемые индексы.
* '''Не создавайте лишних индексов:''' Каждый индекс замедляет запись. Регулярно проверяйте и удаляйте неиспользуемые индексы.
* '''Первичные и уникальные ключи индексируются автоматически.'''
* '''Первичные и уникальные ключи индексируются автоматически''' в PostgreSQL (с помощью B-Tree индекса).


'''Создание Индексов (<code>CREATE INDEX</code>)'''
'''Создание Индексов (<code>CREATE INDEX</code>)'''


<syntaxhighlight lang="sql">CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] имя_индекса
<syntaxhighlight lang="sql">CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ IF NOT EXISTS ] имя_индекса
ON имя_таблицы [ USING метод ]
ON имя_таблицы [ USING метод ]
( { имя_колонки | ( выражение ) } [ ASC | DESC ] [ NULLS { FIRST | LAST } ] [, ...] );</syntaxhighlight>
( { имя_колонки | ( выражение ) } [ COLLATE имя_сортировки ] [ класс_операторов ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ] [, ...] )
* <code>UNIQUE</code>: Создает уникальный индекс (запрещает дубликаты значений в индексируемых столбцах, кроме <code>NULL</code>).
[ WITH ( параметр_хранения = значение [, ... ] ) ]
* <code>CONCURRENTLY</code>: (PostgreSQL) Позволяет создать индекс без блокировки операций записи в таблицу (полезно для больших таблиц на “живой” системе, но выполняется дольше).
[ TABLESPACE имя_табличного_пространства ]
[ WHERE предикат ]; -- Частичный индекс</syntaxhighlight>
''Полный синтаксис сложнее, здесь приведены основные части.''
 
* <code>UNIQUE</code>: Создает уникальный индекс.
* <code>CONCURRENTLY</code>: (PostgreSQL) Позволяет создать индекс без длительной блокировки операций записи в таблицу.
* <code>IF NOT EXISTS</code>: Не выдавать ошибку, если индекс с таким именем уже существует.
* <code>имя_индекса</code>: Имя для вашего индекса.
* <code>имя_индекса</code>: Имя для вашего индекса.
* <code>имя_таблицы</code>: Таблица, для которой создается индекс.
* <code>имя_таблицы</code>: Таблица, для которой создается индекс.
* <code>USING метод</code>: Указывает тип индекса (метод доступа). По умолчанию в PostgreSQL это <code>btree</code>. Другие варианты: <code>hash</code>, <code>gist</code>, <code>gin</code>, <code>spgist</code>, <code>brin</code>.
* <code>USING метод</code>: Указывает тип индекса (<code>btree</code>, <code>hash</code>, <code>gist</code>, <code>gin</code> и др.). По умолчанию в PostgreSQL это <code>btree</code>.
* <code>( имя_колонки | ( выражение ) ... )</code>: Список столбцов или выражений, по которым строится индекс.
* <code>( имя_колонки | ( выражение ) ... )</code>: Список столбцов или выражений, по которым строится индекс.
* <code>ASC | DESC</code>: Порядок сортировки для индекса (по умолчанию <code>ASC</code>).
* <code>ASC | DESC</code>: Порядок сортировки (для B-Tree).
* <code>NULLS FIRST | NULLS LAST</code>: Где размещать <code>NULL</code>-значения в индексе (по умолчанию <code>NULLS LAST</code> для <code>ASC</code> и <code>NULLS FIRST</code> для <code>DESC</code>).
* <code>NULLS FIRST | NULLS LAST</code>: Где размещать <code>NULL</code>-значения.
* <code>WHERE предикат</code>: Создает '''частичный индекс'''. Индексируются только строки, удовлетворяющие предикату. Полезно, если часто ищутся строки с определенным значением в одном столбце, а по другому нужно индексировать только подмножество.


'''Примеры:'''
'''Примеры:'''


<syntaxhighlight lang="sql">-- Простой индекс по одной колонке (используется B-Tree по умолчанию)
<syntaxhighlight lang="sql">-- Простой B-Tree индекс по одной колонке
CREATE INDEX idx_student_groupid ON student (GroupID);
CREATE INDEX idx_student_groupid ON student (GroupID);


-- Уникальный индекс
-- Составной B-Tree индекс по двум колонкам
CREATE UNIQUE INDEX idx_student_email ON student (email);
 
-- Составной индекс по двум колонкам
CREATE INDEX idx_student_surname_name ON student (Surname, Name);
CREATE INDEX idx_student_surname_name ON student (Surname, Name);


-- Индекс по выражению (например, по фамилии в нижнем регистре для регистронезависимого поиска)
-- Индекс по выражению (по фамилии в нижнем регистре)
CREATE INDEX idx_student_surname_lower ON student (lower(Surname));
CREATE INDEX idx_student_surname_lower ON student (lower(Surname));


-- Хеш-индекс (только для равенства '=')
-- Хеш-индекс (только для оператора равенства '=')
CREATE INDEX idx_student_name_hash ON student USING hash (Name);</syntaxhighlight>
CREATE INDEX idx_student_name_hash ON student USING hash (Name);
 
-- Пример из слайда 29 (B-Tree по двум колонкам)
CREATE INDEX idx_example ON table_name USING btree (column1, column2);</syntaxhighlight>
<span id="часть-3-типы-индексов"></span>
<span id="часть-3-типы-индексов"></span>
=== Часть 3: Типы Индексов ===
=== Часть 3: Типы Индексов ===
Строка 119: Строка 235:


# '''B-дерево (B-Tree):'''
# '''B-дерево (B-Tree):'''
#* Используется '''по умолчанию'''.
#* Используется '''по умолчанию''' (<code>USING btree</code>).
#* '''Структура:''' Сбалансированное, сильно ветвистое дерево поиска. Состоит из корневого узла, промежуточных узлов и листовых узлов. Листовые узлы содержат индексируемые значения и указатели (TID - Tuple Identifier, адрес строки на диске) на строки таблицы, отсортированные по индексируемому значению. Внутренние узлы содержат “разделители” и ссылки на дочерние узлы для быстрой навигации по дереву.
#* '''Структура:''' Сбалансированное, многоуровневое дерево.
#** '''Листовые узлы:''' Содержат индексируемые значения (ключи) и указатели (TID - Tuple Identifier, адрес строки на диске) на строки таблицы. Ключи в листовых узлах отсортированы. Листовые узлы связаны между собой для быстрого последовательного прохода (полезно для диапазонных запросов).
#** '''Внутренние узлы (включая корневой):''' Содержат “разделители” (ключи) и ссылки на дочерние узлы. Помогают быстро сузить область поиска.
#* '''Преимущества:''' Очень универсальный. Эффективен для:
#* '''Преимущества:''' Очень универсальный. Эффективен для:
#** Поиска по равенству (<code>=</code>).
#** Поиска по равенству (<code>=</code>).
#** Диапазонных запросов (<code>&gt;</code>, <code>&lt;</code>, <code>BETWEEN</code>).
#** Диапазонных запросов (<code>&gt;</code>, <code>&lt;</code>, <code>BETWEEN</code>, <code>LIKE 'abc%'</code> (поиск по префиксу)).
#** Сортировки (<code>ORDER BY</code>).
#** Сортировки (<code>ORDER BY</code>).
#** Поиска <code>NULL</code> (<code>IS NULL</code>, <code>IS NOT NULL</code>).
#** Поиска <code>NULL</code> (<code>IS NULL</code>, <code>IS NOT NULL</code>).
#** Частичного поиска по префиксу (<code>LIKE 'abc%'</code>).
#** Работы с <code>MIN()</code> и <code>MAX()</code>.
#** Работы с <code>MIN()</code> и <code>MAX()</code>.
#* '''Пример поиска (StudID = 85):''' СУБД спускается от корня B-дерева, сравнивая 85 с ключами в узлах, быстро находит нужный листовой узел, а в нем — запись с ключом 85 и указателем на соответствующую строку в таблице. Для диапазона <code>&gt;= 85</code> находится первый узел с 85, а затем последовательно просматриваются следующие листовые узлы.
#* '''Пример поиска (StudID = 85, слайды 36-41):'''
#*# Запрос: <code>SELECT * FROM STUDENT WHERE StudID = 85;</code>
#*# Начинаем с корневого узла (например, <code>[3, 30, 63]</code>).
#*# <code>85 &gt; 63</code>, идем по правой ветке к узлу (например, <code>[63, 76, 88]</code>).
#*# <code>76 &lt; 85 &lt;= 88</code>, идем по ветке, соответствующей диапазону (88), к листовому узлу (например, содержащему ключи <code>[76(tid), 85(tid)]</code>).
#*# В листовом узле находим ключ <code>85</code> и соответствующий <code>tid</code>.
#*# По <code>tid</code> читаем строку из таблицы.
#* '''Пример диапазонного поиска (StudID &gt;= 85, слайды 42-43):'''
#*# Аналогично находим первый ключ <code>85</code>.
#*# Далее, благодаря связям между листовыми узлами, последовательно читаем следующие листовые узлы (<code>[88(tid), 95(tid)]</code> и т.д.), пока условие выполняется.
# '''Хеш-индекс (Hash):'''
# '''Хеш-индекс (Hash):'''
#* '''Структура:''' Основан на хеш-таблице. Для индексируемого значения вычисляется хеш-код, который используется для определения “корзины” (bucket), где хранятся указатели на строки с этим значением.
#* '''Структура:''' Основан на хеш-таблице. Для индексируемого значения вычисляется хеш-код, который используется как индекс в массиве “корзин” (buckets). Каждая корзина содержит указатели на строки таблицы, чьи индексируемые значения дали этот хеш-код.
#* '''Пример (слайды 46-49):'''
#** Значения: <code>(23, 2)</code>, <code>(34, 2)</code>, <code>(34543, 4)</code>, <code>(234, 4)</code>, <code>(2355, 5)</code>
#** Хеш-функция <code>Hash_Function(val1, val2)</code> дает, например:
#*** <code>Hash_Function(23, 2)</code> -&gt; <code>...2345</code> (хэш значение)
#*** <code>Hash_Function(34, 2)</code> -&gt; <code>...3423</code>
#*** <code>Hash_Function(34543, 4)</code> -&gt; <code>...2345</code> (коллизия с первым!)
#** Хэш-индекс:
#*** Корзина <code>2345</code>: <code>tid1</code> (для <code>(23,2)</code>), <code>tid3</code> (для <code>(34543,4)</code>)
#*** Корзина <code>3423</code>: <code>tid2</code> (для <code>(34,2)</code>)
#** При поиске, например, <code>WHERE StudID=23 AND GroupID=2</code>:
#**# Вычисляется <code>Hash_Function(23, 2)</code> -&gt; <code>...2345</code>.
#**# Смотрим в корзину <code>2345</code>.
#**# Для каждой строки (<code>tid1</code>, <code>tid3</code>) в этой корзине проверяем точное совпадение исходных значений <code>StudID</code> и <code>GroupID</code> (т.к. в корзине могут быть коллизии).
#* '''Преимущества:''' Очень быстрый для операций поиска по '''точному равенству (<code>=</code>)'''.
#* '''Преимущества:''' Очень быстрый для операций поиска по '''точному равенству (<code>=</code>)'''.
#* '''Недостатки:'''
#* '''Недостатки:'''
#** '''Бесполезен''' для диапазонных запросов (<code>&gt;</code>, <code>&lt;</code>), сортировки (<code>ORDER BY</code>), поиска по префиксу (<code>LIKE 'abc%'</code>). Хеш-коды не сохраняют порядок значений.
#** '''Бесполезен''' для диапазонных запросов, сортировки, поиска по префиксу.
#** Требует больше дискового пространства при увеличении числа записей (хеш-таблица может расширяться).
#** Менее эффективен при большом количестве коллизий (когда много разных значений попадают в одну корзину).
#** До PostgreSQL 10 хеш-индексы не логировались в WAL, что делало их ненадежными после сбоев (требовалась переиндексация). Сейчас эта проблема решена.
#* '''Когда использовать:''' Только если запросы к столбцу — это исключительно проверки на точное равенство (<code>WHERE attr = 1</code>), и другие операции не нужны. B-Tree обычно является более гибким выбором.
#* '''Когда использовать:''' Редко. Только если запросы к столбцу — это исключительно проверки на точное равенство, и B-дерево по какой-то причине не устраивает.
# '''GiST (Generalized Search Tree - Обобщенное дерево поиска):'''
# '''GiST (Generalized Search Tree):'''
#* Гибкая инфраструктура для создания индексов для сложных типов данных (геометрические данные, диапазоны, некоторые виды текстового поиска).
#* Обобщенное дерево поиска. Каркас для построения индексов для сложных типов данных (геометрические данные, текстовый поиск, диапазоны и т.д.).
#* Позволяет индексировать операции типа “пересекается”, “содержит”.
#* Позволяет индексировать операции типа “пересекается”, “содержит”, “находится внутри”.
#* Используется расширениями, такими как PostGIS (для геоданных) и <code>pg_trgm</code> (для поиска по похожести строк - триграммам).
#* Используется расширениями, такими как PostGIS (для геоданных) и <code>pg_trgm</code> (для поиска по похожести строк).
# '''GIN (Generalized Inverted Index - Обобщенный инвертированный индекс):'''
# '''GIN (Generalized Inverted Index):'''
#* Оптимизирован для индексации '''составных''' значений, где одно значение в столбце может содержать несколько “ключей” (например, массив слов в документе, элементы массива <code>jsonb</code>, элементы <code>hstore</code>).
#* Обобщенный инвертированный индекс. Оптимизирован для индексации '''составных''' значений, где одно значение в столбце может содержать несколько “ключей” (например, массив слов в документе, элементы массива <code>jsonb</code>).
#* Хранит карту “ключ из составного значения -&gt; список строк, где этот ключ встречается”.
#* Хранит карту “ключ -&gt; список строк, где ключ встречается”.
#* '''Преимущества:''' Очень эффективен для поиска строк, содержащих определенные значения ''внутри'' составного типа (например, найти все документы, содержащие слово ‘postgresql’).
#* '''Преимущества:''' Очень эффективен для поиска строк, содержащих определенные значения внутри составного типа (например, найти все документы, содержащие слово ‘postgresql’, или все записи, где массив содержит число 5).
#* Используется для полнотекстового поиска, индексации <code>jsonb</code> (операторы <code>?</code>, <code>?|</code>, <code>?&amp;</code>), <code>hstore</code>, массивов (операторы <code>@&gt;</code>, <code>&lt;@</code>, <code>&amp;&amp;</code>).
#* Используется для полнотекстового поиска, индексации <code>jsonb</code>, <code>hstore</code>, массивов.
# '''SP-GiST (Space-Partitioned GiST):''' Для не сбалансированных структур данных (например, префиксные деревья).
# '''BRIN (Block Range Indexes):''' Хранит ''сводную'' информацию (минимум, максимум) для больших диапазонов физических блоков таблицы. Очень компактный, но эффективен только для данных, которые сильно коррелируют с их физическим расположением в таблице (например, даты или ID в таблице, куда данные только добавляются).


<span id="часть-4-выполнение-запросов-и-оптимизация"></span>
<span id="часть-4-выполнение-запросов-и-оптимизация"></span>
=== Часть 4: Выполнение Запросов и Оптимизация ===
=== Часть 4: Выполнение Запросов и Оптимизация ===


Как СУБД (на примере PostgreSQL) выполняет SQL-запрос:
(Этот раздел повторяет и немного расширяет информацию из предыдущей версии методички, основываясь на слайдах Лекции 7).


# '''Разбор запроса (Parser):''' Текст SQL-запроса преобразуется во внутреннее представление — '''дерево разбора (parse tree)'''. Проверяется синтаксис.
'''Декларативность SQL:''' Пользователь указывает, ''что'' он хочет получить, а не ''как'' это сделать. СУБД сама решает, как выполнить запрос.
# '''Преобразование запроса (Rewriter):''' Дерево разбора преобразуется с учетом правил и представлений (например, запрос к представлению заменяется на запрос к базовым таблицам). Результат — '''дерево запроса (query tree)'''.
 
# '''Планировщик/Оптимизатор (Planner/Optimizer):''' Самый сложный этап.
'''Этапы выполнения запроса в PostgreSQL:'''
#* Генерирует '''множество возможных планов выполнения''' для дерева запроса, используя правила реляционной алгебры и информацию об имеющихся индексах и алгоритмах выполнения операций (Seq Scan, Index Scan, Nested Loop Join, Hash Join, Merge Join, Sort, Aggregate и т.д.).
 
#* '''Оценивает стоимость''' каждого плана (на основе статистики таблиц: количество строк, распределение значений, размер таблицы и т.д.). Стоимость обычно измеряется в условных единицах, отражающих дисковый ввод-вывод и использование CPU.
# '''Разбор запроса (Parser):''' SQL-текст преобразуется во внутреннее представление — '''дерево разбора (parse tree)'''. Проверяется синтаксис.
# '''Преобразование запроса (Rewriter/Analyzer):''' Дерево разбора анализируется, преобразуется с учетом правил и представлений (например, запрос к представлению заменяется на запрос к базовым таблицам). Семантическая проверка. Результат — '''дерево запроса (query tree)'''.
# '''Планировщик/Оптимизатор (Planner/Optimizer):'''
#* Генерирует множество возможных '''планов выполнения''' для дерева запроса, используя:
#** Законы реляционной алгебры для эквивалентных преобразований.
#** Информацию об имеющихся индексах.
#** Доступные алгоритмы выполнения операций (Seq Scan, Index Scan, Nested Loop Join, Hash Join, Merge Join, Sort, Aggregate и т.д.).
#* '''Оценивает стоимость''' каждого плана на основе статистики таблиц (количество строк, распределение значений, размер таблицы, кардинальность и т.д.) и стоимости операций (I/O, CPU).
#* Выбирает план с '''минимальной оцененной стоимостью'''.
#* Выбирает план с '''минимальной оцененной стоимостью'''.
#* Результат — '''план выполнения (execution plan)'''.
#* Результат — '''план выполнения (execution plan)'''.
# '''Выполнение плана (Executor):''' План выполнения передается исполнителю, который шаг за шагом выполняет операции плана (чтение данных, соединение, фильтрацию, сортировку и т.д.) и возвращает результат пользователю.
# '''Выполнение плана (Executor):''' План выполнения передается исполнителю, который шаг за шагом выполняет операции плана и возвращает результат.


'''Оптимизация и Реляционная Алгебра:'''
'''План выполнения запроса:'''


Оптимизатор активно использует законы реляционной алгебры для генерации эквивалентных планов. Ключевые идеи оптимизации:
* Это, по сути, программа, которую СУБД строит для ответа на SQL-запрос.
* Для одного SQL-запроса может существовать несколько ''эквивалентных'' планов, но с разной ''стоимостью'' выполнения.
* '''Критерий выбора плана:''' Оценочная стоимость (число обменов с диском, время обмена, загрузка CPU).


* '''Выполнять выборку (<code>WHERE</code>) как можно раньше:''' Уменьшает количество строк для последующих операций (особенно для <code>JOIN</code>). Оптимизатор “проталкивает” условия <code>WHERE</code> вниз по дереву плана.
'''Оптимизация на основе Реляционной Алгебры:'''
* '''Выполнять проекцию (<code>SELECT</code>-список) как можно раньше:''' Уменьшает количество и размер данных, передаваемых между операциями (особенно если используется конвейерная обработка).
* '''Выбирать оптимальный порядок и алгоритм соединений:''' Порядок соединения таблиц (<code>A JOIN B JOIN C</code> vs <code>A JOIN C JOIN B</code>) и алгоритм (<code>Nested Loop</code>, <code>Hash Join</code>, <code>Merge Join</code>) сильно влияют на производительность. Оптимизатор оценивает разные варианты.


'''Материализация vs Конвейерная Обработка (Pipelining):'''
Оптимизатор использует законы реляционной алгебры (коммутативность, ассоциативность соединений, проталкивание выборок и проекций и т.д.) для генерации различных, но семантически эквивалентных планов.


* '''Материализация:''' Результат одной операции (например, <code>JOIN</code>) полностью вычисляется и сохраняется во временную структуру (в памяти или на диске), а затем передается следующей операции. Требует доп. памяти/диска и времени на запись/чтение.
* '''Пример (из слайдов 16-22 Лекции 7):''' <code>sql    SELECT *    FROM STUDENTS ST    JOIN EXAMS EXAM ON ST.ID = EXAM.STUD_ID    WHERE ST.GROUP = '3100' AND ST.ID &gt;= 150000;</code>
* '''Конвейерная обработка:''' Как только первая операция производит первую строку результата, эта строка немедленно передается на вход следующей операции, не дожидаясь завершения первой. Значительно экономит ресурсы и время, если это возможно. Оптимизатор старается использовать конвейер.
** '''План 1 (сначала JOIN, потом WHERE):''' <code>σ(ST.GROUP='3100' ∧ ST.ID&gt;=150000) (STUDENTS ⋈(ST.ID=EXAM.STUD_ID) EXAMS)</code> Представление в виде дерева: сначала соединение, потом две операции выборки.
** '''План 2 (сначала WHERE для STUDENTS, потом JOIN):''' <code>(σ(ST.GROUP='3100' ∧ ST.ID&gt;=150000) (STUDENTS)) ⋈(ST.ID=EXAM.STUD_ID) EXAMS</code> Представление в виде дерева: сначала две операции выборки для <code>STUDENTS</code>, потом результат соединяется с <code>EXAMS</code>. ''Этот план обычно эффективнее, так как соединяется меньший объем данных.''


'''Левосторонние Деревья Планов:'''
'''Материализация vs Конвейерная Обработка (Pipelining):'''


* В плане выполнения, где узлы — это операции <code>JOIN</code>, левостороннее дерево — это план, где правым входом для каждой операции <code>JOIN</code> всегда является одна из исходных таблиц, а левым — результат предыдущего соединения.
* '''Материализация:''' Результат промежуточной операции полностью вычисляется и сохраняется во временную структуру, затем передается следующей операции.
* Многие оптимизаторы (включая PostgreSQL по умолчанию для большого числа таблиц) рассматривают в основном левосторонние планы, так как они хорошо подходят для конвейерной обработки и сокращают пространство поиска планов.
* '''Конвейерная обработка:''' Как только первая операция производит первую строку результата, эта строка немедленно передается на вход следующей операции. Экономит ресурсы и время. Оптимизатор старается использовать конвейер. '''Левосторонние деревья планов''' хорошо подходят для конвейерной обработки (результат предыдущего соединения сразу идет на вход следующего).


'''Алгоритмы Выполнения Соединений (<code>JOIN</code>):'''
'''Советы по построению (оптимизации) планов (что пытается делать оптимизатор):'''


СУБД использует разные алгоритмы для физического выполнения операции <code>JOIN</code>. Выбор зависит от размера таблиц, наличия индексов и условий соединения.
* Использовать конвейерную обработку (левосторонние планы, избегать блокирующих операций типа полной сортировки без необходимости).
* '''Делать выборку как можно раньше.'''
* '''Делать проекцию как можно раньше.'''
* Грамотно планировать соединения (порядок, алгоритмы).
* '''Цель:''' Уменьшение размеров промежуточных данных =&gt; уменьшение числа операций чтения/записи во внешнюю память.


# '''Nested Loop Join (Соединение вложенными циклами):'''
'''Влияние индексов на планы:''' Индексы могут кардинально изменить выбранный план, позволяя заменить полное сканирование таблицы (Seq Scan) на более эффективное индексное сканирование (Index Scan) или улучшить производительность соединений (Index Nested Loop Join).
#* '''Принцип:''' Для ''каждой'' строки из внешней таблицы (R) просматриваются ''все'' строки внутренней таблицы (S). Если строки удовлетворяют условию соединения, они комбинируются и возвращаются.
#* '''Простой вариант:''' Очень медленный, если таблицы большие (число сравнений = |R| * |S|).
#* '''Block Nested Loop:''' Оптимизация. Внешняя таблица читается блоками. Для каждого блока внешней таблицы читается вся внутренняя таблица (или ее блок) и выполняется соединение в памяти. Уменьшает количество чтений внутренней таблицы.
#* '''Index Nested Loop:''' Если по столбцу соединения во ''внутренней'' таблице (S) есть '''индекс''', то для каждой строки из внешней таблицы (R) выполняется быстрый поиск по индексу в S, вместо полного сканирования S. Очень эффективен, если внешняя таблица (R) маленькая, а по внутренней (S) есть подходящий индекс.
#* '''Когда используется:''' Часто для соединения маленькой таблицы с большой (при наличии индекса у большой) или для не-эквисоединений.
# '''Sort-Merge Join (Соединение слиянием):'''
#* '''Принцип:'''
#*# ''Обе'' таблицы сортируются по столбцам соединения.
#*# Отсортированные таблицы “сливаются”: СУБД одновременно проходит по обеим таблицам, сравнивая текущие строки. Если значения в столбцах соединения совпадают, строки комбинируются. Если значение в одной таблице меньше, СУБД продвигается по этой таблице до следующего значения.
#* '''Когда используется:''' Эффективен для эквисоединений, особенно если таблицы уже отсортированы (например, читаются из индекса B-дерева в нужном порядке) или если результат соединения нужно будет отсортировать по тем же столбцам. Требует затрат на сортировку, если таблицы не отсортированы.
# '''Hash Join (Соединение по хешу):'''
#* '''Принцип:'''
#*# Строится '''хеш-таблица''' в памяти по ''меньшей'' из двух таблиц (фаза построения). Ключом хеш-таблицы является значение столбца(ов) соединения.
#*# Сканируется ''бОльшая'' таблица (фаза зондирования). Для каждой строки вычисляется хеш от столбца(ов) соединения и выполняется поиск в хеш-таблице. Если совпадение найдено, строки комбинируются.
#* '''Когда используется:''' Очень эффективен для '''эквисоединений''' больших таблиц, особенно если хеш-таблица помещается в память. Может требовать записи временных данных на диск, если хеш-таблица слишком велика. Не подходит для не-эквисоединений.


<span id="часть-5-просмотр-плана-выполнения-в-postgresql"></span>
'''Стоимость плана:''' Складывается из стоимости чтения входных/промежуточных таблиц, записи промежуточных данных (при материализации), сортировки и других операций.
=== Часть 5: Просмотр Плана Выполнения в PostgreSQL ===


Чтобы понять, как PostgreSQL собирается выполнить ваш запрос (или как он его выполнил), используется команда <code>EXPLAIN</code>.
<span id="часть-5-алгоритмы-выполнения-соединений-join"></span>
=== Часть 5: Алгоритмы Выполнения Соединений (<code>JOIN</code>) ===


* '''<code>EXPLAIN SQL_ЗАПРОС;</code>''': Показывает '''план выполнения''', который ''построил'' оптимизатор, но '''не выполняет''' сам запрос. Полезно для анализа сложных запросов без их реального запуска.
(Детальное описание алгоритмов было в предыдущей версии, здесь краткое повторение с акцентом на примеры из Лекции 7)
* '''<code>EXPLAIN ANALYZE SQL_ЗАПРОС;</code>''': '''Выполняет''' запрос и показывает '''реальный план выполнения''' с фактическим временем выполнения каждой операции и количеством строк. Дает более точную картину, но требует реального выполнения запроса (будьте осторожны с <code>UPDATE</code>, <code>DELETE</code>, <code>INSERT</code>).


'''Чтение вывода <code>EXPLAIN</code>:'''
# '''Nested Loop Join (Соединение вложенными циклами):'''
 
#* '''R ⋈<sub>condition</sub> S''' (R - внешнее, S - внутреннее отношение)
Вывод представляет собой дерево операций плана. Каждая строка — узел дерева, с отступом показана вложенность.
#* '''Простой:''' Для каждой строки <code>r</code> из <code>R</code>, просматриваем все строки <code>s</code> из <code>S</code>. Если <code>condition(r, s)</code> истинно, добавляем <code>r</code> + <code>s</code> в результат.
#** ''Пример (слайды 54-70 Лекции 7):'' Таблицы <code>Groups</code> и <code>Students</code>. Для первой группы из <code>Groups</code> перебираются все студенты из <code>Students</code>. Потом для второй группы из <code>Groups</code> снова перебираются все студенты из <code>Students</code>, и так далее.
#* '''Block Nested Loop:''' Внешнее отношение <code>R</code> читается блоками (несколько страниц). Для каждого блока <code>R</code>, сканируется вся внутренняя таблица <code>S</code>. Уменьшает число полных сканирований <code>S</code>.
#** ''Пример (слайды 88-91 Лекции 7):'' Если в буфере помещается блок из 4 страниц <code>Groups</code>, то для этого блока один раз сканируется вся таблица <code>Students</code>. Затем загружается следующий блок <code>Groups</code> и снова сканируется вся <code>Students</code>. Эффективнее, если внешняя таблица меньше внутренней.
#* '''Index Nested Loop:''' Если по столбцу соединения во ''внутренней'' таблице <code>S</code> есть индекс. Для каждой строки <code>r</code> из <code>R</code> выполняется быстрый поиск по индексу в <code>S</code>.
# '''Sort-Merge Join (Соединение слиянием):'''
#* Обе таблицы (<code>R</code> и <code>S</code>) сортируются по атрибутам соединения.
#* Затем отсортированные таблицы “сливаются” (как при merge sort), одновременно проходя по обеим и комбинируя строки с совпадающими значениями атрибутов соединения.
#* ''Пример (слайды 93-99 Лекции 7):'' Таблицы <code>Groups</code> и <code>Students</code> (предполагается, что они уже отсортированы по <code>GroupID</code>). Указатели одновременно движутся по обеим таблицам.


<pre>QUERY PLAN
<span id="часть-6-просмотр-плана-выполнения-в-postgresql"></span>
----------------------------------------------------------------------------------------
=== Часть 6: Просмотр Плана Выполнения в PostgreSQL ===
Index Scan using students_studid_pkey on students  (cost=0.32..8.34 rows=1 width=150)
  Index Cond: (StudId = 942)</pre>
* '''Тип узла:''' <code>Index Scan</code> (использование индекса), <code>Seq Scan</code> (полное сканирование), <code>Nested Loop</code>, <code>Hash Join</code>, <code>Merge Join</code>, <code>Sort</code>, <code>Aggregate</code> и т.д.
* '''<code>on table_name</code>''': Таблица, к которой применяется операция.
* '''<code>using index_name</code>''': Используемый индекс.
* '''<code>cost=startup..total</code>''': Оценочная стоимость. <code>startup</code> - стоимость до получения первой строки, <code>total</code> - стоимость получения всех строк. Единицы условные.
* '''<code>rows=N</code>''': Оценочное количество строк, которое вернет этот узел.
* '''<code>width=N</code>''': Оценочный средний размер строки в байтах.
* '''Дополнительные условия:''' <code>Index Cond</code>, <code>Filter</code>, <code>Join Filter</code>, <code>Hash Cond</code> — условия, применяемые на этом узле.


'''Пример <code>EXPLAIN ANALYZE</code>:'''
Команда <code>EXPLAIN</code> показывает план, который СУБД выбрала для выполнения запроса.


<ul>
<li>'''<code>EXPLAIN &lt;SQL_ЗАПРОС&gt;;</code>''': Показывает '''план выполнения''', но '''не выполняет''' сам запрос.
<ul>
<li>'''Вывод (слайд 103-104):''' <code>QUERY PLAN    ---------------------------------------------------------------------    Seq Scan on Students (cost=0.00..309.00 rows=900 width=170)</code>
<ul>
<li><code>Seq Scan on Students</code>: Операция — последовательное сканирование таблицы <code>Students</code>.</li>
<li><code>cost=0.00..309.00</code>: Оценочная стоимость (начальная..общая).</li>
<li><code>rows=900</code>: Оценочное количество строк, которое вернет эта операция.</li>
<li><code>width=170</code>: Оценочный средний размер строки.</li></ul>
</li>
<li>'''Вывод с индексом (слайд 105):''' <code>QUERY PLAN    ----------------------------------------------------------------------------------------    Index Scan using students_studid_pkey on students  (cost=0.32..8.34 rows=1 width=150)      Index Cond: (StudId = 942)</code>
<ul>
<li><code>Index Scan using students_studid_pkey on students</code>: Используется индекс <code>students_studid_pkey</code>.</li>
<li><code>Index Cond: (StudId = 942)</code>: Условие, применяемое к индексу.</li></ul>
</li></ul>
</li>
<li>'''<code>EXPLAIN ANALYZE &lt;SQL_ЗАПРОС&gt;;</code>''': '''Выполняет''' запрос и показывает '''реальный план выполнения''' с фактическим временем выполнения каждой операции и количеством строк.
<ul>
<li><p>'''Вывод (примерный, на основе слайда 106):'''</p>
<syntaxhighlight lang="sql">EXPLAIN ANALYZE SELECT * FROM students WHERE StudID = 942;
<syntaxhighlight lang="sql">EXPLAIN ANALYZE SELECT * FROM students WHERE StudID = 942;


QUERY PLAN
QUERY PLAN
------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------------------
  Index Scan using students_studid_pkey on students  (cost=0.32..8.34 rows=1 width=150) (actual time=0.025..0.026 rows=1 loops=1)
  Index Scan using students_studid_pkey on students  (cost=0.29..8.30 rows=1 width=153) (actual time=0.020..0.021 rows=1 loops=1)
   Index Cond: (StudId = 942)
   Index Cond: (studid = 942)
  Planning time: 0.150 ms
  Planning Time: 0.075 ms
  Execution time: 0.050 ms
  Execution Time: 0.035 ms
(4 rows)</syntaxhighlight>
(4 rows)</syntaxhighlight>
* '''<code>actual time=startup..total</code>''': Фактическое время выполнения узла в миллисекундах.
<ul>
* '''<code>rows=N</code>''': Фактическое количество строк, возвращенное узлом.
<li><code>(actual time=0.020..0.021 rows=1 loops=1)</code>: Фактическое время (начало..конец), фактическое число строк, сколько раз узел выполнялся.</li>
* '''<code>loops=N</code>''': Сколько раз был выполнен этот узел (важно для вложенных циклов).
<li><code>Planning Time</code>: Время, затраченное на планирование/оптимизацию.</li>
* '''<code>Planning time</code>''': Время, затраченное на планирование/оптимизацию.
<li><code>Execution Time</code>: Общее время выполнения запроса (включая планирование, если не используется <code>SET anl_separate_planning_info = on;</code> или аналогичная опция).</li></ul>
* '''<code>Execution time</code>''': Общее время выполнения запроса.
</li></ul>
 
</li></ul>
Анализ вывода <code>EXPLAIN</code> и <code>EXPLAIN ANALYZE</code> — ключевой навык для оптимизации медленных запросов. Он позволяет понять, какие операции являются “бутылочным горлышком”, используются ли индексы, корректны ли оценки оптимизатора.


[[Category:БД]]
Анализ вывода <code>EXPLAIN</code> (особенно <code>EXPLAIN ANALYZE</code>) помогает понять, почему запрос выполняется медленно, используются ли индексы, корректны ли оценки оптимизатора, и какие части запроса являются “бутылочным горлышком”.
[[Category:БД:Теория]]

Текущая версия от 09:16, 12 мая 2025

Глава 6: Индексы и Выполнение Запросов

В предыдущих главах мы научились создавать таблицы, наполнять их данными и извлекать эти данные с помощью SQL-запросов. Теперь разберемся, как СУБД выполняет эти запросы “под капотом” и как можно ускорить их выполнение с помощью индексов. Эта глава основана на материалах Лекций 6: “Индексы. Выполнение запросов” и Лекции 7: “Выполнение запросов”.

Часть 1: Повышение Производительности Запросов

Когда данных в таблицах становится много (тысячи, миллионы строк), даже простые запросы могут выполняться долго. Почему? Потому что в самом простом случае СУБД приходится последовательно просматривать всю таблицу, чтобы найти нужные строки (это называется полное сканирование таблицы или Sequential Scan).

Способы повышения производительности:

  1. Грамотное проектирование модели данных: Это основа. Если модель данных изначально плохо спроектирована (например, ненормализована, что приводит к избыточности, или наоборот, излишне нормализована, что требует множества сложных соединений для простых задач), то оптимизировать запросы будет очень сложно.
  2. Использование индексов: Создание дополнительных структур данных, которые позволяют СУБД быстро находить нужные строки по значениям в определенных столбцах, не просматривая всю таблицу. (Основная тема этой главы).
  3. Оптимизация запросов: Переписывание SQL-запросов так, чтобы они были более понятны оптимизатору СУБД и могли быть выполнены более эффективно (например, избегая ненужных соединений или используя EXISTS вместо IN в некоторых случаях).
  4. Настройка физических параметров СУБД: Оптимизация конфигурации сервера PostgreSQL (выделение памяти, настройка параллелизма, параметры ввода-вывода и т.д.).

Мы сосредоточимся на индексах и понимании выполнения запросов.

Часть 2: Индексы

Что такое Индекс?

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

Индекс SQL работает похожим образом:

  • Это отдельная структура данных (обычно хранящаяся в отдельном файле), связанная с таблицей.
  • Он содержит копии значений из одного или нескольких столбцов таблицы.
  • Эти значения в индексе упорядочены определенным образом (например, по алфавиту для строк, по возрастанию для чисел, или организованы в специфическую структуру данных типа хеш-таблицы).
  • Каждое значение в индексе имеет указатель (ссылку, адрес, TID - Tuple Identifier) на те строки в основной таблице, где это значение встречается.

Как индекс помогает?

Рассмотрим таблицу STUDENT:

StudID GroupID Name
14 51 Ivan Petrov
23 53 Vladimir Ivanov
18 54 Eugene Serov
13 55 Gennady Surikov
25 56 Nikolay Petrov
25 58 Svetlana Ivanova
25 59 Antony Fedorov
18 63 Petr Menchikov
27 67 Gennady Klokov
23 69 Serge Vidov
18 72 Roman Klever
13 73 Pavel Borisov

И запрос:

SELECT * FROM STUDENT WHERE StudID = 18;
  • Без индекса: СУБД читает каждую строку таблицы STUDENT и сравнивает значение в колонке StudID с числом 18. Если таблица большая (миллионы строк), это займет много времени, так как СУБД должна прочитать все данные с диска в память и проверить каждую строку.
  • С индексом по StudID:
    1. СУБД обращается к структуре индекса по StudID.
    2. Если индекс, например, B-дерево (значения отсортированы), СУБД быстро находит значение 18 (например, используя бинарный поиск или навигацию по дереву).
    3. Из индекса СУБД получает указатели (TID) на строки в основной таблице, где StudID = 18.
    4. СУБД читает только эти конкретные строки из основной таблицы, используя полученные указатели. Это значительно быстрее, чем читать всю таблицу.

Пример (слайды 10-17, упрощенная иллюстрация):

Допустим, у нас есть упорядоченный список значений StudID (это и есть наш “игрушечный” индекс) и стрелки, указывающие на строки в основной таблице.

Индекс (упорядоченный StudID) Таблица STUDENT (StudID, GroupID, Name)
13 14, 51, Ivan Petrov
13 23, 53, Vladimir Ivanov
14 18, 54, Eugene Serov <– Нужная строка
18 13, 55, Gennady Surikov
18 25, 56, Nikolay Petrov
18 25, 58, Svetlana Ivanova
23 25, 59, Antony Fedorov
23 18, 63, Petr Menchikov <– Нужная строка
25 27, 67, Gennady Klokov
25 23, 69, Serge Vidov
25 18, 72, Roman Klever <– Нужная строка
27 13, 73, Pavel Borisov
  1. Запрос: SELECT * FROM STUDENT WHERE StudID = 18;
  2. СУБД смотрит в индекс.
  3. Поскольку индекс отсортирован (в нашем примере это простой список, но представьте более сложную структуру), СУБД быстро находит первое вхождение 18.
  4. Берет указатель и читает строку 18, 54, Eugene Serov.
  5. Переходит к следующему значению 18 в индексе.
  6. Берет указатель и читает строку 18, 63, Petr Menchikov.
  7. Переходит к следующему значению 18 в индексе.
  8. Берет указатель и читает строку 18, 72, Roman Klever.
  9. Следующее значение в индексе уже 23 (или больше 18). Поиск завершен.

Вместо сканирования всех 12 строк таблицы, мы быстро нашли 3 нужные строки через индекс.

Важные моменты:

  • Индексы работают неявно: Вам не нужно указывать в SELECT-запросе, какой индекс использовать. Оптимизатор СУБД сам решает, использовать ли индекс и какой именно, на основе статистики данных и стоимости выполнения разных планов.
  • Иногда СУБД может не использовать индекс, даже если он есть. Например, если запрос выбирает очень большую часть таблицы (проще прочитать всю таблицу), или если условия в WHERE таковы, что индекс неприменим (например, функция над индексированным столбцом WHERE upper(StudName) = 'ИВАНОВ').

Когда Индексы Полезны?

Индексы ускоряют операции, включающие:

  • Поиск строк по условиям (WHERE): Особенно для условий равенства (=), диапазонных запросов (>, <, BETWEEN) по индексированному столбцу.
  • Соединения таблиц (JOIN): Индексы по столбцам, участвующим в условии ON, значительно ускоряют поиск совпадающих строк в соединяемых таблицах. Первичные и уникальные ключи автоматически индексируются в PostgreSQL, что ускоряет соединения по ним.
  • Поиск минимального/максимального значения (MIN(), MAX()): Если столбец индексирован (например, B-деревом), СУБД может просто взять первое/последнее значение из индекса, не сканируя таблицу.
  • Сортировку (ORDER BY): Если порядок сортировки совпадает с порядком индекса (или обратен ему, и индекс это поддерживает), СУБД может избежать фактической сортировки данных, просто прочитав строки в порядке индекса.
  • Группировку (GROUP BY): Индексы могут помочь СУБД быстрее найти строки, относящиеся к одной группе.

Недостатки Индексов:

  1. Занимают место на диске и в памяти: Индекс — это дополнительная структура данных. Чем больше индексов, тем больше места они занимают.
  2. Замедляют операции записи (INSERT, UPDATE, DELETE): При изменении данных в индексированном столбце или добавлении/удалении строки СУБД должна обновить не только саму таблицу, но и все связанные с ней индексы. Это добавляет накладные расходы. Чем больше индексов на таблице, тем медленнее будут операции записи.
    • Пример (слайд 23-24): DELETE FROM STUDENT WHERE GroupID = 54; Если по GroupID есть индекс, то СУБД должна не только удалить строку из таблицы STUDENT, но и удалить соответствующие записи из индекса по GroupID, а также, возможно, из индекса по StudID (если StudID был ключом этого индекса и строка удаляется).
  3. Неэффективны на маленьких таблицах: Если таблица настолько мала, что целиком помещается в несколько блоков памяти, СУБД прочитает ее полностью быстрее, чем будет обращаться к индексу.
  4. Неэффективны при выборке больших объемов данных: Если запрос выбирает значительную часть таблицы (например, WHERE column > 0, где почти все значения больше нуля), СУБД, скорее всего, решит, что проще прочитать всю таблицу последовательно (Seq Scan), чем много раз обращаться к индексу и затем к разным частям таблицы за строками (Index Scan + много случайных чтений с диска, что медленнее последовательного).

Стратегии Применения Индексов:

  • Анализируйте запросы: Какие столбцы часто используются в WHERE и JOIN? Какие операции преобладают — чтение или запись?
  • Индексируйте столбцы в WHERE: Особенно те, по которым идет поиск на равенство или по диапазону и которые обладают хорошей селективностью (т.е. по значению в этом столбце можно отобрать небольшую долю строк таблицы). Индексировать столбец “пол” (М/Ж), где всего два значения, обычно бессмысленно, так как каждое значение выбирает примерно половину таблицы.
  • Индексируйте столбцы внешних ключей: Это критически важно для ускорения JOIN. PostgreSQL не создает их автоматически для внешних ключей (в отличие от первичных/уникальных), поэтому это нужно делать вручную.
  • Индексируйте столбцы в ORDER BY и GROUP BY: Если часто нужна сортировка или группировка по определенному столбцу.
  • Используйте составные индексы: Если часто ищете по комбинации столбцов (WHERE col1 = ? AND col2 = ?), создайте индекс по (col1, col2). Порядок столбцов в составном индексе важен! Индекс по (col1, col2) может использоваться для запросов по col1 и для запросов по col1 И col2, но не для запросов только по col2.
  • Не создавайте лишних индексов: Каждый индекс замедляет запись. Регулярно проверяйте и удаляйте неиспользуемые индексы.
  • Первичные и уникальные ключи индексируются автоматически в PostgreSQL (с помощью B-Tree индекса).

Создание Индексов (CREATE INDEX)

CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ IF NOT EXISTS ] имя_индекса
ON имя_таблицы [ USING метод ]
( { имя_колонки | ( выражение ) } [ COLLATE имя_сортировки ] [ класс_операторов ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ] [, ...] )
[ WITH ( параметр_хранения = значение [, ... ] ) ]
[ TABLESPACE имя_табличного_пространства ]
[ WHERE предикат ]; -- Частичный индекс

Полный синтаксис сложнее, здесь приведены основные части.

  • UNIQUE: Создает уникальный индекс.
  • CONCURRENTLY: (PostgreSQL) Позволяет создать индекс без длительной блокировки операций записи в таблицу.
  • IF NOT EXISTS: Не выдавать ошибку, если индекс с таким именем уже существует.
  • имя_индекса: Имя для вашего индекса.
  • имя_таблицы: Таблица, для которой создается индекс.
  • USING метод: Указывает тип индекса (btree, hash, gist, gin и др.). По умолчанию в PostgreSQL это btree.
  • ( имя_колонки | ( выражение ) ... ): Список столбцов или выражений, по которым строится индекс.
  • ASC | DESC: Порядок сортировки (для B-Tree).
  • NULLS FIRST | NULLS LAST: Где размещать NULL-значения.
  • WHERE предикат: Создает частичный индекс. Индексируются только строки, удовлетворяющие предикату. Полезно, если часто ищутся строки с определенным значением в одном столбце, а по другому нужно индексировать только подмножество.

Примеры:

-- Простой B-Tree индекс по одной колонке
CREATE INDEX idx_student_groupid ON student (GroupID);

-- Составной B-Tree индекс по двум колонкам
CREATE INDEX idx_student_surname_name ON student (Surname, Name);

-- Индекс по выражению (по фамилии в нижнем регистре)
CREATE INDEX idx_student_surname_lower ON student (lower(Surname));

-- Хеш-индекс (только для оператора равенства '=')
CREATE INDEX idx_student_name_hash ON student USING hash (Name);

-- Пример из слайда 29 (B-Tree по двум колонкам)
CREATE INDEX idx_example ON table_name USING btree (column1, column2);

Часть 3: Типы Индексов

PostgreSQL поддерживает несколько типов индексов, оптимизированных для разных задач.

  1. B-дерево (B-Tree):
    • Используется по умолчанию (USING btree).
    • Структура: Сбалансированное, многоуровневое дерево.
      • Листовые узлы: Содержат индексируемые значения (ключи) и указатели (TID - Tuple Identifier, адрес строки на диске) на строки таблицы. Ключи в листовых узлах отсортированы. Листовые узлы связаны между собой для быстрого последовательного прохода (полезно для диапазонных запросов).
      • Внутренние узлы (включая корневой): Содержат “разделители” (ключи) и ссылки на дочерние узлы. Помогают быстро сузить область поиска.
    • Преимущества: Очень универсальный. Эффективен для:
      • Поиска по равенству (=).
      • Диапазонных запросов (>, <, BETWEEN, LIKE 'abc%' (поиск по префиксу)).
      • Сортировки (ORDER BY).
      • Поиска NULL (IS NULL, IS NOT NULL).
      • Работы с MIN() и MAX().
    • Пример поиска (StudID = 85, слайды 36-41):
      1. Запрос: SELECT * FROM STUDENT WHERE StudID = 85;
      2. Начинаем с корневого узла (например, [3, 30, 63]).
      3. 85 > 63, идем по правой ветке к узлу (например, [63, 76, 88]).
      4. 76 < 85 <= 88, идем по ветке, соответствующей диапазону (88), к листовому узлу (например, содержащему ключи [76(tid), 85(tid)]).
      5. В листовом узле находим ключ 85 и соответствующий tid.
      6. По tid читаем строку из таблицы.
    • Пример диапазонного поиска (StudID >= 85, слайды 42-43):
      1. Аналогично находим первый ключ 85.
      2. Далее, благодаря связям между листовыми узлами, последовательно читаем следующие листовые узлы ([88(tid), 95(tid)] и т.д.), пока условие выполняется.
  2. Хеш-индекс (Hash):
    • Структура: Основан на хеш-таблице. Для индексируемого значения вычисляется хеш-код, который используется как индекс в массиве “корзин” (buckets). Каждая корзина содержит указатели на строки таблицы, чьи индексируемые значения дали этот хеш-код.
    • Пример (слайды 46-49):
      • Значения: (23, 2), (34, 2), (34543, 4), (234, 4), (2355, 5)
      • Хеш-функция Hash_Function(val1, val2) дает, например:
        • Hash_Function(23, 2) -> ...2345 (хэш значение)
        • Hash_Function(34, 2) -> ...3423
        • Hash_Function(34543, 4) -> ...2345 (коллизия с первым!)
      • Хэш-индекс:
        • Корзина 2345: tid1 (для (23,2)), tid3 (для (34543,4))
        • Корзина 3423: tid2 (для (34,2))
      • При поиске, например, WHERE StudID=23 AND GroupID=2:
        1. Вычисляется Hash_Function(23, 2) -> ...2345.
        2. Смотрим в корзину 2345.
        3. Для каждой строки (tid1, tid3) в этой корзине проверяем точное совпадение исходных значений StudID и GroupID (т.к. в корзине могут быть коллизии).
    • Преимущества: Очень быстрый для операций поиска по точному равенству (=).
    • Недостатки:
      • Бесполезен для диапазонных запросов, сортировки, поиска по префиксу.
      • Менее эффективен при большом количестве коллизий (когда много разных значений попадают в одну корзину).
    • Когда использовать: Только если запросы к столбцу — это исключительно проверки на точное равенство (WHERE attr = 1), и другие операции не нужны. B-Tree обычно является более гибким выбором.
  3. GiST (Generalized Search Tree - Обобщенное дерево поиска):
    • Гибкая инфраструктура для создания индексов для сложных типов данных (геометрические данные, диапазоны, некоторые виды текстового поиска).
    • Позволяет индексировать операции типа “пересекается”, “содержит”.
    • Используется расширениями, такими как PostGIS (для геоданных) и pg_trgm (для поиска по похожести строк - триграммам).
  4. GIN (Generalized Inverted Index - Обобщенный инвертированный индекс):
    • Оптимизирован для индексации составных значений, где одно значение в столбце может содержать несколько “ключей” (например, массив слов в документе, элементы массива jsonb, элементы hstore).
    • Хранит карту “ключ из составного значения -> список строк, где этот ключ встречается”.
    • Преимущества: Очень эффективен для поиска строк, содержащих определенные значения внутри составного типа (например, найти все документы, содержащие слово ‘postgresql’).
    • Используется для полнотекстового поиска, индексации jsonb (операторы ?, ?|, ?&), hstore, массивов (операторы @>, <@, &&).

Часть 4: Выполнение Запросов и Оптимизация

(Этот раздел повторяет и немного расширяет информацию из предыдущей версии методички, основываясь на слайдах Лекции 7).

Декларативность SQL: Пользователь указывает, что он хочет получить, а не как это сделать. СУБД сама решает, как выполнить запрос.

Этапы выполнения запроса в PostgreSQL:

  1. Разбор запроса (Parser): SQL-текст преобразуется во внутреннее представление — дерево разбора (parse tree). Проверяется синтаксис.
  2. Преобразование запроса (Rewriter/Analyzer): Дерево разбора анализируется, преобразуется с учетом правил и представлений (например, запрос к представлению заменяется на запрос к базовым таблицам). Семантическая проверка. Результат — дерево запроса (query tree).
  3. Планировщик/Оптимизатор (Planner/Optimizer):
    • Генерирует множество возможных планов выполнения для дерева запроса, используя:
      • Законы реляционной алгебры для эквивалентных преобразований.
      • Информацию об имеющихся индексах.
      • Доступные алгоритмы выполнения операций (Seq Scan, Index Scan, Nested Loop Join, Hash Join, Merge Join, Sort, Aggregate и т.д.).
    • Оценивает стоимость каждого плана на основе статистики таблиц (количество строк, распределение значений, размер таблицы, кардинальность и т.д.) и стоимости операций (I/O, CPU).
    • Выбирает план с минимальной оцененной стоимостью.
    • Результат — план выполнения (execution plan).
  4. Выполнение плана (Executor): План выполнения передается исполнителю, который шаг за шагом выполняет операции плана и возвращает результат.

План выполнения запроса:

  • Это, по сути, программа, которую СУБД строит для ответа на SQL-запрос.
  • Для одного SQL-запроса может существовать несколько эквивалентных планов, но с разной стоимостью выполнения.
  • Критерий выбора плана: Оценочная стоимость (число обменов с диском, время обмена, загрузка CPU).

Оптимизация на основе Реляционной Алгебры:

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

  • Пример (из слайдов 16-22 Лекции 7): sql SELECT * FROM STUDENTS ST JOIN EXAMS EXAM ON ST.ID = EXAM.STUD_ID WHERE ST.GROUP = '3100' AND ST.ID >= 150000;
    • План 1 (сначала JOIN, потом WHERE): σ(ST.GROUP='3100' ∧ ST.ID>=150000) (STUDENTS ⋈(ST.ID=EXAM.STUD_ID) EXAMS) Представление в виде дерева: сначала соединение, потом две операции выборки.
    • План 2 (сначала WHERE для STUDENTS, потом JOIN): (σ(ST.GROUP='3100' ∧ ST.ID>=150000) (STUDENTS)) ⋈(ST.ID=EXAM.STUD_ID) EXAMS Представление в виде дерева: сначала две операции выборки для STUDENTS, потом результат соединяется с EXAMS. Этот план обычно эффективнее, так как соединяется меньший объем данных.

Материализация vs Конвейерная Обработка (Pipelining):

  • Материализация: Результат промежуточной операции полностью вычисляется и сохраняется во временную структуру, затем передается следующей операции.
  • Конвейерная обработка: Как только первая операция производит первую строку результата, эта строка немедленно передается на вход следующей операции. Экономит ресурсы и время. Оптимизатор старается использовать конвейер. Левосторонние деревья планов хорошо подходят для конвейерной обработки (результат предыдущего соединения сразу идет на вход следующего).

Советы по построению (оптимизации) планов (что пытается делать оптимизатор):

  • Использовать конвейерную обработку (левосторонние планы, избегать блокирующих операций типа полной сортировки без необходимости).
  • Делать выборку как можно раньше.
  • Делать проекцию как можно раньше.
  • Грамотно планировать соединения (порядок, алгоритмы).
  • Цель: Уменьшение размеров промежуточных данных => уменьшение числа операций чтения/записи во внешнюю память.

Влияние индексов на планы: Индексы могут кардинально изменить выбранный план, позволяя заменить полное сканирование таблицы (Seq Scan) на более эффективное индексное сканирование (Index Scan) или улучшить производительность соединений (Index Nested Loop Join).

Стоимость плана: Складывается из стоимости чтения входных/промежуточных таблиц, записи промежуточных данных (при материализации), сортировки и других операций.

Часть 5: Алгоритмы Выполнения Соединений (JOIN)

(Детальное описание алгоритмов было в предыдущей версии, здесь краткое повторение с акцентом на примеры из Лекции 7)

  1. Nested Loop Join (Соединение вложенными циклами):
    • R ⋈condition S (R - внешнее, S - внутреннее отношение)
    • Простой: Для каждой строки r из R, просматриваем все строки s из S. Если condition(r, s) истинно, добавляем r + s в результат.
      • Пример (слайды 54-70 Лекции 7): Таблицы Groups и Students. Для первой группы из Groups перебираются все студенты из Students. Потом для второй группы из Groups снова перебираются все студенты из Students, и так далее.
    • Block Nested Loop: Внешнее отношение R читается блоками (несколько страниц). Для каждого блока R, сканируется вся внутренняя таблица S. Уменьшает число полных сканирований S.
      • Пример (слайды 88-91 Лекции 7): Если в буфере помещается блок из 4 страниц Groups, то для этого блока один раз сканируется вся таблица Students. Затем загружается следующий блок Groups и снова сканируется вся Students. Эффективнее, если внешняя таблица меньше внутренней.
    • Index Nested Loop: Если по столбцу соединения во внутренней таблице S есть индекс. Для каждой строки r из R выполняется быстрый поиск по индексу в S.
  2. Sort-Merge Join (Соединение слиянием):
    • Обе таблицы (R и S) сортируются по атрибутам соединения.
    • Затем отсортированные таблицы “сливаются” (как при merge sort), одновременно проходя по обеим и комбинируя строки с совпадающими значениями атрибутов соединения.
    • Пример (слайды 93-99 Лекции 7): Таблицы Groups и Students (предполагается, что они уже отсортированы по GroupID). Указатели одновременно движутся по обеим таблицам.

Часть 6: Просмотр Плана Выполнения в PostgreSQL

Команда EXPLAIN показывает план, который СУБД выбрала для выполнения запроса.

  • EXPLAIN <SQL_ЗАПРОС>;: Показывает план выполнения, но не выполняет сам запрос.
    • Вывод (слайд 103-104): QUERY PLAN --------------------------------------------------------------------- Seq Scan on Students (cost=0.00..309.00 rows=900 width=170)
      • Seq Scan on Students: Операция — последовательное сканирование таблицы Students.
      • cost=0.00..309.00: Оценочная стоимость (начальная..общая).
      • rows=900: Оценочное количество строк, которое вернет эта операция.
      • width=170: Оценочный средний размер строки.
    • Вывод с индексом (слайд 105): QUERY PLAN ---------------------------------------------------------------------------------------- Index Scan using students_studid_pkey on students (cost=0.32..8.34 rows=1 width=150) Index Cond: (StudId = 942)
      • Index Scan using students_studid_pkey on students: Используется индекс students_studid_pkey.
      • Index Cond: (StudId = 942): Условие, применяемое к индексу.
  • EXPLAIN ANALYZE <SQL_ЗАПРОС>;: Выполняет запрос и показывает реальный план выполнения с фактическим временем выполнения каждой операции и количеством строк.
    • Вывод (примерный, на основе слайда 106):

      EXPLAIN ANALYZE SELECT * FROM students WHERE StudID = 942;
      
      QUERY PLAN
      ------------------------------------------------------------------------------------------------------------------------------------
       Index Scan using students_studid_pkey on students  (cost=0.29..8.30 rows=1 width=153) (actual time=0.020..0.021 rows=1 loops=1)
         Index Cond: (studid = 942)
       Planning Time: 0.075 ms
       Execution Time: 0.035 ms
      (4 rows)
      
      • (actual time=0.020..0.021 rows=1 loops=1): Фактическое время (начало..конец), фактическое число строк, сколько раз узел выполнялся.
      • Planning Time: Время, затраченное на планирование/оптимизацию.
      • Execution Time: Общее время выполнения запроса (включая планирование, если не используется SET anl_separate_planning_info = on; или аналогичная опция).

Анализ вывода EXPLAIN (особенно EXPLAIN ANALYZE) помогает понять, почему запрос выполняется медленно, используются ли индексы, корректны ли оценки оптимизатора, и какие части запроса являются “бутылочным горлышком”.