6.5.6 Оптимізація бази даних за допомогою індексів
Індекси у базах даних - це спеціальні структури, які прискорюють пошук даних у таблиці. Вони працюють аналогічно покажчику у книзі: замість переглядати всю таблицю, СУБД використовує індекс для швидкого знаходження потрібних записів.
Без індексів пошук потрібних рядків потребує повного перебору (Full Table Scan), що неефективно великих таблиць. Індексація дозволяє значно скоротити кількість операцій читання, покращуючи продуктивність.
Приклад без індексу:
SELECT * FROM users WHERE email = 'user@example.com';
Якщо e-mail не індексований, СУБД переглядає всю таблицю 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 stat> = '< ^>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) допомагає уникнути деградації продуктивності.
Дотримуючись цих принципів, можна значно прискорити роботу бази даних та підвищити ефективність системи.