digital-garden/source/lecture/Доклад. Индексы в PostgreSQL. Как понять, что создавать.md

144 lines
15 KiB
Markdown
Raw Normal View History

---
parents:
- "[[Java конференция Joker]]"
aliases:
tags:
- maturity/🌱
- type/source/lecture
- type/source/best
date: 2022-10-14
---
**Организатор**:: [[../../meta/organizations/JUG Ru Group|JUG Ru Group]]
**Конференция**:: [[../conference/Конференция. Jpont 2022|Jpont 2022]]
**Автор**:: [[../../meta/people/Сальников Андрей|Сальников Андрей]]
**Ссылка**:: [Андрей Сальников — Индексы в PostgreSQL. Как понять, что создавать - YouTube](https://www.youtube.com/watch?v=ju9F8OvnL4E)
***
![Андрей Сальников — Индексы в PostgreSQL. Как понять, что создавать - YouTube](https://www.youtube.com/watch?v=ju9F8OvnL4E)
***
**О чем доклад:** Любой разработчик знает, что индексы — это мощный инструмент, который может улучшить работу запросов в базе данных и, как следствие, сократить отклик приложения или сервиса на внешние запросы. Но опыт Андрея, как ДБА, показывает, что у разработчиков нет понимания, какой, когда и из каких соображений можно создавать индекс. Спикер приведет простые и понятные примеры, которые вы сможете легко повторить на своих реальных базах данных.
В докладе в основном говорится об [[../../dev/database/Online Transaction Processing|OLTP]] нагрузке и объем баз данных от 20 Гб до 10 Тб.
## Тезисы
-
## Конспект
Многие разработчики не проводят исследовательскую работу перед созданием индексов и создают их как считают правильно, и не всегда это мнение совпадает с реальностью.
Первым делом стоит ознакомиться с документаций: [PostgreSQL: Documentation: 16: CREATE INDEX](https://www.postgresql.org/docs/current/sql-createindex.html)
Прежде чем создавать индексы, нужно понять что такое индексы. Фактически это легализованные косты для [[../../dev/database/Оптимизация SQL запросов|ускорения SQL запросов]]. В PostgreSQL индекс для первичного ключа индекс создается автоматически.
**Какие накладные расходы от индексов?**
- Замедление операций вставки и обновлений. Так как необходимо будет перестраивать индекс при вставке новых значений. Но это должно быть не так страшно, так как профиль нагрузки на реляционную базу данных выглядит следующим образом: 80% запросов это чтение, 20% запросов это запись. Если запросов на запись больше, то возможно реляционная база данных вам не подходит.
- Дополнительные объемы дискового пространства для хранения индекса. Размер индексов на таблицу в половину размера таблицы считается нормальным и оптимальным. Если размер всех индексов таблицы приближается или становится больше, значит что-то идет не так.
- Усложненное технического обслуживание. Индексы пухнут и переодически их нужно пересоздавать. В каких-то СУБД это происходит автоматически. Пересоздание индекса сложный процесс и может повлечь недоступность сервиса.
При создании индекса нужно провести анализ. Иначе можно получить все накладные расходы, и не получить преимущества.
**Что нужно для создания индекса?**
- Ориентироваться только на продуктовое окружение, так как тестовые окружения не соответствуют реальности.
- Собрать статистику нагрузки на БД от запросов. Чтобы понять какие запросы действительно требуют оптимизации. - [[../../dev/database/Плохой SQL запрос|Плохой SQL запрос]]. Для этого можно использовать различные инструменты
- [[../../dev/database/postgresql/pg_stat_statements|pg_stat_statements]]
- pgBadger - использовать с осторожностью. Собирает статистику из логов. Но в логи попадают не все запросы.
- Иметь примеры запросов с параметрами. Это необходимо для проверки проведенных оптимизаций.
- Нужно уметь читать статистику распределения данных - [[../../dev/database/postgresql/Таблица статистик pg_stats|Таблица статистик pg_stats]]. Это нужно, чтобы понимать как планировщик БД будет строить план выполнения запроса.
- По умолчанию PostgrteSQL использует для сбора статистики только 30k строк из таблицы. Из-за этого статистика может расходиться с реальностью. И нужно уметь собирать более полную статистику вручную. Когда есть подозрения, что в статистике есть существенные промахи.
Далее идет описание типов индексов, которые есть в Postgres, и которые перечислены в моей заметке [[../../dev/database/postgresql/Индекс в PostgreSQL#Типы индексов|Индекс в PostgreSQL]].
Для OLTP нагрузки не стоит использовать параллельное выполнение запроса. Так как это значит, что мы забираем ядро процессора у другого запроса. В OLTP нагрузке каждый запрос должен выполняться на одном процессе так быстро, как только возможно.
## Практика
![](../../meta/files/images/Pasted%20image%2020240331092706.png)
![](../../meta/files/images/Pasted%20image%2020240331092753.png)
- У таблицы есть первичный (bigint) и внешний ключи.
- Таблица имеет колонки различных типов данных.
- Таблица ссылается сама на себя, но это сделано для удобства доклада. Те же самые выводы распространятся и на связи с другими таблицами.
- Количество кортежей 10_000_000
- Индекс по первичному ключу занял 1/4 (214 Mb) от размера таблицы (816 Mb)
### Удаляем строку
Удаляем строку по первичному ключу.
![600](../../meta/files/images/Pasted%20image%2020240331093028.png)
В удалении задействован первичный ключ, поэтому используется поиск по индексу. Но по итогу ==самый долгий этап это проверка внешних связей с таблицей==. По итогу запрос вроде бы быстрый, но он не оптимальный, он потребляет намного больше ресурсов сервера, чем должен.
Под капотом для поиска внешних связей используется полное сканирование таблицы (Seq Scan). В данном примере специально включено паралеллельное выполнение, но это все равно занимает много времени.
![600](../../meta/files/images/Снимок%20экрана%202024-03-31%20в%2009.32.34.png)
Поэтому важно не забывать [[../../dev/database/Индекс для внешнего ключа таблицы БД|создавать индексы на Foreign Key]], чтобы различные проверки БД выполнялись с использованием индекса.
Добавляем индекс на FK и проверяем результат:
![600](../../meta/files/images/Pasted%20image%2020240331093524.png)
Время выполнения было 281 ms, а стало 0.1 ms!
Большинство разработчиков на этом этапе успокоится, но можно ли сделать лучше?
#### Смотрим статистику
- [Таблица статистик pg_stats](../../dev/database/postgresql/Таблица%20статистик%20pg_stats.md)
![600](../../meta/files/images/Снимок%20экрана%202024-03-31%20в%2009.36.56.png)
В данном случае у нас 92% значений в колонке это null значения.
При большом значении `null_frac` нас уже меньше волнуют остальные параметры. Основываясь на этой информации мы можем уменьшить размер индекса. Для этого изменим запрос на создание индекса, добавив `where fk_id is not null`.
![600](../../meta/files/images/Pasted%20image%2020240331094502.png)
Это не ускорит нам запрос, но таким образом у нас получилось сжать индекс в 14 раз:
![500](../../meta/files/images/Pasted%20image%2020240331095259.png)
### Поиск записей по статусу
Возьмем типичную табличку, в которой есть какие-то статусы мы хотим находить данные по этому статусу.
![300](../../meta/files/images/Pasted%20image%2020240331095959.png)
![600](../../meta/files/images/Pasted%20image%2020240331100144.png)
Часто появляется желание сделать индекс по полю статуса:
![600](../../meta/files/images/Снимок%20экрана%202024-03-31%20в%2010.07.02.png)
Но по факту мы индексируем поле, которое имеет небольшую [селективность](../../dev/database/Селективность%20колонки.md). Такой индекс не эффективный.
Хороший вариант в данном случае:
![600](../../meta/files/images/Снимок%20экрана%202024-03-31%20в%2010.13.39.png)
Почти идеальный:
![600](../../meta/files/images/Снимок%20экрана%202024-03-31%20в%2010.15.06.png)
![500](../../meta/files/images/Pasted%20image%2020240331101612.png)
2024-10-31 18:45:22 +03:00
Идеальный. Совмещаем и [[../../dev/database/postgresql/Составной индекс в PostgreSQL|составной индекс]] и [[../../dev/database/Частичный индекс|Частичный индекс]]
![[../../meta/files/images/Pasted image 20241021225124.png]]
### Онлайн статистика
Представим, что у вас небольшой продукт и PostgreSQL у вас единственное хранилище данных. И у вас есть задача показывать какую-то аналитику. Обычно для аналитических запросов хорошо использовать колоночные БД. Но так как ресурсов проекта у вас не много и задач на анализ не так много, то позволить себе такое вы не можете.
Можно сделать небольшой шаг в сторону колоночных БД в PostgreSQL за счет индексов.
В данном случае будем показывать сколько фруктов продано. Если использовать в лоб операции coun и sum, то они будут занимать достаточно много времени.
![[../../meta/files/images/Pasted image 20241021225909.png]]
Если не используем индексы
![[../../meta/files/images/Pasted image 20241021230132.png]]
Лучшее решение здесь это расчитывать заранее агрегирующие результаты за старые данные и обновлять их раз в сутки, а наиболее актуальные (за последние сутки) расчитывать отдельно и приплюсовывать к историческим.
Попробуем придумать индекс. Посмотрим на статистику по полю item. Видим, что больше половины таблицы занимает значение "дыня", значит с дыней придется прочитать половину таблицы.
![[../../meta/files/images/Pasted image 20241022212435.png]]
Поэтому первым стоит указать поле `created_at`, учитывая что оно участвует в запросе, а вторым полем добавить `item`.
![[../../meta/files/images/Pasted image 20241022212658.png]]
Но можно пойти еще дальше и использовать include(amount). Таким образом мы присоединим поле к индексу, оно не будет индексироваться. То есть значение amount будет лежать рядом, не нужно будет доставать значение из таблицы. Также используем where, чтобы отрезать все ненужные колонки.
![[../../meta/files/images/Pasted image 20241022213322.png]]
## Ответы на вопросы
- Автор в незнакомых базах смотрит
- на соотношение размеров таблицы и индексов.
- на количество чтений индексов
- дальше уже смотрит на то как были созданы индексы