← Назад к вопросам

Что такое некластеризованный индекс SQL?

2.0 Middle🔥 201 комментариев
#Базы данных (SQL)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Некластеризованный индекс SQL (Non-Clustered Index)

Некластеризованный индекс (non-clustered index) — это отдельная структура данных в SQL базе, которая создаёт отсортированный список значений столбца(ов) с указателями на соответствующие строки. Это позволяет быстро находить данные без сканирования всей таблицы, но не влияет на физическое расположение данных в БД.

Кластеризованный vs Некластеризованный

КЛАСТЕРИЗОВАННЫЙ ИНДЕКС (Clustered Index)
┌─────────────────────┐
│ Определяет порядок  │
│ ФИЗИЧЕСКОГО         │
│ расположения строк  │
└─────────────────────┘

Обычно PRIMARY KEY создаёт кластеризованный индекс
В таблице может быть ТОЛЬКО ОДИН

Пример: таблица отсортирована по id
┌────┬──────────┐
│ id │ name     │
├────┼──────────┤
│ 1  │ Alice    │  ← Строки физически расположены
│ 2  │ Bob      │     в порядке id
│ 3  │ Charlie  │
└────┴──────────┘


НЕКЛАСТЕРИЗОВАННЫЙ ИНДЕКС (Non-Clustered Index)
┌─────────────────────┐
│ УКАЗАТЕЛЬ на стро ки│
│ без изменения их    │
│ физического порядка │
└─────────────────────┘

В таблице может быть МНОГО (до 999 в SQL Server)

Пример: индекс по name
Индекс:               Таблица:
┌─────┐────┐        ┌────┬──────────┐
│name │ptr │        │id  │name      │
├─────┼────┤        ├────┼──────────┤
│Alice│ 1  │───────→│ 1  │Alice     │
│Bob  │ 2  │───────→│ 2  │Bob       │
│Char │ 3  │───────→│ 3  │Charlie   │
└─────┴────┘        └────┴──────────┘

Создание некластеризованного индекса

-- Базовый синтаксис
CREATE INDEX idx_email ON users(email);

-- По нескольким столбцам
CREATE INDEX idx_name_email ON users(first_name, last_name, email);

-- С условием (фильтрованный индекс)
CREATE INDEX idx_active_users ON users(email)
WHERE is_active = true;

-- С INCLUDE (покрывающий индекс)
CREATE INDEX idx_email_include ON users(email)
INCLUDE (first_name, last_name, phone);

-- Уникальный индекс
CREATE UNIQUE INDEX idx_unique_email ON users(email);

-- Удаление индекса
DROP INDEX idx_email ON users;  -- MySQL
DROP INDEX users.idx_email;  -- PostgreSQL

Как работает поиск

БЕЗ ИНДЕКСА: Table Scan

Запрос: SELECT * FROM users WHERE email = 'alice@example.com'

BD проверяет КАЖДУЮ строку:
┌──────┬─────────────────────┬─────┐
│id    │email                │name │
├──────┼─────────────────────┼─────┤
│1     │john@example.com     │ ✗ │
│2     │alice@example.com    │ ✓ Найда!
│3     │bob@example.com      │ ✗ │
│...   │...                  │... │
│10000 │charlie@example.com  │ ✗ │
└──────┴─────────────────────┴─────┘

10000 операций чтения! МЕДЛЕННО


С ИНДЕКСОМ: Index Seek

Построенный индекс (B-tree):

           [alice...]
          /            \
    [alice..john]  [john..zoe]
    /    |    \
  [a]   [i]   [j]...  ← Листья с указателями
   ↓
alice@example.com → строка 2

БДО делает бинарный поиск:
Операция: log₂(10000) ≈ 14 операций! БЫСТРО

Практический пример с Python/SQL

import psycopg2
from time import time

conn = psycopg2.connect("dbname=test user=postgres")
cursor = conn.cursor()

# Таблица с 1 млн записей
cursor.execute("""
    CREATE TABLE IF NOT EXISTS users (
        id BIGSERIAL PRIMARY KEY,
        email VARCHAR(255),
        first_name VARCHAR(100),
        last_name VARCHAR(100),
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
""")

# Вставляем тестовые данные (1 млн)
for i in range(1000000):
    cursor.execute(
        "INSERT INTO users (email, first_name, last_name) VALUES (%s, %s, %s)",
        (f"user{i}@example.com", f"First{i}", f"Last{i}")
    )
