Налаштування індексів бази даних для оптимізації веб-застосунку

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Налаштування індексів бази даних для оптимізації веб-застосунку
Середня
від 1 робочого дня до 3 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Оптимізація індексів бази даних для веб-додатків

Індекс — найшвидший спосіб прискорити запит. І одночасно найшвидший спосіб сповільнити запис, займати десятки гігабайт і ввести планувальника в оману. Розберемо, які індекси додавати, а які видаляти.

Типи індексів у PostgreSQL

B-tree — дефолт, підходить для рівності, діапазонів, ORDER BY, LIKE 'prefix%'.

GIN — для масивів, JSONB, tsvector (повнотекстовий пошук), оператори @>, ?, @@.

GiST — для геометричних типів, range типів, повнотексту (альтернатива GIN з меншим розміром, повільніше при побудові).

BRIN — для дуже великих таблиць із корельованими даними (метрики за часом, логи). Мінімальний розмір, повільніше GIN/B-tree при пошуку.

Hash — тільки для рівності (=). Рідко потрібен, B-tree зазвичай краще.

Обов'язкові індекси

Усі FK-колонки — без них DELETE/UPDATE батьківського запису викликає sequential scan дочірної таблиці:

-- PostgreSQL автоматично індексує PK, але не FK
CREATE INDEX idx_products_category_id  ON products  (category_id);
CREATE INDEX idx_order_items_order_id  ON order_items (order_id);
CREATE INDEX idx_order_items_product_id ON order_items (product_id);
CREATE INDEX idx_comments_post_id      ON comments   (post_id);
CREATE INDEX idx_comments_user_id      ON comments   (user_id);

Унікальні обмеження — автоматично створюють індекс:

ALTER TABLE users    ADD CONSTRAINT uq_users_email   UNIQUE (email);
ALTER TABLE products ADD CONSTRAINT uq_products_slug UNIQUE (slug);

Складені індекси

Порядок колонок критичний. Правило: equality спочатку, range/sort в кінці.

-- Запит: WHERE status = 'published' AND created_at > '2024-01-01' ORDER BY created_at DESC
-- Правильний складений індекс:
CREATE INDEX idx_products_status_created ON products (status, created_at DESC);

-- Неправильно: range умова спочатку — індекс частково використовується
CREATE INDEX idx_products_created_status ON products (created_at, status); -- гірше

Перевірка: EXPLAIN (ANALYZE, BUFFERS) SELECT ... WHERE status = 'published' ORDER BY created_at DESC LIMIT 20;

Очікуємо: Index Scan using idx_products_status_created з rows removed by filter: 0 (ідеально) або мінімальна кількість видалених рядків.

Часткові індекси

Частковий індекс покриває підмножину рядків — менший розмір, швидша побудова, кращий selectivity:

-- Індекс тільки для опублікованих продуктів
CREATE INDEX idx_products_published_created
    ON products (created_at DESC)
    WHERE status = 'published';

-- Індекс для незавершених замовлень (їх мало)
CREATE INDEX idx_orders_pending
    ON orders (user_id, created_at DESC)
    WHERE status IN ('pending', 'processing');

-- Для м'якого видалення: індекс по активним записам
CREATE INDEX idx_users_active_email
    ON users (email)
    WHERE deleted_at IS NULL;

Частковий індекс використовується тільки коли WHERE умова запиту включає умову індексу.

Покриваючі індекси (INCLUDE)

PostgreSQL 11+ підтримує INCLUDE — додає колонки до leaf-сторінок без впливу на порядок:

-- Запит: SELECT id, title, price FROM products WHERE status = 'published' ORDER BY created_at DESC
-- Покриваючий індекс — не потрібен heap fetch
CREATE INDEX idx_products_published_cover
    ON products (status, created_at DESC)
    INCLUDE (id, title, price);

Запит виконується через Index Only Scan — дані беруться прямо з індексу, без читання heap.

GIN для JSONB

-- specs: {"ram": "16GB", "storage": "512GB", "os": "linux"}
CREATE INDEX idx_products_specs ON products USING GIN (specs);

-- Пошук за наявністю ключ-значення
SELECT * FROM products WHERE specs @> '{"os": "linux"}';

-- Пошук за наявністю ключа
SELECT * FROM products WHERE specs ? 'ram';

-- GIN з jsonb_path_ops — тільки для @>, менший розмір
CREATE INDEX idx_products_specs_path ON products USING GIN (specs jsonb_path_ops);

Виявлення невикористовуваних індексів

-- Індекси, які ніколи не використовувалися (з моменту останнього pg_stat_reset)
SELECT
    schemaname,
    tablename,
    indexname,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
    idx_scan,
    idx_tup_read,
    idx_tup_fetch
FROM pg_stat_user_indexes
WHERE idx_scan = 0
  AND schemaname = 'public'
ORDER BY pg_relation_size(indexrelid) DESC;

Індекси з idx_scan = 0 — кандидати на видалення. Виключення: унікальні індекси (потрібні для перевірки обмежень при INSERT).

Виявлення дублюючих індексів

SELECT
    t.relname AS table_name,
    ix1.relname AS index1,
    ix2.relname AS index2,
    array_to_string(a1.attnames, ', ') AS columns1,
    array_to_string(a2.attnames, ', ') AS columns2
FROM pg_index i1
JOIN pg_index i2 ON i1.indrelid = i2.indrelid AND i1.indexrelid < i2.indexrelid
JOIN pg_class t  ON t.oid = i1.indrelid
JOIN pg_class ix1 ON ix1.oid = i1.indexrelid
JOIN pg_class ix2 ON ix2.oid = i2.indexrelid
CROSS JOIN LATERAL (
    SELECT array_agg(a.attname ORDER BY ordinality) AS attnames
    FROM unnest(i1.indkey) WITH ORDINALITY AS u(attnum, ordinality)
    JOIN pg_attribute a ON a.attrelid = i1.indrelid AND a.attnum = u.attnum
) a1
CROSS JOIN LATERAL (
    SELECT array_agg(a.attname ORDER BY ordinality) AS attnames
    FROM unnest(i2.indkey) WITH ORDINALITY AS u(attnum, ordinality)
    JOIN pg_attribute a ON a.attrelid = i2.indrelid AND a.attnum = u.attnum
) a2
WHERE i1.indkey[0] = i2.indkey[0]   -- перша колонка збігається
ORDER BY t.relname;

Створення індексів без блокування

В production додаємо індекси тільки через CONCURRENTLY:

CREATE INDEX CONCURRENTLY idx_products_new ON products (new_column);

Недоліки CONCURRENTLY: займає більше часу (два проходження), не можна в транзакції. Але не блокує INSERT/UPDATE/DELETE під час побудови.

Bloat та rebuild

З часом індекси фрагментуються. Перевіряємо вздування:

-- через pgstattuple розширення
SELECT * FROM pgstattuple('idx_products_status_created');
-- dead_tuple_percent > 20% — пора REINDEX
-- Перебудова без блокування (PG 12+)
REINDEX INDEX CONCURRENTLY idx_products_status_created;

Графіки

Аудит індексів існуючого проекту (невикористовувані, дубліки, відсутні FK-індекси, рекомендації): 1 день. Розробка та додавання оптимальних індексів під конкретний набір запитів: 1–2 дні.