БД:Теория:Глава 6: различия между версиями
Ivabus (обсуждение | вклад) Импорт |
Zhabka (обсуждение | вклад) Статья переписана в соответствии с последней лекцией. Метка: визуальный редактор отключён |
||
Строка 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 | {| class="wikitable" | ||
* '''Без индекса:''' СУБД читает ''каждую'' строку таблицы <code> | |- | ||
! 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>. | ||
*# | *# Если индекс, например, 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> <– ''Нужная строка'' | |||
|- | |||
| 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> <– ''Нужная строка'' | |||
|- | |||
| 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> <– ''Нужная строка'' | |||
|- | |||
| style="text-align: left;"| <code>27</code> → | |||
| style="text-align: left;"| <code>13, 73, Pavel Borisov</code> | |||
|} | |||
''' | # '''Запрос:''' <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). Поиск завершен. | |||
Вместо сканирования всех 12 строк таблицы, мы быстро нашли 3 нужные строки через индекс. | |||
'''Важные моменты:''' | '''Важные моменты:''' | ||
* Индексы работают '''неявно''': Вам не нужно указывать в <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>></code>, <code><</code>, <code>BETWEEN</code>) по индексированному столбцу. | * '''Поиск строк по условиям (<code>WHERE</code>)''': Особенно для условий равенства (<code>=</code>), диапазонных запросов (<code>></code>, <code><</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 > 0</code>, где почти все значения больше нуля), СУБД, скорее всего, решит, что проще прочитать всю таблицу последовательно (<code>Seq Scan</code>), чем много раз обращаться к индексу и затем к разным частям таблицы за строками (<code>Index Scan</code> + много чтений с диска). | # '''Неэффективны на маленьких таблицах:''' Если таблица настолько мала, что целиком помещается в несколько блоков памяти, СУБД прочитает ее полностью быстрее, чем будет обращаться к индексу. | ||
# '''Неэффективны при выборке больших объемов данных:''' Если запрос выбирает значительную часть таблицы (например, <code>WHERE column > 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>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>: Создает уникальный индекс | [ 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>: Указывает тип индекса ( | * <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 | DESC</code>: Порядок сортировки (для B-Tree). | ||
* <code>NULLS FIRST | NULLS LAST</code>: Где размещать <code>NULL</code>-значения | * <code>NULLS FIRST | NULLS LAST</code>: Где размещать <code>NULL</code>-значения. | ||
* <code>WHERE предикат</code>: Создает '''частичный индекс'''. Индексируются только строки, удовлетворяющие предикату. Полезно, если часто ищутся строки с определенным значением в одном столбце, а по другому нужно индексировать только подмножество. | |||
'''Примеры:''' | '''Примеры:''' | ||
<syntaxhighlight lang="sql">-- Простой индекс по одной колонке | <syntaxhighlight lang="sql">-- Простой B-Tree индекс по одной колонке | ||
CREATE INDEX idx_student_groupid ON student (GroupID); | 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_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, адрес строки на диске) на строки таблицы. Ключи в листовых узлах отсортированы. Листовые узлы связаны между собой для быстрого последовательного прохода (полезно для диапазонных запросов). | |||
#** '''Внутренние узлы (включая корневой):''' Содержат “разделители” (ключи) и ссылки на дочерние узлы. Помогают быстро сузить область поиска. | |||
#* '''Преимущества:''' Очень универсальный. Эффективен для: | #* '''Преимущества:''' Очень универсальный. Эффективен для: | ||
#** Поиска по равенству (<code>=</code>). | #** Поиска по равенству (<code>=</code>). | ||
#** Диапазонных запросов (<code>></code>, <code><</code>, <code>BETWEEN</code>). | #** Диапазонных запросов (<code>></code>, <code><</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>MIN()</code> и <code>MAX()</code>. | #** Работы с <code>MIN()</code> и <code>MAX()</code>. | ||
#* '''Пример поиска (StudID = 85):''' | #* '''Пример поиска (StudID = 85, слайды 36-41):''' | ||
#*# Запрос: <code>SELECT * FROM STUDENT WHERE StudID = 85;</code> | |||
#*# Начинаем с корневого узла (например, <code>[3, 30, 63]</code>). | |||
#*# <code>85 > 63</code>, идем по правой ветке к узлу (например, <code>[63, 76, 88]</code>). | |||
#*# <code>76 < 85 <= 88</code>, идем по ветке, соответствующей диапазону (88), к листовому узлу (например, содержащему ключи <code>[76(tid), 85(tid)]</code>). | |||
#*# В листовом узле находим ключ <code>85</code> и соответствующий <code>tid</code>. | |||
#*# По <code>tid</code> читаем строку из таблицы. | |||
#* '''Пример диапазонного поиска (StudID >= 85, слайды 42-43):''' | |||
#*# Аналогично находим первый ключ <code>85</code>. | |||
#*# Далее, благодаря связям между листовыми узлами, последовательно читаем следующие листовые узлы (<code>[88(tid), 95(tid)]</code> и т.д.), пока условие выполняется. | |||
# '''Хеш-индекс (Hash):''' | # '''Хеш-индекс (Hash):''' | ||
#* '''Структура:''' Основан на хеш-таблице. Для индексируемого значения вычисляется хеш-код, который используется | #* '''Структура:''' Основан на хеш-таблице. Для индексируемого значения вычисляется хеш-код, который используется как индекс в массиве “корзин” (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> -> <code>...2345</code> (хэш значение) | |||
#*** <code>Hash_Function(34, 2)</code> -> <code>...3423</code> | |||
#*** <code>Hash_Function(34543, 4)</code> -> <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> -> <code>...2345</code>. | |||
#**# Смотрим в корзину <code>2345</code>. | |||
#**# Для каждой строки (<code>tid1</code>, <code>tid3</code>) в этой корзине проверяем точное совпадение исходных значений <code>StudID</code> и <code>GroupID</code> (т.к. в корзине могут быть коллизии). | |||
#* '''Преимущества:''' Очень быстрый для операций поиска по '''точному равенству (<code>=</code>)'''. | #* '''Преимущества:''' Очень быстрый для операций поиска по '''точному равенству (<code>=</code>)'''. | ||
#* '''Недостатки:''' | #* '''Недостатки:''' | ||
#** '''Бесполезен''' для диапазонных запросов | #** '''Бесполезен''' для диапазонных запросов, сортировки, поиска по префиксу. | ||
#** | #** Менее эффективен при большом количестве коллизий (когда много разных значений попадают в одну корзину). | ||
#* '''Когда использовать:''' Только если запросы к столбцу — это исключительно проверки на точное равенство (<code>WHERE attr = 1</code>), и другие операции не нужны. B-Tree обычно является более гибким выбором. | |||
#* '''Когда использовать:''' | # '''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>). | ||
#* | #* Хранит карту “ключ из составного значения -> список строк, где этот ключ встречается”. | ||
#* Хранит карту “ключ -> список строк, где ключ встречается”. | #* '''Преимущества:''' Очень эффективен для поиска строк, содержащих определенные значения ''внутри'' составного типа (например, найти все документы, содержащие слово ‘postgresql’). | ||
#* '''Преимущества:''' Очень эффективен для поиска строк, содержащих определенные значения внутри составного типа (например, найти все документы, содержащие слово ‘postgresql’ | #* Используется для полнотекстового поиска, индексации <code>jsonb</code> (операторы <code>?</code>, <code>?|</code>, <code>?&</code>), <code>hstore</code>, массивов (операторы <code>@></code>, <code><@</code>, <code>&&</code>). | ||
#* Используется для полнотекстового поиска, индексации <code>jsonb</code>, <code> | |||
<span id="часть-4-выполнение-запросов-и-оптимизация"></span> | <span id="часть-4-выполнение-запросов-и-оптимизация"></span> | ||
=== Часть 4: Выполнение Запросов и Оптимизация === | === Часть 4: Выполнение Запросов и Оптимизация === | ||
(Этот раздел повторяет и немного расширяет информацию из предыдущей версии методички, основываясь на слайдах Лекции 7). | |||
# '''Разбор запроса (Parser):''' | '''Декларативность SQL:''' Пользователь указывает, ''что'' он хочет получить, а не ''как'' это сделать. СУБД сама решает, как выполнить запрос. | ||
# '''Преобразование запроса (Rewriter):''' Дерево разбора преобразуется с учетом правил и представлений (например, запрос к представлению заменяется на запрос к базовым таблицам). Результат — '''дерево запроса (query tree)'''. | |||
# '''Планировщик/Оптимизатор (Planner/Optimizer):''' | '''Этапы выполнения запроса в PostgreSQL:''' | ||
#* Генерирует ''' | |||
#* '''Оценивает стоимость''' каждого плана | # '''Разбор запроса (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). | |||
'''Оптимизация на основе Реляционной Алгебры:''' | |||
Оптимизатор использует законы реляционной алгебры (коммутативность, ассоциативность соединений, проталкивание выборок и проекций и т.д.) для генерации различных, но семантически эквивалентных планов. | |||
* ''' | * '''Пример (из слайдов 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 >= 150000;</code> | ||
* ''' | ** '''План 1 (сначала JOIN, потом WHERE):''' <code>σ(ST.GROUP='3100' ∧ ST.ID>=150000) (STUDENTS ⋈(ST.ID=EXAM.STUD_ID) EXAMS)</code> Представление в виде дерева: сначала соединение, потом две операции выборки. | ||
** '''План 2 (сначала WHERE для STUDENTS, потом JOIN):''' <code>(σ(ST.GROUP='3100' ∧ ST.ID>=150000) (STUDENTS)) ⋈(ST.ID=EXAM.STUD_ID) EXAMS</code> Представление в виде дерева: сначала две операции выборки для <code>STUDENTS</code>, потом результат соединяется с <code>EXAMS</code>. ''Этот план обычно эффективнее, так как соединяется меньший объем данных.'' | |||
''' | '''Материализация vs Конвейерная Обработка (Pipelining):''' | ||
* | * '''Материализация:''' Результат промежуточной операции полностью вычисляется и сохраняется во временную структуру, затем передается следующей операции. | ||
* | * '''Конвейерная обработка:''' Как только первая операция производит первую строку результата, эта строка немедленно передается на вход следующей операции. Экономит ресурсы и время. Оптимизатор старается использовать конвейер. '''Левосторонние деревья планов''' хорошо подходят для конвейерной обработки (результат предыдущего соединения сразу идет на вход следующего). | ||
''' | '''Советы по построению (оптимизации) планов (что пытается делать оптимизатор):''' | ||
* Использовать конвейерную обработку (левосторонние планы, избегать блокирующих операций типа полной сортировки без необходимости). | |||
* '''Делать выборку как можно раньше.''' | |||
* '''Делать проекцию как можно раньше.''' | |||
* Грамотно планировать соединения (порядок, алгоритмы). | |||
* '''Цель:''' Уменьшение размеров промежуточных данных => уменьшение числа операций чтения/записи во внешнюю память. | |||
'''Влияние индексов на планы:''' Индексы могут кардинально изменить выбранный план, позволяя заменить полное сканирование таблицы (Seq Scan) на более эффективное индексное сканирование (Index Scan) или улучшить производительность соединений (Index Nested Loop Join). | |||
'''Стоимость плана:''' Складывается из стоимости чтения входных/промежуточных таблиц, записи промежуточных данных (при материализации), сортировки и других операций. | |||
<span id="часть-5-алгоритмы-выполнения-соединений-join"></span> | |||
=== Часть 5: Алгоритмы Выполнения Соединений (<code>JOIN</code>) === | |||
(Детальное описание алгоритмов было в предыдущей версии, здесь краткое повторение с акцентом на примеры из Лекции 7) | |||
''' | # '''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>). Указатели одновременно движутся по обеим таблицам. | |||
< | <span id="часть-6-просмотр-плана-выполнения-в-postgresql"></span> | ||
=== Часть 6: Просмотр Плана Выполнения в PostgreSQL === | |||
Команда <code>EXPLAIN</code> показывает план, который СУБД выбрала для выполнения запроса. | |||
<ul> | |||
<li>'''<code>EXPLAIN <SQL_ЗАПРОС>;</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 <SQL_ЗАПРОС>;</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. | 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: ( | Index Cond: (studid = 942) | ||
Planning | Planning Time: 0.075 ms | ||
Execution | Execution Time: 0.035 ms | ||
(4 rows)</syntaxhighlight> | (4 rows)</syntaxhighlight> | ||
<ul> | |||
<li><code>(actual time=0.020..0.021 rows=1 loops=1)</code>: Фактическое время (начало..конец), фактическое число строк, сколько раз узел выполнялся.</li> | |||
<li><code>Planning Time</code>: Время, затраченное на планирование/оптимизацию.</li> | |||
<li><code>Execution Time</code>: Общее время выполнения запроса (включая планирование, если не используется <code>SET anl_separate_planning_info = on;</code> или аналогичная опция).</li></ul> | |||
</li></ul> | |||
</li></ul> | |||
Анализ вывода <code>EXPLAIN</code> (особенно <code>EXPLAIN ANALYZE</code>) помогает понять, почему запрос выполняется медленно, используются ли индексы, корректны ли оценки оптимизатора, и какие части запроса являются “бутылочным горлышком”. | |||
Текущая версия от 09:16, 12 мая 2025
Глава 6: Индексы и Выполнение Запросов
В предыдущих главах мы научились создавать таблицы, наполнять их данными и извлекать эти данные с помощью SQL-запросов. Теперь разберемся, как СУБД выполняет эти запросы “под капотом” и как можно ускорить их выполнение с помощью индексов. Эта глава основана на материалах Лекций 6: “Индексы. Выполнение запросов” и Лекции 7: “Выполнение запросов”.
Часть 1: Повышение Производительности Запросов
Когда данных в таблицах становится много (тысячи, миллионы строк), даже простые запросы могут выполняться долго. Почему? Потому что в самом простом случае СУБД приходится последовательно просматривать всю таблицу, чтобы найти нужные строки (это называется полное сканирование таблицы или Sequential Scan).
Способы повышения производительности:
- Грамотное проектирование модели данных: Это основа. Если модель данных изначально плохо спроектирована (например, ненормализована, что приводит к избыточности, или наоборот, излишне нормализована, что требует множества сложных соединений для простых задач), то оптимизировать запросы будет очень сложно.
- Использование индексов: Создание дополнительных структур данных, которые позволяют СУБД быстро находить нужные строки по значениям в определенных столбцах, не просматривая всю таблицу. (Основная тема этой главы).
- Оптимизация запросов: Переписывание SQL-запросов так, чтобы они были более понятны оптимизатору СУБД и могли быть выполнены более эффективно (например, избегая ненужных соединений или используя
EXISTS
вместоIN
в некоторых случаях). - Настройка физических параметров СУБД: Оптимизация конфигурации сервера 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
:- СУБД обращается к структуре индекса по
StudID
. - Если индекс, например, B-дерево (значения отсортированы), СУБД быстро находит значение 18 (например, используя бинарный поиск или навигацию по дереву).
- Из индекса СУБД получает указатели (TID) на строки в основной таблице, где
StudID = 18
. - СУБД читает только эти конкретные строки из основной таблицы, используя полученные указатели. Это значительно быстрее, чем читать всю таблицу.
- СУБД обращается к структуре индекса по
Пример (слайды 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
|
- Запрос:
SELECT * FROM STUDENT WHERE StudID = 18;
- СУБД смотрит в индекс.
- Поскольку индекс отсортирован (в нашем примере это простой список, но представьте более сложную структуру), СУБД быстро находит первое вхождение
18
. - Берет указатель и читает строку
18, 54, Eugene Serov
. - Переходит к следующему значению
18
в индексе. - Берет указатель и читает строку
18, 63, Petr Menchikov
. - Переходит к следующему значению
18
в индексе. - Берет указатель и читает строку
18, 72, Roman Klever
. - Следующее значение в индексе уже
23
(или больше 18). Поиск завершен.
Вместо сканирования всех 12 строк таблицы, мы быстро нашли 3 нужные строки через индекс.
Важные моменты:
- Индексы работают неявно: Вам не нужно указывать в
SELECT
-запросе, какой индекс использовать. Оптимизатор СУБД сам решает, использовать ли индекс и какой именно, на основе статистики данных и стоимости выполнения разных планов. - Иногда СУБД может не использовать индекс, даже если он есть. Например, если запрос выбирает очень большую часть таблицы (проще прочитать всю таблицу), или если условия в
WHERE
таковы, что индекс неприменим (например, функция над индексированным столбцомWHERE upper(StudName) = 'ИВАНОВ'
).
Когда Индексы Полезны?
Индексы ускоряют операции, включающие:
- Поиск строк по условиям (
WHERE
): Особенно для условий равенства (=
), диапазонных запросов (>
,<
,BETWEEN
) по индексированному столбцу. - Соединения таблиц (
JOIN
): Индексы по столбцам, участвующим в условииON
, значительно ускоряют поиск совпадающих строк в соединяемых таблицах. Первичные и уникальные ключи автоматически индексируются в PostgreSQL, что ускоряет соединения по ним. - Поиск минимального/максимального значения (
MIN()
,MAX()
): Если столбец индексирован (например, B-деревом), СУБД может просто взять первое/последнее значение из индекса, не сканируя таблицу. - Сортировку (
ORDER BY
): Если порядок сортировки совпадает с порядком индекса (или обратен ему, и индекс это поддерживает), СУБД может избежать фактической сортировки данных, просто прочитав строки в порядке индекса. - Группировку (
GROUP BY
): Индексы могут помочь СУБД быстрее найти строки, относящиеся к одной группе.
Недостатки Индексов:
- Занимают место на диске и в памяти: Индекс — это дополнительная структура данных. Чем больше индексов, тем больше места они занимают.
- Замедляют операции записи (
INSERT
,UPDATE
,DELETE
): При изменении данных в индексированном столбце или добавлении/удалении строки СУБД должна обновить не только саму таблицу, но и все связанные с ней индексы. Это добавляет накладные расходы. Чем больше индексов на таблице, тем медленнее будут операции записи.- Пример (слайд 23-24):
DELETE FROM STUDENT WHERE GroupID = 54;
Если поGroupID
есть индекс, то СУБД должна не только удалить строку из таблицыSTUDENT
, но и удалить соответствующие записи из индекса поGroupID
, а также, возможно, из индекса поStudID
(еслиStudID
был ключом этого индекса и строка удаляется).
- Пример (слайд 23-24):
- Неэффективны на маленьких таблицах: Если таблица настолько мала, что целиком помещается в несколько блоков памяти, СУБД прочитает ее полностью быстрее, чем будет обращаться к индексу.
- Неэффективны при выборке больших объемов данных: Если запрос выбирает значительную часть таблицы (например,
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 поддерживает несколько типов индексов, оптимизированных для разных задач.
- 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):
- Запрос:
SELECT * FROM STUDENT WHERE StudID = 85;
- Начинаем с корневого узла (например,
[3, 30, 63]
). 85 > 63
, идем по правой ветке к узлу (например,[63, 76, 88]
).76 < 85 <= 88
, идем по ветке, соответствующей диапазону (88), к листовому узлу (например, содержащему ключи[76(tid), 85(tid)]
).- В листовом узле находим ключ
85
и соответствующийtid
. - По
tid
читаем строку из таблицы.
- Запрос:
- Пример диапазонного поиска (StudID >= 85, слайды 42-43):
- Аналогично находим первый ключ
85
. - Далее, благодаря связям между листовыми узлами, последовательно читаем следующие листовые узлы (
[88(tid), 95(tid)]
и т.д.), пока условие выполняется.
- Аналогично находим первый ключ
- Используется по умолчанию (
- Хеш-индекс (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
:- Вычисляется
Hash_Function(23, 2)
->...2345
. - Смотрим в корзину
2345
. - Для каждой строки (
tid1
,tid3
) в этой корзине проверяем точное совпадение исходных значенийStudID
иGroupID
(т.к. в корзине могут быть коллизии).
- Вычисляется
- Значения:
- Преимущества: Очень быстрый для операций поиска по точному равенству (
=
). - Недостатки:
- Бесполезен для диапазонных запросов, сортировки, поиска по префиксу.
- Менее эффективен при большом количестве коллизий (когда много разных значений попадают в одну корзину).
- Когда использовать: Только если запросы к столбцу — это исключительно проверки на точное равенство (
WHERE attr = 1
), и другие операции не нужны. B-Tree обычно является более гибким выбором.
- GiST (Generalized Search Tree - Обобщенное дерево поиска):
- Гибкая инфраструктура для создания индексов для сложных типов данных (геометрические данные, диапазоны, некоторые виды текстового поиска).
- Позволяет индексировать операции типа “пересекается”, “содержит”.
- Используется расширениями, такими как PostGIS (для геоданных) и
pg_trgm
(для поиска по похожести строк - триграммам).
- GIN (Generalized Inverted Index - Обобщенный инвертированный индекс):
- Оптимизирован для индексации составных значений, где одно значение в столбце может содержать несколько “ключей” (например, массив слов в документе, элементы массива
jsonb
, элементыhstore
). - Хранит карту “ключ из составного значения -> список строк, где этот ключ встречается”.
- Преимущества: Очень эффективен для поиска строк, содержащих определенные значения внутри составного типа (например, найти все документы, содержащие слово ‘postgresql’).
- Используется для полнотекстового поиска, индексации
jsonb
(операторы?
,?|
,?&
),hstore
, массивов (операторы@>
,<@
,&&
).
- Оптимизирован для индексации составных значений, где одно значение в столбце может содержать несколько “ключей” (например, массив слов в документе, элементы массива
Часть 4: Выполнение Запросов и Оптимизация
(Этот раздел повторяет и немного расширяет информацию из предыдущей версии методички, основываясь на слайдах Лекции 7).
Декларативность SQL: Пользователь указывает, что он хочет получить, а не как это сделать. СУБД сама решает, как выполнить запрос.
Этапы выполнения запроса в PostgreSQL:
- Разбор запроса (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).
- Генерирует множество возможных планов выполнения для дерева запроса, используя:
- Выполнение плана (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
. Этот план обычно эффективнее, так как соединяется меньший объем данных.
- План 1 (сначала JOIN, потом WHERE):
Материализация vs Конвейерная Обработка (Pipelining):
- Материализация: Результат промежуточной операции полностью вычисляется и сохраняется во временную структуру, затем передается следующей операции.
- Конвейерная обработка: Как только первая операция производит первую строку результата, эта строка немедленно передается на вход следующей операции. Экономит ресурсы и время. Оптимизатор старается использовать конвейер. Левосторонние деревья планов хорошо подходят для конвейерной обработки (результат предыдущего соединения сразу идет на вход следующего).
Советы по построению (оптимизации) планов (что пытается делать оптимизатор):
- Использовать конвейерную обработку (левосторонние планы, избегать блокирующих операций типа полной сортировки без необходимости).
- Делать выборку как можно раньше.
- Делать проекцию как можно раньше.
- Грамотно планировать соединения (порядок, алгоритмы).
- Цель: Уменьшение размеров промежуточных данных => уменьшение числа операций чтения/записи во внешнюю память.
Влияние индексов на планы: Индексы могут кардинально изменить выбранный план, позволяя заменить полное сканирование таблицы (Seq Scan) на более эффективное индексное сканирование (Index Scan) или улучшить производительность соединений (Index Nested Loop Join).
Стоимость плана: Складывается из стоимости чтения входных/промежуточных таблиц, записи промежуточных данных (при материализации), сортировки и других операций.
Часть 5: Алгоритмы Выполнения Соединений (JOIN
)
(Детальное описание алгоритмов было в предыдущей версии, здесь краткое повторение с акцентом на примеры из Лекции 7)
- 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
, и так далее.
- Пример (слайды 54-70 Лекции 7): Таблицы
- Block Nested Loop: Внешнее отношение
R
читается блоками (несколько страниц). Для каждого блокаR
, сканируется вся внутренняя таблицаS
. Уменьшает число полных сканированийS
.- Пример (слайды 88-91 Лекции 7): Если в буфере помещается блок из 4 страниц
Groups
, то для этого блока один раз сканируется вся таблицаStudents
. Затем загружается следующий блокGroups
и снова сканируется всяStudents
. Эффективнее, если внешняя таблица меньше внутренней.
- Пример (слайды 88-91 Лекции 7): Если в буфере помещается блок из 4 страниц
- Index Nested Loop: Если по столбцу соединения во внутренней таблице
S
есть индекс. Для каждой строкиr
изR
выполняется быстрый поиск по индексу вS
.
- 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)
: Условие, применяемое к индексу.
- Вывод (слайд 103-104):
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
) помогает понять, почему запрос выполняется медленно, используются ли индексы, корректны ли оценки оптимизатора, и какие части запроса являются “бутылочным горлышком”.