conn.commit()

# Тест 1: Поиск БЕЗ индекса
start = time()
cursor.execute("EXPLAIN ANALYZE SELECT * FROM users WHERE email = %s", 
               ("user999999@example.com",))
print("Без индекса:")
for row in cursor:
    print(row)
print(f"Время: {time() - start:.3f}s")

# Создаём индекс
start = time()
cursor.execute("CREATE INDEX idx_users_email ON users(email)")
conn.commit()
print(f"Создание индекса: {time() - start:.3f}s")

# Тест 2: Поиск С индексом
start = time()
cursor.execute("EXPLAIN ANALYZE SELECT * FROM users WHERE email = %s", 
               ("user999999@example.com",))
print("\nС индексом:")
for row in cursor:
    print(row)
print(f"Время: {time() - start:.3f}s")

conn.close()

EXPLAIN: как понять план запроса

-- БЕЗ индекса
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'alice@example.com';

-- Результат:
Seq Scan on users  (cost=0.00..5000.00 rows=1)  ← Sequential scan
  Filter: (email = 'alice@example.com')
  Planning Time: 0.1 ms
  Execution Time: 150.2 ms  ← МЕДЛЕННО


-- С индексом
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'alice@example.com';

-- Результат:
Index Scan using idx_email on users  (cost=0.42..4.44 rows=1)
  Index Cond: (email = 'alice@example.com')
  Planning Time: 0.2 ms
  Execution Time: 0.5 ms  ← БЫСТРО

Типы некластеризованных индексов

1. Простой индекс (Single Column)

-- Индекс по одному столбцу
CREATE INDEX idx_last_name ON employees(last_name);

-- Поиск по фамилии будет быстрым
SELECT * FROM employees WHERE last_name = 'Smith';

2. Составной индекс (Composite Index)

-- Индекс по нескольким столбцам (ORDER важен)
CREATE INDEX idx_name ON employees(last_name, first_name);

-- Эффективен для обоих запросов:
SELECT * FROM employees WHERE last_name = 'Smith';

SELECT * FROM employees WHERE last_name = 'Smith' AND first_name = 'John';

-- Но НЕ эффективен для:
SELECT * FROM employees WHERE first_name = 'John';
-- Индекс не помогает без last_name (leading column)

3. Покрывающий индекс (Covering Index)

-- Индекс INCLUDE включает доп. столбцы
CREATE INDEX idx_email_include ON users(email)
INCLUDE (first_name, last_name, phone);

-- Запрос может быть выполнен ТОЛЬКО из индекса
SELECT first_name, last_name, phone FROM users WHERE email = 'alice@example.com';

-- БД читает индекс и находит все данные
-- НЕ нужно обращаться к основной таблице (Index Only Scan)

4. Фильтрованный индекс (Filtered Index)

-- Индекс только на активных пользователей
CREATE INDEX idx_active_emails ON users(email)
WHERE is_active = true;

-- Экономит место, ускоряет поиск
SELECT * FROM users WHERE email = 'alice@example.com' AND is_active = true;

-- Но НЕ помогает для неактивных:
SELECT * FROM users WHERE email = 'alice@example.com' AND is_active = false;

5. Уникальный индекс (Unique Index)

-- Гарантирует уникальность значений
CREATE UNIQUE INDEX idx_unique_email ON users(email);

-- Попытка вставить дубликат → ошибка
INSERT INTO users (email) VALUES ('alice@example.com');
INSERT INTO users (email) VALUES ('alice@example.com');  -- ERROR: duplicate key

Производительность индексов

# Правило 3:16:97

# КОГДА ИСПОЛЬЗОВАТЬ ИНДЕКС:
# - Таблица > 10000 строк
# - Столбец часто используется в WHERE
# - Запросы читают < 10% данных

# КОГДА НЕ ИСПОЛЬЗОВАТЬ:
# - Таблица маленькая (< 1000)
# - Столбец имеет низкую selectivity (много одинаковых значений)
#   Пример: пол (M/F) → индекс бесполезен
# - Часто делаются INSERT/UPDATE/DELETE на этот столбец
#   Индекс нужно обновлять → медленнее

Проблемы с индексами

-- ❌ Проблема 1: Фрагментация индекса
-- После много DELETE/UPDATE индекс становится фрагментированным
ALTER INDEX idx_email REBUILD;  -- Пересоздание
ALTER INDEX idx_email REORGANIZE;  -- Реорганизация

