Індекси у MS SQL Server
Індекси у MS SQL Server
Проблема: Повільні запити
Уявіть бібліотеку з мільйоном книг, розставлених у випадковому порядку. Вам потрібно знайти книгу "Кобзар" Тараса Шевченка. Скільки часу це займе?
Без каталогу (індексу): Ви переглядаєте КОЖНУ книгу по черзі. У гіршому випадку — всі мільйон книг.
З каталогом (індексом): Ви шукаєте в алфавітному каталозі "Шевченко" → отримуєте номер полиці → знаходите книгу за хвилини.
Так само працюють індекси в базі даних!
Демонстрація проблеми
-- Таблиця з 10 мільйонами користувачів БЕЗ індексу
CREATE TABLE Users (
user_id INT PRIMARY KEY,
email NVARCHAR(100),
first_name NVARCHAR(50),
last_name NVARCHAR(50),
registration_date DATE
);
-- Пошук користувача за email
SELECT * FROM Users WHERE email = 'ivan@example.com';
-- ⏱️ Час виконання: ~15 секунд
-- 🔍 Метод: TABLE SCAN (перегляд ВСІХ 10 млн рядків)
Що відбувається під капотом?
SQL Server послідовно перевіряє КОЖЕН рядок таблиці, поки не знайде потрібний email. Це називається Table Scan (повне сканування таблиці).
Тепер додамо індекс:
-- Створюємо індекс на стовпець email
CREATE INDEX IX_Users_Email ON Users(email);
-- Той самий запит
SELECT * FROM Users WHERE email = 'ivan@example.com';
-- ⏱️ Час виконання: ~0.001 секунди (у тисячі разів швидше!)
-- 🔍 Метод: INDEX SEEK (використання індексу)
Що таке індекс?
Аналогія з книгою
| Елемент книги | Елемент БД | Пояснення |
|---|---|---|
| Зміст | Індекс | Вказує, де знайти потрібну інформацію |
| Номери сторінок | Покажчики (Pointers) | Посилання на реальні дані |
| Сторінки з текстом | Дані таблиці | Реальна інформація |
| Алфавітний покажчик | Non-Clustered Index | Додаткова структура для пошуку за ключовими словами |
Ключові компоненти індексу
Внутрішня структура: Pages та Extents
Навіщо розробнику знати про Pages?
Питання: "Чому SQL Server читає 8 KB даних, навіть якщо мені потрібен лише 1 рядок розміром 100 байт?"
Відповідь: Бо SQL Server читає дані цілими сторінками (pages), а не окремими рядками!
- Чому широкі рядки сповільнюють запити
- Чому
SELECT *погана практика - Як індекси насправді працюють на диску
- Чому фрагментація — це проблема
Що таке Page (Сторінка)?
Розмір: 8 KB (8192 байти).
SQL Server ЗАВЖДИ читає та пише дані цілими pages, навіть для 1 рядка!
Візуалізація Page з реальними даними
Уявімо таблицю Employees:
CREATE TABLE Employees (
employee_id INT, -- 4 байти
first_name NVARCHAR(50), -- до 100 байт (Unicode)
last_name NVARCHAR(50), -- до 100 байт
salary DECIMAL(10,2), -- 5 байт
hire_date DATE -- 3 байти
);
-- Приблизно 212 байт на рядок
Скільки рядків влізе в одну page?
Page size = 8192 байти
- Header = 96 байт
- Row Offset Array = 36 байт
= 8060 байт для даних
Рядків на page: 8060 / 212 ≈ 38 рядків
Що зберігає Header (96 байт)?
┌──────────────────────────────────┐
│ PAGE HEADER (96 bytes) │
├──────────────────────────────────┤
│ • Page Type (data/index) │ ← Тип сторінки
│ • Object ID │ ← Якій таблиці належить
│ • Free Space (bytes) │ ← Скільки вільного місця
│ • Previous Page ID │ ← Подвійно зв'язаний список
│ • Next Page ID │ ← для швидкого сканування
│ • Slot Count │ ← Кількість рядків
└──────────────────────────────────┘
Реальний приклад: Вплив на продуктивність
-- ❌ ПОГАНО: Читає всі 212 байт * 38 рядків = 8060 байт
SELECT * FROM Employees WHERE employee_id = 123;
-- SQL Server:
-- 1. Читає ПОВНУ page (8 KB) з диска
-- 2. Шукає потрібний рядок
-- 3. Повертає ВСІ стовпці (навіть ті, що не потрібні)
-- Час: ~15ms (включаючи I/O з диска)
-- ✅ ДОБРЕ: Читає лише потрібні стовпці
SELECT employee_id, first_name, last_name
FROM Employees
WHERE employee_id = 123;
-- SQL Server:
-- 1. Все одно читає повну page (8 KB) - це фізичне обмеження
-- 2. Але НЕ передає зайві дані через мережу
-- 3. Якщо є covering index - може взагалі не читати data page!
-- Час: ~5ms (якщо data page вже в пам'яті)
-- ~0.1ms (якщо використовує covering index)
Index page може містити більше рядків, бо зберігає лише ключові стовпці:
- Data page: 38 рядків (212 байт кожен)
- Index page: ~200 рядків (40 байт кожен, лише email + ID)
Що таке Extent (Екстент)?
SQL Server виділяє дисковий простір екстентами, а не окремими pages.
┌─────────────────────────────────────────────────┐
│ EXTENT = 64 KB │
├──────┬──────┬──────┬──────┬──────┬──────┬──────┤
│ Page │ Page │ Page │ Page │ Page │ Page │ Page │
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │
│ 8KB │ 8KB │ 8KB │ 8KB │ 8KB │ 8KB │ 8KB │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┘
Навіщо потрібні Extents?
Швидше виділення пам'яті: Замість виділяти 8 KB → 8 KB → 8 KB, SQL Server виділяє одразу 64 KB блоком.
Mixed vs Uniform Extents: Практичний сценарій
Коли: Таблиця займає менше 64 KB (менше 8 pages).
-- Невелика таблиця настройок
CREATE TABLE AppSettings (
setting_name NVARCHAR(50),
setting_value NVARCHAR(200)
);
-- Вставлено 10 рядків ≈ 2 KB
INSERT INTO AppSettings VALUES ...;
Що відбувається:
┌─────────────── MIXED EXTENT ───────────────┐
│ Page 1: AppSettings (частина) │
│ Page 2: UserSettings (інша таблиця) │
│ Page 3: LogSettings (ще одна) │
│ Page 4: AppSettings (продовження) │
│ ... │
└────────────────────────────────────────────┘
Переваги: Економія місця (багато малих таблиць не марнують простір).
Недоліки: Фрагментація (потрібно читати pages з різних місць диска).
Коли: Таблиця займає більше 64 KB (більше 8 pages).
-- Велика таблиця користувачів
CREATE TABLE Users (
user_id INT PRIMARY KEY,
email NVARCHAR(100),
...
);
-- Вставлено 1,000,000 рядків
Що відбувається:
┌─────────── UNIFORM EXTENT #1 ──────────────┐
│ Page 1: Users row 1-38 │
│ Page 2: Users row 39-76 │
│ Page 3: Users row 77-114 │
│ ... ВСІ 8 pages лише для Users │
└────────────────────────────────────────────┘
┌─────────── UNIFORM EXTENT #2 ──────────────┐
│ Page 9: Users row 305-342 │
│ Page 10: Users row 343-380 │
│ ... │
└────────────────────────────────────────────┘
Переваги: Швидке читання (сусідні pages на диску).
Недоліки: Навіть для 65 KB даних виділиться повних 128 KB (2 extents).
Практичний кейс: Чому широкі рядки — це погано?
Проблема: У вас є таблиця з великими стовпцями.
CREATE TABLE Products (
product_id INT,
name NVARCHAR(100),
description NVARCHAR(MAX), -- ⚠️ До 2 ГБ!
specs NVARCHAR(MAX), -- ⚠️ До 2 ГБ!
price DECIMAL(10,2)
);
Що відбувається:
Рядок з NVARCHAR(MAX):
- Маленька description (500 байт) → зберігається IN-ROW (в page)
- Велика description (10 KB) → зберігається OFF-ROW (окремо)
┌──────── DATA PAGE ────────┐ ┌─── LOB PAGES ────┐
│ product_id: 1 │ │ description: │
│ name: "Laptop" │───→│ (10 KB текст) │
│ description: [pointer] │ │ │
│ price: 1299.99 │ └──────────────────┘
└───────────────────────────┘
Проблеми:
- Фрагментація: Дані розкидані по різних pages
- Повільний пошук: Потрібно читати data page + LOB pages
- Index не допоможе: Covering index НЕ може включити NVARCHAR(MAX)
-- ✅ Оптимізована структура
CREATE TABLE Products (
product_id INT PRIMARY KEY,
name NVARCHAR(100),
price DECIMAL(10,2)
);
CREATE TABLE ProductDetails (
product_id INT PRIMARY KEY,
description NVARCHAR(MAX),
specs NVARCHAR(MAX),
FOREIGN KEY (product_id) REFERENCES Products(product_id)
);
-- Тепер швидкий запит НЕ читає LOB pages:
SELECT product_id, name, price
FROM Products
WHERE price > 1000; -- ⚡ Швидко!
Як перевірити використання Pages?
-- Скільки pages займає таблиця?
SELECT
t.name AS TableName,
p.rows AS RowCount,
SUM(a.used_pages) AS UsedPages,
SUM(a.used_pages) * 8 / 1024.0 AS UsedMB,
SUM(a.used_pages) / 8.0 AS UsedExtents
FROM sys.tables t
INNER JOIN sys.partitions p ON t.object_id = p.object_id
INNER JOIN sys.allocation_units a ON p.partition_id = a.container_id
WHERE t.name = 'Users'
GROUP BY t.name, p.rows;
Приклад результату:
TableName RowCount UsedPages UsedMB UsedExtents
Users 1,000,000 26,316 205.28 3,289
Розрахунок:
- 1,000,000 рядків / 38 рядків на page ≈ 26,316 pages ✅
- 26,316 pages × 8 KB = 205 MB ✅
- 26,316 pages / 8 pages на extent ≈ 3,289 extents ✅
Чому це важливо для розробника: Резюме
- SELECT * — повільно: Навіть якщо потрібен 1 стовпець, SQL Server читає всю page (8 KB)
- Широкі рядки — погано: Менше рядків на page = більше I/O = повільніше
- Covering indexes — швидко: Менші рядки в index pages = більше рядків = менше I/O
- Нормалізація допомагає: NVARCHAR(MAX) у окремій таблиці = швидші основні запити
- Фрагментація = проблема: Розкидані pages по диску = повільне читання
B-Tree: Серце індексів
B-Tree (Balanced Tree) — це структура даних у вигляді збалансованого дерева, яка забезпечує швидкий пошук, вставку та видалення.
Властивості B-Tree
- Збалансованість: Всі листові вузли знаходяться на одній відстані від кореня
- Відсортованість: Ключі в кожному вузлі відсортовані
- Logarithmic Search: Пошук займає O(log N) часу
Структура B-Tree в SQL Server
Рівні дерева:
- Root Level (Level 2): Корінь дерева, точка входу
- Intermediate Levels (Level 1): Проміжні вузли для навігації
- Leaf Level (Level 0): Листові вузли з реальними даними або покажчиками
Приклад пошуку: Знайти значення 72
Крок 1: Root Page
→ 72 > 50 AND 72 < 100
→ Іде middle child (L1B)
Крок 2: Intermediate Page (L1B)
→ 72 > 60 AND 72 < 75
→ Іде right child (Leaf5)
Крок 3: Leaf Page (Leaf5)
→ Знайдено: 72 ✅
Всього операцій читання: 3 pages (замість сканування всіх pages!)
Це означає максимум 5 операцій читання для знаходження будь-якого рядка.
Кластерний індекс (Clustered Index)
Аналогія з реального життя: Енциклопедія
- Статті фізично впорядковані за алфавітом (А → Я)
- Щоб знайти "Київ", відкриваєте розділ "К" — стаття реально там
- Не потрібен окремий покажчик — сама книга вже впорядкована
- Ви НЕ можете мати ту саму енциклопедію впорядковану і за алфавітом, і за датами одночасно
Що таке Clustered Index?
Leaf level B-Tree містить реальні рядки таблиці, а не покажчики на них.
Ключові особливості
- Один на таблицю: Може бути лише ОДИН clustered index (дані можна фізично впорядкувати лише одним способом)
- Таблиця = Індекс: Дані таблиці І Є clustered index (не окрема структура!)
- Фізичний порядок: Рядки фізично зберігаються у порядку ключа індексу
- Автоматично створюється: На PRIMARY KEY (якщо не вказано інакше)
Структура Clustered Index
Ключова відмінність: Leaf pages містять ВСІ стовпці таблиці (ID, ім'я, прізвище, зарплата), а не лише ключ індексу!
Практичний приклад: Таблиця працівників
-- Таблиця без Clustered Index = HEAP (купа)
CREATE TABLE Employees (
employee_id INT,
first_name NVARCHAR(50),
last_name NVARCHAR(50),
salary DECIMAL(10,2)
);
INSERT INTO Employees VALUES
(105, 'Alice', 'Brown', 75000),
(12, 'Bob', 'Smith', 65000),
(501, 'Charlie', 'Davis', 85000),
(7, 'Diana', 'Wilson', 70000);
Як зберігається на диску (випадковий порядок):
Page 1: [ID=105: Alice Brown] [ID=12: Bob Smith]
Page 2: [ID=501: Charlie Davis] [ID=7: Diana Wilson]
Пошук WHERE employee_id = 501:
❌ TABLE SCAN: Сканує ВСІ pages послідовно
Page 1: ID=105? ❌ ID=12? ❌
Page 2: ID=501? ✅ ЗНАЙДЕНО!
Читання: 2 pages
-- Створення Clustered Index
CREATE CLUSTERED INDEX IX_Employees_ID
ON Employees(employee_id);
-- SQL Server РЕОРГАНІЗУЄ дані!
Як тепер зберігається (відсортовано за employee_id):
Page 1: [ID=7: Diana Wilson] [ID=12: Bob Smith]
Page 2: [ID=105: Alice Brown] [ID=501: Charlie Davis]
Той самий пошук WHERE employee_id = 501:
✅ INDEX SEEK: B-Tree навігація
Root: 501 > 100? → йде вправо
Leaf: Прямо знаходить Page 2, Slot 2
Читання: 2-3 pages (залежно від глибини дерева)
Швидкість: O(log N) замість O(N)
SQL Server фізично переміщує всі рядки на диску для впорядкування!
Це може зайняти години для великих таблиць (> 100 млн рядків).
Чому «кластерний»?
Назва пояснює поведінку: Рядки з близькими значеннями ключа фізично згруповані (clustered) разом.
-- Приклад: Замовлення за датами
CREATE CLUSTERED INDEX IX_Orders_Date ON Orders(order_date);
-- Дані на диску:
Page 1: [2024-01-01] [2024-01-01] [2024-01-02] ← січневі разом
Page 2: [2024-01-15] [2024-01-16] [2024-01-20]
Page 3: [2024-02-01] [2024-02-02] [2024-02-05] ← лютневі разом
Range queries надшвидкі:
SELECT * FROM Orders
WHERE order_date BETWEEN '2024-01-01' AND '2024-01-31';
-- SQL Server:
-- 1. Знаходить першу січневу page (B-Tree)
-- 2. Читає pages ПОСЛІДОВНО доки order_date <= '2024-01-31'
-- 3. Зупиняється на лютневій page
-- Результат: Читаємо лише релевантні pages! ⚡
Heap vs Clustered Table
Переваги:
- Швидка вставка (INSERT) — додає в кінець
- Не потребує підтримки індексу
Недоліки:
- Повільний пошук (Table Scan)
- Фрагментація з часом
Переваги:
- Швидкий пошук за ключем (Index Seek)
- Range queries дуже швидкі
- Фізична впорядкованість
Недоліки:
- Вставка може бути повільнішою (потрібно підтримувати порядок)
- Можлива фрагментація при частих INSERT/UPDATE
Некластерний індекс (Non-Clustered Index)
Аналогія з реального життя: Бібліотечний каталог
- Книги впорядковані за інвентарним номером (Clustered Index)
- Але вам потрібна книга за автором "Шевченко"
- Ви йдете до алфавітного каталогу (картки з іменами авторів)
- Знаходите картку "Шевченко" → на ній написано номер полиці: 5-B-12
- З номером йдете до полиці і беретє саму книгу
Він не містить самих книг, лише покажчики (номери полиць).
Що таке Non-Clustered Index?
- Відсортовані значення індексованих стовпців (наприклад, email)
- Покажчики на реальні рядки в таблиці (RID або Clustered Key)
Структура Non-Clustered Index
Практичний приклад: Пошук за Email
CREATE TABLE Users (
user_id INT PRIMARY KEY CLUSTERED, -- Впорядковано за ID
email NVARCHAR(100),
name NVARCHAR(50)
);
-- Пошук за email
SELECT * FROM Users WHERE email = 'alice@example.com';
Що робить SQL Server:
❌ CLUSTERED INDEX SCAN (TABLE SCAN)
Сканує ВСІ рядки таблиці (впорядковані за user_id):
ID=1: john@... ❌
ID=2: bob@... ❌
ID=3: alice@... ✅ ЗНАЙДЕНО!
ID=4: mary@... (продовжує до кінця)
Читання: ВСІ pages таблиці
-- Створення індексу на email
CREATE NONCLUSTERED INDEX IX_Users_Email ON Users(email);
Структура індексу (окрема B-Tree):
Leaf Level (відсортовано за email):
alice@example.com → user_id: 3 ← покажчик!
bob@example.com → user_id: 2
john@example.com → user_id: 1
mary@example.com → user_id: 4
Той самий запит:
✅ INDEX SEEK + KEY LOOKUP
Крок 1: INDEX SEEK в IX_Users_Email
alice@example.com → user_id = 3 ✅ (швидко!)
Крок 2: KEY LOOKUP в Clustered Index
user_id = 3 → [3, alice@..., Alice Smith] ✅
Читання: 2-3 pages (індекс) + 1 page (дані)
Швидкість: O(log N) замість O(N)
- Non-Clustered Index містить лише
email+user_id. - Але запит хоче
SELECT *(всі стовпці: email, name). nameнемає в індексі → потрібен Key Lookup до таблиці.
RID vs Clustered Key: Що зберігає покажчик?
-- Таблиця БЕЗ Clustered Index
CREATE TABLE Products (
product_id INT,
name NVARCHAR(100)
);
CREATE NONCLUSTERED INDEX IX_Products_Name ON Products(name);
Leaf level індексу містить RID (Row Identifier):
┌────────────────────────────────┐
│ IX_Products_Name (Non-Clust) │
├────────────────────────────────┤
│ name RID │
│ "Laptop" → 1:500:2 │ FileID:PageID:SlotID
│ "Mouse" → 1:500:5 │
│ "Keyboard" → 1:501:0 │
└────────────────────────────────┘
RID = Фізична адреса:
RID = 1 : 500 : 2
↓ ↓ ↓
File Page Slot (позиція на page)
-- Таблиця З Clustered Index
CREATE TABLE Products (
product_id INT PRIMARY KEY CLUSTERED,
name NVARCHAR(100)
);
CREATE NONCLUSTERED INDEX IX_Products_Name ON Products(name);
Leaf level індексу містить Clustered Key:
┌────────────────────────────────┐
│ IX_Products_Name (Non-Clust) │
├────────────────────────────────┤
│ name product_id │ ← Clustered Key!
│ "Laptop" → 101 │
│ "Mouse" → 205 │
│ "Keyboard" → 89 │
└────────────────────────────────┘
Процес Key Lookup:
1. Non-Clustered Index: name="Laptop" → product_id=101
2. Clustered Index B-Tree: шукаємо product_id=101
3. Знаходимо повний рядок: [101, "Laptop", 1299.99, ...]
- RID: Фізична адреса може змінитися (page split, reorganize)
- Clustered Key: Логічний ключ, завжди стабільний
Якщо Clustered Index на UNIQUEIDENTIFIER (16 байт) → всі Non-Clustered індекси «роздуваються».
Візуалізація: Один запит, два індекси
SELECT user_id, name FROM Users WHERE email = 'alice@example.com';
- Для кожного рядка результату потрібен окремий Key Lookup.
- Якщо запит повертає 10,000 рядків → 10,000 Key Lookups! 😱
name в індекс через INCLUDE).Множинні індекси на одній таблиці
CREATE TABLE Products (
product_id INT PRIMARY KEY CLUSTERED, -- Clustered Index #1
category_id INT,
name NVARCHAR(200),
price DECIMAL(10,2),
stock_quantity INT
);
-- Non-Clustered Indexes
CREATE NONCLUSTERED INDEX IX_Products_Category
ON Products(category_id); -- Index #2
CREATE NONCLUSTERED INDEX IX_Products_Name
ON Products(name); -- Index #3
CREATE NONCLUSTERED INDEX IX_Products_Price
ON Products(price); -- Index #4
Візуалізація:
Composite Indexes (Складені індекси)
Composite Index — це індекс на декілька стовпців одночасно.
CREATE INDEX IX_Users_LastName_FirstName
ON Users(last_name, first_name);
Порядок стовпців має значення!
-- ✅ Використає індекс
SELECT * FROM Users
WHERE last_name = 'Smith';
-- ✅ Використає індекс
SELECT * FROM Users
WHERE last_name = 'Smith' AND first_name = 'John';
-- ❌ НЕ використає індекс (first_name не перший!)
SELECT * FROM Users
WHERE first_name = 'John';
-- ✅ Використає індекс
SELECT * FROM Users
WHERE first_name = 'John';
-- ✅ Використає індекс
SELECT * FROM Users
WHERE first_name = 'John' AND last_name = 'Smith';
-- ❌ НЕ використає індекс
SELECT * FROM Users
WHERE last_name = 'Smith';
Правило лівої руки (Leftmost Prefix Rule):
Індекс можна використати, якщо запит фільтрує за першим стовпцем в індексі (або першим + другим, або всіма).
Covering Index (Включений індекс)
Проблема: Key Lookup повільний, бо потрібно 2 операції (non-clustered index → clustered index).
Рішення: Включити додаткові стовпці в індекс через INCLUDE.
-- Запит, який часто виконується
SELECT email, first_name, last_name
FROM Users
WHERE email = 'ivan@example.com';
-- ❌ Без INCLUDE
CREATE INDEX IX_Users_Email ON Users(email);
-- Процес: Index Seek (email) → Key Lookup (first_name, last_name) 💥 ПОВІЛЬНО
-- ✅ З INCLUDE (Covering Index)
CREATE INDEX IX_Users_Email_Covering
ON Users(email)
INCLUDE (first_name, last_name);
-- Процес: Index Seek (email) → ВСІ ДАНІ в індексі! ⚡ ШВИДКО
-- Не потрібен Key Lookup!
- Здавалось би, якщо
INCLUDEтакий корисний, чому б не додати туди всі стовпці таблиці, щоб уникнути Key Lookup для будь-якого запиту?
- Дублювання даних: Ви фактично створюєте копію всієї таблиці. Це подвоює використання дискового простору.
- Сповільнення запису (WRITE Performance): Кожен
INSERT,UPDATE,DELETEв таблиці змушує SQL Server оновлювати і саму таблицю, і цей величезний індекс. Це суттєво знижує продуктивність операцій зміни даних. - Більше пам'яті (Buffer Pool): Широкі індекси займають більше сторінок пам'яті. Менше корисних даних кешується в RAM → більше читань з повільного диска.
- Складніше обслуговування: Rebuild/Reorganize такого індексу буде тривати набагато довше.
INCLUDE тільки для критично важливих стовпців, які часто зустрічаються в SELECT частині ваших "найгарячіших" запитів.Unique Index
Unique Index гарантує унікальність значень у стовпці.
CREATE UNIQUE INDEX IX_Users_Email_Unique
ON Users(email);
-- Тепер неможливо вставити дублікат
INSERT INTO Users (email) VALUES ('test@example.com'); -- ✅ OK
INSERT INTO Users (email) VALUES ('test@example.com'); -- ❌ ERROR!
UNIQUE constraint автоматично створює UNIQUE INDEX.
Filtered Index
Filtered Index — це індекс з WHERE умовою, який індексує лише частину рядків.
-- Проблема: тільки 1% замовлень активні, але ми часто їх шукаємо
CREATE TABLE Orders (
order_id INT PRIMARY KEY,
customer_id INT,
status NVARCHAR(20), -- 'Active', 'Completed', 'Cancelled'
order_date DATE
);
-- ❌ Звичайний індекс (індексує ВСІ 10 млн рядків)
CREATE INDEX IX_Orders_Status ON Orders(status);
-- ✅ Filtered Index (індексує лише активні ~ 100K рядків)
CREATE INDEX IX_Orders_Active
ON Orders(customer_id, order_date)
WHERE status = 'Active';
-- Виграш: менший розмір індексу, швидше обслуговування
- Коли потрібна підмножина рядків (напр.,
is_deleted = 0) - Для часткових даних (напр.,
created_date >= '2024-01-01') - Економія місця та швидше обслуговування
Index Fragmentation (Фрагментація)
Що таке фрагментація?
З часом, через INSERT/UPDATE/DELETE, сторінки індексу стають неоптимально заповненими або розкиданими по диску.
Проблеми:
- Більше операцій читання (потрібно більше pages)
- Повільніші range queries
- Марнування дискового простору
Перевірка фрагментації
SELECT
OBJECT_NAME(ips.object_id) AS TableName,
i.name AS IndexName,
ips.avg_fragmentation_in_percent,
ips.page_count
FROM sys.dm_db_index_physical_stats(
DB_ID(), NULL, NULL, NULL, 'DETAILED'
) AS ips
INNER JOIN sys.indexes AS i
ON ips.object_id = i.object_id
AND ips.index_id = i.index_id
WHERE ips.avg_fragmentation_in_percent > 10 -- Більше 10% фрагментації
ORDER BY ips.avg_fragmentation_in_percent DESC;
Виправлення фрагментації
-- Дефрагментація online (не блокує таблицю)
ALTER INDEX IX_Users_Email ON Users REORGANIZE;
Коли: Фрагментація 5-30%
Переваги: Online, не блокує
Недоліки: Повільніше, не завжди ефективно
-- Повна перебудова індексу
ALTER INDEX IX_Users_Email ON Users REBUILD;
Коли: Фрагментація >30%
Переваги: Повні оптимізація, оновлює статистику
Недоліки: Блокує таблицю (якщо не ONLINE)
-- Перебудова без блокування (Enterprise Edition)
ALTER INDEX IX_Users_Email ON Users
REBUILD WITH (ONLINE = ON);
Enterprise Edition only
Дозволяє перебудову без блокування таблиці
Best Practices: Коли створювати індекси
✅ Створюйте індекси на:
- Primary Keys (автоматично)
- Foreign Keys (для JOIN queries)
- WHERE clauses (часті фільтри)
- ORDER BY (сортування)
- GROUP BY (агрегація)
❌ НЕ створюйте індекси на:
- Малі таблиці (<1000 рядків) — table scan швидше
- Стовпці з малою селективністю (напр.,
genderз 2 значеннями) - Часті UPDATE/INSERT таблиці — індекси уповільнюють зміни
- Широкі стовпці (
NVARCHAR(MAX),XML) — індекс буде величезним
Приклад: Аналіз запиту
-- Часто виконуваний запит
SELECT product_id, name, price
FROM Products
WHERE category_id = 5
AND price > 100
ORDER BY price DESC;
-- ✅ Оптимальний індекс
CREATE INDEX IX_Products_Category_Price
ON Products(category_id, price DESC)
INCLUDE (name);
Чому саме такий індекс?
category_id— перший, бо фільтруємо за ним (WHERE)price DESC— другий, бо сортуємо (ORDER BY DESC)INCLUDE (name)— covering index (не потрібен key lookup)
Практичні завдання
Завдання 1: Оптимізація повільного запиту
-- Таблиця
CREATE TABLE Orders (
order_id INT PRIMARY KEY,
customer_id INT,
order_date DATE,
total_amount DECIMAL(10,2),
status NVARCHAR(20)
);
-- Повільний запит (10 секунд на 10 млн рядків)
SELECT order_id, customer_id, total_amount
FROM Orders
WHERE customer_id = 12345
AND order_date >= '2024-01-01'
AND status = 'Completed';
Завдання: Створіть оптимальний індекс для цього запиту.
💡 Розв'язок
CREATE INDEX IX_Orders_Customer_Date_Status
ON Orders(customer_id, order_date, status)
INCLUDE (total_amount);
Пояснення:
customer_id— перший (найселективніший фільтр)order_date— другий (range filter)status— третій (equality filter)INCLUDE (total_amount)— covering index
Завдання 2: Фрагментація
Перевірте фрагментацію індексів вашої бази даних та виконайте reorganize/rebuild де потрібно.
Перевірка знань
Резюме
- Індекси == Швидкість: Правильні індекси можуть прискорити запити в тисячі разів
- B-Tree структура: Забезпечує O(log N) пошук
- Clustered Index: Один на таблицю, містить реальні дані
- Non-Clustered Index: Множинні, містять покажчики
- Composite Indexes: Порядок стовпців критичний
- Covering Index: Включає всі потрібні стовпці (INCLUDE)
- Фрагментація: Регулярне обслуговування індексів необхідне
Наступна тема: Системні бази даних MS SQL Server