6.5.6 Оптимизация баз данных с помощью индексов
Индексы в базах данных — это специальные структуры, которые ускоряют поиск данных в таблице. Они работают аналогично указателю в книге: вместо того чтобы просматривать всю таблицу, СУБД использует индекс для быстрого нахождения нужных записей.
Без индексов поиск нужных строк требует полного перебора (Full Table Scan), что неэффективно для больших таблиц. Индексация позволяет значительно сократить количество операций чтения, улучшая производительность.
Пример без индекса:
SELECT * FROM users WHERE email = 'user@example.com';
Если email
не индексирован, СУБД просматривает всю таблицу users
.
Пример с индексом:
CREATE INDEX idx_users_email ON users(email);
Теперь поиск выполняется гораздо быстрее.
Учтите: хотя индексы ускоряют чтение данных, у них есть и недостатки:
- Замедление операций записи – при вставке, обновлении или удалении записи индексы нужно обновлять.
- Дополнительное потребление памяти – индексы занимают место на диске.
- Не всегда ускоряют запросы – если запрос выбирает значительную часть данных, индекс может не дать прироста.
Поэтому важно разумно подходить к индексации, выбирая только те столбцы, по которым часто выполняются поиск и сортировка.
Дополнительно: у нас имеется инструкция о создании индекса на практике. Ознакомиться с ней Вы можете по адресу.
Основные виды индексов
Индексы в реляционных базах данных бывают разных типов, каждый из которых подходит для определенных сценариев. Разберем наиболее распространенные.
B-Tree индексы
B-Tree (сбалансированные деревья) — самый распространенный тип индексов, используемый по умолчанию в PostgreSQL, MySQL (InnoDB) и других СУБД.
Как работают:
- Данные хранятся в виде сбалансированного дерева, что обеспечивает логарифмическую сложность поиска (
O(log n)
). - Поддерживают эффективный поиск, сортировку и диапазонные запросы.
Пример использования:
CREATE INDEX idx_users_lastname ON users(last_name);
Теперь запросы вроде WHERE last_name = 'Smith'
или ORDER BY last_name
выполняются быстрее.
Когда использовать:
- Для поиска по
=
(равенство). - Для
BETWEEN
,>
,<
(диапазонные запросы). - Для
ORDER BY
иGROUP BY
.
Когда неэффективны:
- На малых таблицах (перебор быстрее).
- При частых вставках и удалениях (затраты на поддержку дерева).
Hash индексы
Хеш-индексы работают по принципу отображения значений в хеш-таблицу. Присутствовали в MySQL (InnoDB) до версии 8.0, после доступны только для PostgreSQL.
Как работают:
- Очень быстрые при точном поиске (
=
). - Не поддерживают диапазонные запросы (
>
,<
,BETWEEN
).
Пример в PostgreSQL:
CREATE INDEX idx_users_email_hash ON users USING HASH(email);
Теперь WHERE email = 'user@example.com'
выполняется быстрее.
Когда использовать:
- Только для точного поиска (
=
). - Когда B-Tree избыточен.
Когда неэффективны:
- Если требуется сортировка или диапазонный поиск.
GIN
GIN (обобщенный инвертированный индекс) — предназначен для быстрого поиска по массивам, JSONB, полнотекстовым данным и другим структурам с множеством значений в одном поле. Доступны только для PostgreSQL.
Как работают:
- Оптимизирован для поиска по множеству значений в одном столбце.
- Используется для полнотекстового поиска и JSONB.
- Подходит для
@>
,<@
,?
,?|
,?&
в PostgreSQL.
Пример GIN для JSONB:
CREATE INDEX idx_users_data ON users USING GIN(data);
Теперь WHERE data @> '{"age": 30}'
выполняется быстрее.
GiST
GiST (обобщенное дерево поиска) — гибкий индекс, который хорошо подходит для многомерных данных, географических объектов и нечеткого поиска. Доступны только для PostgreSQL.
Как работают:
- Гибкий, подходит для многомерных данных (геоиндексы, подобие строк).
- Используется в PostgreSQL для pg_trgm (поиск по подстрокам).
Пример GiST для поиска по подстрокам:
CREATE INDEX idx_users_name_gist ON users USING gist(last_name gist_trgm_ops);
Теперь WHERE last_name LIKE '%mit%'
работает быстрее.
BRIN индексы
BRIN (Block Range INdex) полезен для огромных таблиц, где данные отсортированы по какому-то признаку. Доступны только для PostgreSQL.
Как работают:
- Индекс хранит минимальное и максимальное значения для блоков строк.
- Очень маленькие по размеру.
Пример для временных данных:
CREATE INDEX idx_logs_date ON logs USING BRIN(event_date);
Теперь WHERE event_date BETWEEN '2024-01-01' AND '2024-02-01'
работает быстрее.
Когда использовать:
- Для больших таблиц (миллионы строк).
- Когда данные упорядочены (например, по дате).
Когда неэффективны:
- Если данные хаотично распределены.
Когда и какие индексы использовать
Индексы улучшают производительность выборки (SELECT
), но могут замедлять операции изменения (INSERT
, UPDATE
, DELETE
). Поэтому важно понимать, когда и какие индексы применять.
Индексы на первичных ключах и уникальных полях
При создании PRIMARY KEY
или UNIQUE
автоматически создается B-Tree индекс.
Пример:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE
);
Это означает, что:
PRIMARY KEY
(id) создаст индекс для быстрого поиска по id.UNIQUE
(email) создаст индекс, предотвращающий дублирование.
Когда полезно:
- id часто используется в
JOIN
. - Поиск по email выполняется часто.
Индексация внешних ключей
Внешние ключи (FOREIGN KEY
) сами по себе не создают индексы автоматически (например, в PostgreSQL). Если по ним выполняются JOIN, индексация обязательна.
Пример:
CREATE INDEX idx_orders_user_id ON orders(user_id);
Теперь JOIN users ON orders.user_id = users.id
работает быстрее.
Когда использовать:
- Если внешний ключ участвует в
JOIN
. - Если часто используется в
WHERE
.
Индексы для фильтрации и сортировки
Индексация помогает, если запрос содержит WHERE
, ORDER BY
, GROUP BY
.
Пример:
CREATE INDEX idx_users_lastname ON users(last_name);
Теперь WHERE last_name = 'Smith' и ORDER BY last_name
работают быстрее.
Когда индексы вредны
- Редко используемые индексы. Если столбец редко используется в запросах, индекс только занимает место.
- Малые таблицы. Если таблица содержит менее 1000 строк, полный перебор (Seq Scan) быстрее индекса.
- Частые
INSERT
,UPDATE
,DELETE
. Индексы замедляют вставку и обновление. Например, если есть 5 индексов, каждое обновление должно изменять их все.
Пример проблемы:
UPDATE users SET email = 'new@example.com' WHERE id = 100;
Если email проиндексирован, обновление требует изменения индекса.
Решение:
- Избегать индексов на часто изменяемых столбцах.
- Использовать частичные индексы (ограничение индексации по WHERE).
Пример частичного индекса:
CREATE INDEX idx_active_users ON users(email) WHERE is_active = true;
Теперь индекс применяется только к активным пользователям.
Создание и управление индексами
Дополнительно: у нас имеется инструкция о создании индекса на практике. Ознакомиться с ней Вы можете по адресу. Работать с индексами удобнее всего с помощью phpMyAdmin, материал про которую у нас тоже имеется.
В этом разделе разберем основные операции с индексами: создание, удаление, автоматическое управление индексами в разных СУБД и специальные виды индексов.
Создание индексов (CREATE INDEX)
Индексы создаются с помощью CREATE INDEX
.
- Пример обычного B-Tree индекса:
CREATE INDEX idx_users_lastname ON users(last_name);
- Создание уникального индекса:
CREATE UNIQUE INDEX idx_users_email ON users(email);
- Создание многоколонного индекса:
CREATE INDEX idx_orders_date_status ON orders(order_date, status);
Такой индекс ускоряет:
WHERE order_date = '2024-01-01' AND status = 'shipped'
ORDER BY order_date, status
- Но не ускоряет поиск только по
status
.
Удаление и переименование индексов (DROP INDEX, ALTER INDEX)
Если индекс больше не нужен, его можно удалить:
DROP INDEX idx_users_lastname;
Или при необходимости переименовать:
ALTER INDEX idx_users_lastname RENAME TO idx_users_lname;
Автоматические индексы в разных СУБД
Некоторые СУБД создают индексы автоматически:
- PostgreSQL – создает индексы для PRIMARY KEY и UNIQUE, но не для FOREIGN KEY.
- MySQL (InnoDB) – автоматически индексирует PRIMARY KEY, UNIQUE и FOREIGN KEY.
- SQLite – индексирует PRIMARY KEY и UNIQUE.
Проверить индексы можно с помощью (пример для PostgreSQL):
SELECT * FROM pg_indexes WHERE tablename = 'users';
Покрывающие и частичные индексы
Обычный индекс хранит только ключевые столбцы. Покрывающий индекс позволяет добавить дополнительные данные, ускоряя SELECT
.
CREATE INDEX idx_users_email_include ON users(email) INCLUDE (name, created_at);
Теперь SELECT name, created_at FROM users WHERE email = 'user@example.com'
не требует обращения к таблице.
Частичные индексы
Индексировать только часть данных – полезно для больших таблиц.
CREATE INDEX idx_active_users ON users(email) WHERE is_active = true;
Теперь индекс применяется только к активным пользователям.
Анализ и оптимизация запросов с индексами
Индексы работают эффективно только при правильной настройке. Разберем, как анализировать их использование и какие ошибки могут снижать их эффективность.
Использование EXPLAIN и EXPLAIN ANALYZE
Перед оптимизацией важно понять, использует ли запрос индексы. Для этого применяется EXPLAIN:
EXPLAIN SELECT * FROM users WHERE email = 'user@example.com';
Результат покажет, будет ли использован Index Scan или Seq Scan (полный перебор).
Для детального анализа с измерением времени выполнения добавляют ANALYZE
:
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'user@example.com';
Это выполняет запрос и показывает реальные затраты.
Как понять, работает ли индекс
Если используется Seq Scan – индекс не применяется, возможные причины:
- Нет индекса на поле в
WHERE
. - Фильтрация возвращает слишком много строк (оптимальнее полный перебор).
- Условия запроса не соответствуют структуре индекса.
Если используется Index Scan или Index Only Scan – индекс задействован:
- Index Scan – индекс используется, но данные запрашиваются из таблицы.
- Index Only Scan – индекс полностью покрывает запрос (работает быстрее).
Проблемы с индексами и оптимизация
-
Селективность индекса. Индекс эффективен, если он отбрасывает большую часть данных. Если индексируется поле с небольшим количеством уникальных значений (например,
is_active = true/false
), индекс может не дать прироста. -
Правильный порядок полей в составных индексах. При использовании индекса на нескольких столбцах (
CREATE INDEX idx_orders ON orders(status, created_at)
) запросWHERE status = 'shipped' AND created_at > '2024-01-01'
будет эффективным, ноWHERE created_at > '2024-01-01'
индекс не использует.
Стратегии оптимизации индексов
- Удалять неиспользуемые индексы – можно проверить статистику индексного доступа:
SELECT * FROM pg_stat_user_indexes WHERE schemaname = 'public';
- Использовать
VACUUM ANALYZE
(PostgreSQL) – обновляет статистику планировщика запросов:
VACUUM ANALYZE;
-
Не индексировать столбцы с низкой селективностью – индекс на gender (м/ж) бесполезен.
-
Использовать частичные индексы – уменьшает размер индекса, ускоряя работу.
-
Перестраивать индексы (
REINDEX
) – удаляет фрагментацию, улучшая производительность:
REINDEX TABLE users;
Заключение
Оптимизация работы базы данных с помощью индексов — это баланс между ускорением выборки (SELECT
) и минимизацией накладных расходов на запись (INSERT
, UPDATE
, DELETE
).
Выводы и рекоммендации:
- Индексы ускоряют выборку данных, но требуют дополнительных ресурсов.
- Не все запросы используют индексы — важно анализировать
EXPLAIN ANALYZE
. - Тип индекса имеет значение –
B-Tree
универсален, ноGIN/GiST
подходят для специфичных задач. - Чрезмерная индексация вредна – слишком много индексов замедляет запись и потребляет память.
- Регулярное обслуживание индексов (
REINDEX
,VACUUM
) помогает избежать деградации производительности.
Соблюдая эти принципы, можно значительно ускорить работу базы данных и повысить эффективность системы.