-- ❌ Проблема 2: Неиспользуемые индексы
-- Занимают место и замедляют INSERT/UPDATE

-- Найти неиспользуемые индексы (PostgreSQL):
SELECT indexname FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND indexname NOT LIKE 'pg_toast%';

-- ❌ Проблема 3: Слишком много индексов
-- Каждый индекс должен обновляться при вставке
-- 10+ индексов → каждый INSERT медленнее

-- ❌ Проблема 4: Плохая selectivity
CREATE INDEX idx_is_deleted ON users(is_deleted);
-- Таблица: 1M активных, 100 удалённых
-- Поиск "is_deleted = true" вернёт 100 из 1M
-- Это 0.01% → индекс помогает
-- Но поиск "is_deleted = false" вернёт 1M из 1M
-- Индекс не поможет → таблица сканируется

Оптимизация запросов с индексами

-- ✓ Хорошо: используется индекс
SELECT * FROM users WHERE email = 'alice@example.com';
-- Index Scan

-- ❌ Плохо: индекс не используется
SELECT * FROM users WHERE email LIKE '%@example.com';
-- % в начале → Seq Scan

-- ✓ Хорошо: LIKE с префиксом работает
SELECT * FROM users WHERE email LIKE 'alice%';
-- Index Scan может использоваться

-- ❌ Плохо: функция над столбцом
SELECT * FROM users WHERE UPPER(email) = 'ALICE@EXAMPLE.COM';
-- Seq Scan (индекс на email не помогает)

-- ✓ Хорошо: индекс на функцию
CREATE INDEX idx_email_upper ON users(UPPER(email));
SELECT * FROM users WHERE UPPER(email) = 'ALICE@EXAMPLE.COM';
-- Index Scan

-- ❌ Плохо: OR условия
SELECT * FROM users 
WHERE email = 'alice@example.com' OR phone = '555-1234';
-- Нужны два индекса на разных столбцах

-- ✓ Хорошо: IN вместо OR
SELECT * FROM users 
WHERE status IN ('active', 'pending', 'review');
-- Может использовать индекс лучше

Мониторинг индексов

-- PostgreSQL: индексы и их размер
SELECT 
    indexname,
    pg_size_pretty(pg_relation_size(indexrelid)) AS size,
    idx_scan AS scans
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;

-- MySQL: список индексов
SHOW INDEX FROM users;

-- SQL Server: информация об индексах
SELECT 
    OBJECT_NAME(i.object_id) AS table_name,
    i.name AS index_name,
    s.user_updates,
    s.user_seeks + s.user_scans + s.user_lookups AS user_reads
FROM sys.indexes AS i
LEFT JOIN sys.dm_db_index_usage_stats AS s 
    ON i.object_id = s.object_id 
    AND i.index_id = s.index_id
WHERE OBJECT_NAME(i.object_id) = 'users'
ORDER BY user_updates DESC;

Рекомендации

# 1. Начни без индексов (YAGNI)
# Добавляй индексы только когда нужны

# 2. Профилируй (EXPLAIN ANALYZE)
# Посмотри план запроса перед созданием индекса

# 3. Индексируй WHERE, JOIN, ORDER BY
# Столбцы в этих частях запросов выигрывают от индексов

# 4. Составные индексы
# Порядок столбцов важен
# (A, B) помогает WHERE A=? AND B=?
# Но НЕ помогает WHERE B=?

# 5. Покрывающие индексы
# Когда нужны только несколько столбцов → используй INCLUDE

# 6. Удаляй неиспользуемые
# Мониторь и удаляй индексы с idx_scan = 0

# 7. Баланс
# INSERT/UPDATE медленнее с индексами
# Каждый индекс нужно обновлять

Ключевые моменты

  • Некластеризованный индекс создаёт отсортированный список с указателями
  • Один кластеризованный (PRIMARY KEY) + много некластеризованных
  • B-tree структура позволяет быстрый поиск за O(log n)
  • Составные индексы требуют правильного порядка столбцов
  • Покрывающие индексы могут сделать поиск без обращения к таблице
  • EXPLAIN ANALYZE показывает использует ли запрос индекс
  • Фрагментация происходит при много DELETE/UPDATE
  • Selectivity критична — индекс на булеве поле часто бесполезен