Типы индексов Oracle, MySQL, PostgreSQL, MS SQL

Лекция



Индексы в базах данных — это специальные структуры, которые ускоряют поиск и сортировку данных, но требуют дополнительной памяти и замедляют операции вставки/обновления. Основные виды: B-Tree, Hash, Bitmap, GiST и полнотекстовые индексы. Альтернативой индексам могут быть денормализация, материализованные представления, кеширование и партиционирование.

Назначение индексов

  • Ускорение поиска — позволяют находить строки без полного сканирования таблицы.

  • Сортировка и фильтрация — запросы ORDER BY, WHERE, JOIN выполняются быстрее.

  • Оптимизация высоконагруженных систем — критично при больших объемах данных и тысячах запросов.

  • Поддержка уникальности — индексы могут обеспечивать уникальные значения в колонках.

какие типы индексов поддерживаются в наиболее популярных СУБД

B-Tree


Семейство B-Tree индексов — это наиболее часто используемый тип индексов, организованных как сбалансированное дерево, упорядоченных ключей. Они поддерживаются практически всеми СУБД как реляционными, так нереляционными, и практически для всех типов данных.



Так как большинство, наверное, их хорошо знает(или могут прочесть о них например, здесь), то единственное, что, пожалуй, следует здесь отметить, это то, что данный тип индекса оптимален для множества с хорошим распределением значений и высокой мощностью(cardinality-количество уникальных значений).

B-дерево (B-Tree) – основный тип индекса. Элементы упорядочены по значению, что эффективно для операций равенства и диапазона ( = , >, <, BETWEEN и т.п.)​. Оптимизатор может использовать B-Tree даже для префиксного поиска по шаблону (LIKE 'prefix%'), так как данные отсортированы​. B-Tree индексы подходят для высокоизбирательных колонок (с большим числом различных значений)​. По сложности доступ ~ O(log N), поэтому поиск одной записи или небольшого диапазона масштабируется логарифмически. Влияние на запросы:

  • Точечный поиск: Очень быстрый – находит нужное значение за несколько шагов.

  • Диапазонный запрос: B-Tree эффективно находит начало диапазона и затем последовательно считывает подходящие узлы.

  • Сортировка (ORDER BY): Если столбец отсортирован в индексе, можно вернуть данные по порядку без дополнительной сортировки.

  • JOIN: При соединении, если во второй таблице есть индекс B-Tree по ключу соединения, можно быстро находить соответствия, избегая полного сканирования​.

Влияние на запросы:

  • Точечный поиск: Очень быстрый (в среднем O(1) доступ), но выгода над B-Tree заметна только на очень больших таблицах и специфичных сценариях.

  • Диапазон: невозможен (нельзя получить упорядоченное множество ключей из хеша).

  • JOIN: не применяется, т.к. нужен именно равный ключ – можно использовать, если точно сравниваем ключи, но чаще достаточно B-Tree.

Пространственные индексы


В данный момент все данные СУБД имеют пространственные типы данных и функции для работы с ними, для Oracle — это множество типов и функций в схеме MDSYS, для PostgreSQL — point, line, lseg, polygon, box, path, polygon, circle, в MySQL — geometry, point, linestring, polygon, multipoint, multilinestring, multipolygon, geometrycollection, MS SQL — Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection.
В схеме работы пространственных запросов обычно выделяют две стадии или две ступени фильтрации. СУБД, обладающие слабой пространственной поддержкой, отрабатывают только первую ступень (грубая фильтрация, MySQL). Как правило, на этой стадии используется приближенное, аппроксимированное представление объектов. Самый распространенный тип аппроксимации – минимальный ограничивающий прямоугольник (MBR – Minimum Bounding Rectangle) .
Для пространственных типов данных существуют особые методы индексирования на основе R-деревьев(R-Tree index) и сеток(Grid-based Spatial index).

Spatial grid


Spatial grid(пространственная сетка) index – это древовидная структура, подобная B-дереву, но используется для организации доступа к пространственным(Spatial) данным, то есть для индексации многомерной информации, такой, например, как географические данные с двумерными координатами(широтой и долготой). В этой структуре узлами дерева выступают ячейки пространства. Например, для двухмерного пространства: сначала вся родительская площадь будет разбита на сетку строго определенного разрешения, затем каждая ячейка сетки, в которой количество объектов превышает установленный максимум объектов в ячейке, будет разбита на подсетку следующего уровня. Этот процесс будет продолжаться до тех пор, пока не будет достигнут максимум вложенности (если установлен), или пока все не будет разделено до ячеек, не превышающих максимум объектов.


В случае трехмерного или многомерного пространства это будут прямоугольные параллелепипеды (кубоиды) или параллелотопы.

Quadtree


Quadtree – это подвид Grid-based Spatial index, в котором в родительской ячейке всегда 4 потомка и разрешение сетки варьируется в зависимости от характера или сложности данных.

R-Tree


R-Tree (Regions Tree) – это тоже древовидная структура данных подобная Spatial Grid, предложенная в 1984 году Антонином Гуттманом. Эта структура данных тоже разбивает пространство на множество иерархически вложенных ячеек, но которые, в отличие от Spatial Grid, не обязаны полностью покрывать родительскую ячейку и могут пересекаться.
Для расщепления переполненных вершин могут применяться различные алгоритмы, что порождает деление R-деревьев на подтипы: с квадратичной и линейной сложностью(Гуттман, конечно, описал и с экспоненциальной сложностью — Exhaustive Search, но он, естественно, нигде не используется).
Квадратичный подтип заключается в разбиении на два прямоугольника с минимальной площадью, покрывающие все объекты. Линейный – в разбиении по максимальной удаленности.

HASH


Hash-индексы были предложены Артуром Фуллером, и предполагают хранение не самих значений, а их хэшей, благодаря чему уменьшается размер(а, соответственно, и увеличивается скорость их обработки) индексов из больших полей. Таким образом, при запросах с использованием HASH-индексов, сравниваться будут не искомое со значения поля, а хэш от искомого значения с хэшами полей.
Из-за нелинейнойсти хэш-функций данный индекс нельзя сортировать по значению, что приводит к невозможности использования в сравнениях больше/меньше и «is null». Кроме того, так как хэши не уникальны, то для совпадающих хэшей применяются методы разрешения коллизий.

Хеш-индекс – структура на основе хеш-таблицы. Поддерживает только операции равенства (=) – для диапазонов или сортировки не годится, так как данные неупорядочены​. В PostgreSQL хеш-индексы официально WAL-логируются и поддерживаются начиная с версии 10, но применяются редко, так как B-Tree тоже справляется с равенством достаточно быстро. Хеш-индекс может быть чуть компактнее и быстрее для строго точных совпадений​, однако его узкая специализация ограничивает применение. В MySQL нет пользовательских хеш-индексов для InnoDB (только внутренняя Adaptive Hash Index) – большинство индексов там тоже B-Tree​.

Полнотекстовый индекс (Fulltext)

Полнотекстовый индекс (Fulltext) в MySQL – особый вид индекса для полнотекстового поиска по большим текстовым полям. Реализован как инвертированный список слов (аналогично GIN). Доступен для движков MyISAM и InnoDB. Применение: позволяет выполнять операции MATCH() ... AGAINST() для поиска по ключевым словам. Ограничения: работает только с текстом, не подходит для обычных WHERE-условий, не упорядочен по значениям, поэтому не помогает с обычными сравнениями, диапазонами или сортировкой. Зато для поиска по словам гораздо быстрее последовательного обхода.

Bitmap


Bitmap index – метод битовых индексов заключается в создании отдельных битовых карт (последовательность 0 и 1) для каждого возможного значения столбца, где каждому биту соответствует строка с индексируемым значением, а его значение равное 1 означает, что запись, соответствующая позиции бита содержит индексируемое значение для данного столбца или свойства.

EmpID Пол
1 Мужской
2 Женский
3 Женский
4 Мужской
5 Женский



Битовые карты

Значение Начало Конец Битовая маска
Мужской Адрес первой строки Адрес последней строки 10010
Женский Адрес первой строки Адрес последней строки 01101


Основное преимущество битовых индексов в том, что на больших множествах с низкой мощностью и хорошей кластеризацией по их значениям индекс будет меньше чем B*-tree. (Подробнее стоит прочесть здесь или здесь)

Reverse index


Reverse index – это тоже B-tree индекс но с реверсированным ключом, используемый в основном для монотонно возрастающих значений(например, автоинкрементный идентификатор) в OLTP системах с целью снятия конкуренции за последний листовой блок индекса, т.к. благодаря переворачиванию значения две соседние записи индекса попадают в разные блоки индекса. Он не может использоваться для диапазонного поиска.
Пример:

Поле в таблице(bin) Ключ reverse-индекса(bin)
00000001 10000000
00001001 10010000
00001010 01010000
00001011 11010000

Как видите, значение в индексе изменяется намного больше, чем само значение в таблице, и поэтому в структуре b-tree, они попадут в разные блоки.

Inverted index


Инвертированный индекс – это полнотекстовый индекс, хранящий для каждого лексемы ключей отсортированный список адресов записей таблицы, которые содержат данный ключ.

1 Мама мыла раму
2 Папа мыл раму
3 Папа мыл машину
4 Мама отполировала машину


В упрощенном виде это будет выглядеть так:

Мама 1,4
Мыла 1
Раму 1,2
Папа 2,3
Отполировала 4
Машину 3,4

Partial index


Partial index — это индекс, построенный на части таблицы, удовлетворяющей определенному условию самого индекса. Данный индекс создан для уменьшения размера индекса.

Function-based index Функциональный индекс


Самим же гибким типом индексов являются функциональные индексы, то есть индексы, ключи которых хранят результат пользовательских функций. Функциональные индексы часто строятся для полей, значения которых проходят предварительную обработку перед сравнением в команде SQL. Например, при сравнении строковых данных без учета регистра символов часто используется функция UPPER. Создание функционального индекса с функцией UPPER улучшает эффективность таких сравнений.
Кроме того, функциональный индекс может помочь реализовать любой другой отсутствующий тип индексов данной СУБД(кроме, пожалуй, битового индекса, например, Hash для Oracle)

Функциональный индекс (MySQL ≥ 8.0.13) Позволяет индексировать выражение JSON напрямую, без отдельной колонки. Например:

CREATE INDEX idx_manufacturer
ON vehicle ((JSON_UNQUOTE(JSON_EXTRACT(vehicle_data, '$.manufacturer'))));

Для поиска по множеству ключей лучше использовать JSON_TABLE, чтобы развернуть JSON в табличный вид.

Функциональный индекс CREATE INDEX idx_manufacturer ((JSON_UNQUOTE(JSON_EXTRACT(vehicle_data, '$.manufacturer')))) Для поиска по конкретному ключу JSON Быстро для точных совпадений, но менее эффективно для сложных условий

Виртуальные индексы или генерируемая (сохраняемая)колонка

Генерируемая колонка Можно создать виртуальную или сохраненную колонку, которая извлекает значение из JSON (например, JSON_EXTRACT(vehicle_data, '$.manufacturer')). Затем на эту колонку накладывается обычный индекс.

STORED колонка ALTER TABLE vehicle ADD manufacturer VARCHAR(50) GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(vehicle_data, '$.manufacturer'))) STORED; CREATE INDEX idx_manufacturer ON vehicle(manufacturer); Для часто используемых ключей JSON На 10–30% быстрее при вставках и до 5× быстрее при LIKE‑запросах

Мультизначные индексы (multi-valued indexes)

Мультизначные индексы — это мощный инструмент для оптимизации запросов по

JSON‑массивам.

  • Они позволяют индексировать каждый элемент массива.

  • Работают с операторами MEMBER OF, JSON_CONTAINS, JSON_OVERLAPS.

  • Существенно ускоряют поиск без необходимости денормализации данных.

  • Работает только с JSON‑массивами, не с объектами.

  • Нельзя использовать ASC/DESC порядок для мультизначных индексов.

  • Не поддерживает префиксные индексы (например, col(10)).

  • Поддерживается только в InnoDB.

  • Не может быть PRIMARY KEY или FOREIGN KEY.

ALTER TABLE user_events ADD INDEX idx_tags ((CAST(event_data->'$.tags' AS CHAR(64) ARRAY))); Для массивов внутри JSON Позволяют индексировать элементы массива напрямую, EXPLAIN показывает переход от full scan к range scan

GIN (Generalized Inverted Index)

GIN (Generalized Inverted Index) – обобщенный инвертированный индекс. Предназначен для случаев, когда одна запись содержит много значений, например, массив, JSON или документ в полнотекстовом поиске​. GIN индексирует каждый элемент, позволяя эффективно искать вхождения. Пример – колонка типа text[]: GIN позволит быстро найти все строки, где массив содержит определенное значение. PostgreSQL также использует GIN для полнотекстового поиска (тип tsvector). Особенности: GIN-индексы обычно большие по размеру и относительно медленные при вставке, зато очень ускоряют сложные запросы по содержимому​.

  • Влияние:

    • Поиск по содержимому: для операторов типа “contains” или полнотекстового поиска GIN незаменим – без него пришлось бы сканировать все данные.

    • Точечный поиск по одному полю: GIN не используется (вместо него B-Tree).

    • Диапазоны, сортировка, join: не применим.

GiST (Generalized Search Tree)

GiST (Generalized Search Tree) – обобщенное дерево поиска. Гибкая структура для кастомных типов данных и условий. Например, используется для геометрических типов (поиск точек по области, индексы в PostGIS), для полнотекстового поиска по шаблону (через расширение pg_trgm), для диапазонных типов (tsrange и пр.) и реализации nearest-neighbor запросов​. GiST индексы могут быть неточными (lossy) – иногда возвращают кандидатов, требующих дополнительной проверки.

  • Применение и влияние:

    • Гео и диапазонные запросы: Позволяют эффективно искать пересечения интервалов, попадание точки в регион, ближайшие соседние значения и т.п., что невозможно через B-Tree.

    • Стандартные запросы =, > <: можно, но B-Tree обычно эффективнее для простых типов.

    • Размер: GiST индексы могут быть компактнее GIN, но тоже занимают доп. место.

BRIN (Block Range Index)

BRIN (Block Range Index) – упрощенный индекс по блокам, присутствует в PostgreSQL​. Полезен на очень больших таблицах, где данные по колонке имеют локальную сортировку (например, append-only таблица по времени). BRIN хранит минимальные/максимальные значения по блокам страниц и очень легок по размеру. Это не точный индекс, а скорее ускоритель сканирования больших разреженных таблиц: хорошо отфильтровывает блоки, не содержащие подходящих значений. В highload-сценариях BRIN полезен для time-series данных, архивов.

Сводная таблица типов индексов

MySQL PostgreSQL MS SQL Oracle
B-Tree index Есть Есть Есть Есть
Поддерживаемые пространственные индексы(Spatial indexes) R-Tree с квадратичным разбиением Rtree_GiST(используется линейное разбиение) 4-х уровневый Grid-based spatial index (отдельные для географических и геодезических данных) R-Tree c квадратичным разбиением; Quadtree
Hash index Только в таблицах типа Memory Есть Нет Нет
Bitmap index Нет Есть Нет Есть
Reverse index Нет Нет Нет Есть
Inverted index Есть Есть Есть Есть
Partial index Нет Есть Есть Нет
Function based index Есть начиная с 8 версии Есть Есть Есть


Стоит упомянуть, что в PostgreSQL GiST позволяет создать для любого собственного типа данных индекс основанный на R-Tree. Для этого нужно реализовать все 7 функций механизма R-Tree.

Прочие: MySQL поддерживает R-Tree индексы для геоданных (для колонок типа Geometry при использовании SPATIAL INDEX), PostgreSQL имеет специализированные индексы (например, SP-GiST, bloom-фильтры). Эти индексы применяются для узких случаев и в общем случае в highload-системах используются реже.

Вывод по типам индексов: В большинстве ситуаций B-Tree остается оптимальным выбором для реляционных баз – он универсален и ускоряет равенство, диапазоны, сортировку и соединения. Специализированные индексы (GIN/GiST/Fulltext) нужны для специфических запросов (полнотекстовый поиск, JSON/массивы, гео-запросы), а хеш-индексы – редкий случай для чистых точечных поисков. Понимание возможностей каждого типа позволяет подобрать лучший инструмент под задачу. Например, если нужно искать подстроку в середине текста (LIKE '%word%'), обычный индекс B-Tree не поможет – понадобится GIN/GiST с триграммами​. А для поиска точного ключа на миллиардах записей, возможно, стоит рассмотреть хеш-индекс.

Альтернативы индексам

  • Денормализация — хранение данных в более удобной для выборки форме.

  • Материализованные представления — сохраненные результаты сложных запросов.

  • Кеширование — использование Redis/Memcached для ускорения доступа.

  • Партиционирование — разделение таблицы на части для ускорения выборки.

  • NoSQL решения — MongoDB, Cassandra используют собственные механизмы индексации.

Ограничения и риски

  • Замедление записи — каждый INSERT/UPDATE требует обновления индекса.

  • Рост памяти — индексы занимают дополнительное место.

  • Избыточные индексы — могут ухудшить производительность вместо ускорения.

  • Переиндексация — иногда требуется пересоздание индексов для оптимизации.

Типы индексов в реляционных СУБД и их влияние на производительность

Подбор индекса под конкретные запросы

Проектируя индекс, исходим из запросов, которые этот индекс должен ускорять. Рассмотрим основные типы запросов (SELECT с различными конструкциями) и как выбирать индексы для них:

Индексы для фильтрации (WHERE)

Самый частый сценарий – ускорение фильтрации в WHERE-условиях SELECT. Правила выбора индекса:

  • Если условие по одному столбцу (например, WHERE status = 'ACTIVE'): индексируем этот столбец. Для равенства или небольшого набора значений подойдет обычный B-Tree. Для предиката сравнения (>, <, BETWEEN) – тоже B-Tree, так как он поддерживает диапазоны. Если WHERE содержит функцию или выражение над столбцом, то обычный индекс на столбце не поможет – либо уберите функцию из условия, либо создайте индекс на выражение (функциональный индекс). Например, WHERE LOWER(name) = 'john' – обычный индекс по name не используется, нужно либо хранить отдельное поле name_lower с индексом, либо в PostgreSQL создать индекс USING btree(lower(name)) для поддержки такого запроса.

  • Комбинация нескольких условий (AND/OR): важно понять, будет ли СУБД использовать один индекс или несколько. В MySQL и PostgreSQL оптимизатор, как правило, может использовать только один индекс на таблицу в рамках одного сканирования. Исключение – Bitmap Index Scan в PostgreSQL или Index Merge в MySQL, которые объединяют несколько индексов, но такой план обычно менее эффективен, чем один подходящий составной индекс. Поэтому если запрос имеет, к примеру, WHERE col1 = ? AND col2 = ?, лучше создать составной индекс по (col1, col2). Такой индекс покроет сразу оба условия. В MongoDB аналогично: при двух отдельных индексах по col1 и col2 используется лишь один, а второй фильтр применяется после чтения документов​. Составной же индекс по двум полям позволит сразу найти записи, удовлетворяющие обоим условиям, без лишних проверок​.

  • Порядок столбцов в составном индексе имеет значение. Классическое правило – более селективные (кардинальные) или используемые в равенствах условия ставятся первыми. Часто упоминают правило ESR (Equality, Sort, Range): сперва поля, участвующие в равенствах, затем поля для сортировки, потом диапазоны​. Например, запрос WHERE category = ? AND date >= ? ORDER BY date – разумно индексировать (category, date) именно в таком порядке: category (равенство) впереди, date после (для диапазона и сортировки). Это позволит использовать индекс и для фильтрации по категории, и для извлечения по дате в отсортированном виде.

  • Избыточные условия: если есть составной индекс (col1, col2, col3), он покроет запросы по (col1) и (col1, col2) – достаточно левой части. Например, индекс (country, city) может использоваться для фильтра WHERE country = 'US' и для WHERE country='US' AND city='NY', но не для запроса только по city без country (порядок имеет значение)​. Поэтому при проектировании учитывайте, по каким комбинациям колонок реально будут запросы. Иногда вместо одного широкого индекса имеет смысл сделать несколько более узких.

Пример: предположим, есть таблица users со столбцами (id, email, status, created_at). Какие индексы выбрать?

  • Запросы для поиска пользователя по email: SELECT * FROM users WHERE email = 'user@example.com'. Здесь эффективен уникальный индекс (или PK) на email – точечный поиск.

  • Запросы для выборки «активных» пользователей: WHERE status = 'ACTIVE'. Если активных мало (условие высокоизбирательно), индекс по status ускорит выборку. Но если половина пользователей активны, такой индекс мало поможет – он вернет слишком много строк (проще последовательное сканирование)​. В этом случае лучше не создавать отдельный индекс, т.к. низкая селективность.

  • Запросы с диапазоном по дате: WHERE created_at BETWEEN '2023-01-01' AND '2023-01-31'. Индекс по created_at сильно ускорит такой запрос, особенно если за месяц лишь небольшая доля от общего числа пользователей. Также такой индекс может ускорить сортировку по дате.

  • Комбинированные запросы: WHERE status='ACTIVE' AND created_at > ... ORDER BY created_at DESC. Здесь можно завести составной индекс (status, created_at). Он позволит сначала отфильтровать по статусу, а внутри – по дате сразу в нужном порядке (например, по убыванию, если создать индекс DESC). База сможет сразу выбрать нужные данные из индекса, без сортировки, и прочитать только несколько верхних строк при наличии LIMIT.

Важно упомянуть покрывающие индексы: если запрос выбирает лишь столбцы, присутствующие в индексе, СУБД может выполнить его, не обращаясь к основной таблице. Например, если у нас есть индекс по (status, created_at) и запрос SELECT status, created_at FROM users WHERE status='ACTIVE', то PostgreSQL выполнит Index Only Scan – все данные есть в индексе. В MySQL подобный эффект называется covering index. Это снижает нагрузку на I/O. Поэтому при необходимости можно включить в индекс дополнительные колонки (в PostgreSQL – через INCLUDE, в MySQL – просто добавив в конец неключевые), чтобы сделать индекс покрывающим для часто исполняемого запроса.

Индексы для JOIN (соединений)

Для ускорения JOIN-ов следует индексировать колонки, по которым происходит соединение таблиц. Обычно это внешние ключи. Например, есть две таблицы: orders и customers, соединяем по orders.customer_id = customers.id. Рекомендация – создать индекс (или сделать первичным ключом) поле customers.id – тогда при соединении по каждому order СУБД быстро находит соответствующего customer по PK. Если customer_id в orders не является PK, имеет смысл создать индекс и на нем (особенно если часто нужно получить все заказы конкретного клиента).

Алгоритмы соединений:

  • Nested Loop Join: для каждой строки внешней таблицы ищется соответствие во внутренней. Индекс на внутренней таблице позволяет быстро находить нужные строки, иначе пришлось бы сканировать всю внутреннюю таблицу для каждой внешней записи​. В нашем примере, обходя orders (внешняя), база по индексу на customers.id мгновенно находит нужную запись в customers вместо полного скана.

  • Merge Join: требует, чтобы обе входные последовательности были отсортированы по ключу соединения. Индексы могут обеспечить эту отсортированность без отдельной операции Sort​. Например, если объединяем по date две большие таблицы, наличие индекса по date на обеих позволит использовать Merge Join эффективно.

  • Hash Join: не использует индексы (строит хеш-таблицу в памяти), поэтому для очень больших таблиц с ограниченной памятью лучше все же иметь индексы и позволять оптимизатору выбирать другие методы.

Практический вывод: Всегда индексируйте столбцы, по которым делаются JOIN (особенно внешние ключи в дочерних таблицах). Без индексов соединения могут деградировать до полных переборов, что недопустимо в highload. Индекс на столбцах соединения – залог того, что даже при росте данных время соединения будет расти незначительно.

Индексы для сортировки (ORDER BY) и группировки (GROUP BY)

Сортировка (ORDER BY) может быть дорогой операцией на больших наборах. Индексы помогают избежать явной сортировки, если порядок выборки совпадает с порядком индекса:

  • СУБД может считывать данные в отсортированном виде по индексу​. Например, запрос SELECT * FROM sales ORDER BY date при наличии индекса по date просто выполнит последовательный обход индекса – строки сразу получаются по возрастанию даты. Это экономит CPU и память на сортировку.

  • Если запрос сочетает сортировку с условием: WHERE user_id = 123 ORDER BY timestamp, то составной индекс (user_id, timestamp) выдаст данные в нужном порядке для конкретного пользователя. Иначе, даже если есть индекс по user_id, результаты придется сортировать заново.

  • В MySQL важно помнить: индекс используется для сортировки только если порядок сортировки полностью совпадает с порядком в индексе, и не смешивается ASC/DESC (или используйте desc-индексы для всех)​.

Группировка (GROUP BY) схожа с сортировкой – часто СУБД сортирует по полям группировки или использует хеш. Индекс может помочь:

  • Если GROUP BY происходит по колонке с индексом, и особенно если SELECT выбирает агрегаты по этой колонке, оптимизатор может использовать индекс для более быстрого группирования​. В MySQL, например, если группировка по префиксу индекса, можно обойтись без создания временной таблицы – чтение по индексу будет уже сгруппировано​.

  • Тем не менее, выигрыш от индекса при GROUP BY менее прямой, чем при WHERE/ORDER. Группировка часто и так требует просмотр всех строк (или всех после фильтра), поэтому индекс полезен, если он значительно уменьшает количество обрабатываемых строк (например, индекс на столбце позволяет сразу пропускать диапазоны неинтересующих значений).

Пример: Пусть есть таблица payments(amount, type, created_at) и запрос: вывести топ-10 самых крупных платежей типа 'refund' за последний год. Этот запрос содержит и фильтрацию (WHERE type='refund' AND created_at > ...), и сортировку (ORDER BY amount DESC), и ограничение (LIMIT 10). Какой индекс поможет? Идеальное решение – составной индекс по (type, created_at, amount) в котором type и/или created_at отсортированы по возрастанию, а amount по убыванию (или все ASC, тогда читаем в обратном порядке). СУБД сможет мгновенно перейти к первым записям для type='refund' в заданном диапазоне времени, уже упорядоченным по amount, и взять 10 записей. Без индекса пришлось бы вычитать все подходящие платежи и сортировать их, что под нагрузкой крайне неэффективно.

Общее правило: анализируйте полный запрос – условия, соединения, сортировки, лимиты – и старайтесь спроектировать индекс, который максимально покрывает этот запрос, чтобы работа выполнялась на уровне индекса (в идеале – индексный скан вместо seq scan, и без дополнительных сортировок). При этом всегда учитывайте баланс: добавление каждого нового столбца в индекс увеличивает его размер и замедляет модификации.

Подводные камни индексации в высоконагруженных системах

Несмотря на огромную пользу индексов, неправильное их использование может привести к проблемам. Рассмотрим некоторые подводные камни:

  • Замедление записей (INSERT/UPDATE/DELETE). Главный компромисс индексации: ускоряя чтение, мы замедляем запись​. При вставке или обновлении строки все затронутые индексы должны быть обновлены. В системе с интенсивной записью (например, логирование, трекинг событий) избыточные индексы могут стать узким местом. Каждая дополнительная запись в индекс – это дополнительные IO и CPU. Поэтому для write-heavy нагрузок индексировать только самое необходимое. Если запросы на чтение позволяют, лучше пожертвовать быстродействием некоторых селектов, но держать скорость вставки.

  • Избыточные индексы (overindexing). Иногда разработчики создают «на всякий случай» много индексов или дублирующие индексы. Например, отдельный индекс на col1 и составной на (col1, col2) – первый часто избыточен, так как те же запросы можно обслужить вторым. Лишние индексы съедают диск и память, замедляют все модификации, а пользы не приносят. Худший индекс – неиспользуемый индекс​. Признак проблемы: индексы, у которых idx_scan = 0 или близко к тому в статистике PostgreSQL, либо которые никогда не задействованы в планах (можно отследить по EXPLAIN или по статистике). Такие индексы стоит удалять. В целом, ищите баланс: нет строгого правила, сколько индексов должно быть, но, как советуют эксперты, если индексы не окупаются ускорением чтения, а сильно тормозят запись – их слишком много​.

  • Фрагментация и bloating индексов. В длительно работающих системах с активными обновлениями и удалениями со временем может накапливаться пустое пространство в индексах – так называемый блоат. В PostgreSQL из-за MVCC старые версии записей остаются «мертвыми» в индексах до вакуума. B-Tree может терять плотность заполнения узлов. Индексный блоат – это неиспользуемые фрагменты, раздувающие размер индекса​. Он приводит к лишнему расходу памяти, замедляет сканирование (нужно пройти через пустоты) и увеличивает IO​. Причины: частые INSERT/DELETE, неэффективный паттерн обновлений​. Решение – периодическая переиндексация (команда REINDEX в PostgreSQL) либо использование утилит типа pg_repack. В высоконагруженной системе важно мониторить блоат и устранять его, иначе производительность может постепенно деградировать.

  • Переиндексация и блокировки. Операция перестроения индекса может быть тяжелой и блокировать таблицу (в разных СУБД по-разному: в PostgreSQL REINDEX блокирует, а CREATE INDEX CONCURRENTLY не блокирует долгие чтения). В highload-практике важно планировать переиндексацию в окна низкой нагрузки или в режиме онлайн. Также в некоторых СУБД обновление статистики индексов (ANALYZE) может не успевать за изменениями, что приводит к неидеальному выбору плана.

  • Рост размера индексов. Индексы могут занимать больше места, чем данные таблицы, особенно составные и GIN-индексы. Это увеличивает требования к памяти (буферный кеш) – если индекс не умещается в памяти, его эффективность снижается. В распределенных системах большие индексы расходуют сетевой трафик при репликации. Следует контролировать размер индексов и избегать индексирования "всего подряд".

  • Локальные нюансы СУБД. Каждая СУБД имеет свои тонкости:

    • В MySQL InnoDB кластерный индекс по PK означает, что сама таблица хранится по порядку PK. Это нужно учитывать: PK индекс всегда существует и влияет на физический порядок. Соответственно, выбор PK = выбор основного порядка хранения. Остальные индексы в InnoDB хранят PK как указатель на запись, поэтому слишком длинный PK раздувает каждый вторичный индекс.

    • В PostgreSQL нет кластерных индексов (таблица хранится как Heap отдельно), но есть команда CLUSTER для упорядочивания данных согласно индексу – может ускорить range-сканы по этому индексу (правда, без поддержания порядка при последующих изменениях).

Вывод: Индексы – мощный инструмент, но требующий ухода. В highload-системах необходимо:

  • избегать ненужных индексов,

  • учитывать влияние на запись,

  • мониторить здоровье индексов (размер, блоат, использование),

  • выполнять профилактику (ваккуум, переиндексация) по мере необходимости.

Анализ необходимости индексации: инструменты и подходы

Как понять, какие индексы нужны, а какие – нет? В этом помогают специальные инструменты и методы профилирования запросов:

  • EXPLAIN / План запроса – базовый инструмент оптимизации. Выполняйте EXPLAIN [ANALYZE] для ваших SQL-запросов, чтобы увидеть, как они выполняются. План покажет, используются ли индексы или СУБД делает Seq Scan (полное сканирование таблицы). Если видите Seq Scan на большой таблице там, где ожидался индекс – это сигнал: либо индекс не создан, либо не подходит, либо оптимизатор считает, что с ним не быстрее. Анализируя план, вы можете скорректировать стратегию (создать недостающий индекс, переписать запрос, обновить статистику). Например, если EXPLAIN в PostgreSQL показывает Bitmap Heap Scan + Bitmap Index Scan – это значит, используется индекс, но выборка довольно большая (двухэтапный отбор через битовую карту). А Index Only Scan означает, что индекс полностью покрывает запрос (идеально).

  • Статистика использования индексов. PostgreSQL ведет системную статистику в представлении pg_stat_user_indexes (и sysindexes для системных)​. Там есть поля idx_scan (сколько раз индекс использован в запросах), idx_tup_read и idx_tup_fetch (сколько строк прочитано из индекса и таблицы). Эти данные помогут найти неиспользуемые индексы – у них idx_scan близок к 0​. Если индекс долго существует и idx_scan не растет – вероятно, запросы им не пользуются. Также можно выявить индексы, которые используются, но неэффективно (прочитывают много tuple, но мало получают – признак низкой селективности). По этой статистике DBA принимает решение об удалении или создании индексов. В MySQL подобной встроенной статистики меньше, но есть косвенные признаки (Handler_read_next и др.), а также можно включать USER_STATISTICS при компиляции. Проще – опираться на планы EXPLAIN и логи.

  • Slow Query Log (лог медленных запросов). И PostgreSQL, и MySQL умеют логировать запросы, превышающие определенный порог времени. В MySQL есть Slow Query Log, куда пишутся запросы дольше long_query_time. В PostgreSQL – можно включить логирование запросов по времени (log_min_duration_statement). Анализируя эти логи, в высоконагруженной системе вы обнаружите самые «тяжелые» запросы. Часто достаточно глянуть на Top-N медленных запросов и понять: ага, вот здесь у нас отсутствует индекс на условие, или делается тяжелый JOIN без индекса. Добавив нужный индекс и убедившись через EXPLAIN, что план изменился, вы существенно снизите нагрузку. Подход: найти медленный запрос -> проанализировать план -> добавить индекс -> проверить улучшение

  • Мониторинг производительности и профайлинг. На уровне СУБД есть расширения и утилиты (например, pg_stat_statements в PostgreSQL), которые собирают статистику частоты и стоимости запросов. Это помогает сконцентрироваться на наиболее частых и дорогостоящих запросах. Бывает, что редкий разовый запрос может быть очень медленным без индекса, но если он выполняется раз в месяц – не так критично. А вот запрос, идущий сотни раз в секунду, нужно оптимизировать даже если он сейчас работает приемлемо, чтобы был запас по нагрузке.

  • Автоматический советники. В некоторых СУБД и облачных платформах есть средства, подсказывающие, какие индексы добавить. Например, SQL Server имеет Database Tuning Advisor, PostgreSQL в планах (HypoPG) позволяет имитировать индексы, MySQL в Cloud может предлагать индексы. Но полагаться на них не стоит вслепую – всегда оценивайте воздействие на общую рабочую нагрузку.

Резюме: регулярно профилируйте систему. В highload среде картина запросов может меняться – добавляются новые фичи, меняется распределение данных. Индекс, нужный вчера, сегодня может быть неактуальным, и наоборот. Поэтому оптимизация – непрерывный процесс: меряем – оптимизируем – проверяем.

Индексация в NoSQL: MongoDB и Cassandra

Не только реляционные, но и NoSQL-базы используют индексы для ускорения поиска. Однако архитектура NoSQL (денормализация, шардинг) диктует свои подходы. Рассмотрим на примерах MongoDB и Apache Cassandra.

Индексы в MongoDB

MongoDB – документоориентированная БД, но по части индексов очень схожа с реляционными:

  • B-Tree индексы: используются под капотом для большинства типов индексов в MongoDB. Есть индекс по умолчанию на _id (Primary Key каждого документа). Дополнительно можно создавать обычные индексы на поля (включая вложенные, через точечную нотацию).

  • Single Field vs Compound: Если запросы фильтруют по нескольким полям, составной индекс эффективнее. Как и в SQL, при двух раздельных индексах MongoDB обычно выберет один и по второму просто отфильтрует результаты, или сделает пересечение индексов (что менее эффективно). Например, запрос find({city: "Boston", age: 25}) при индексах только по city и по age все равно использует один из них, проверяя второй фильтр уже на извлеченных документах​. Правильнее создать составной индекс {city: 1, age: 1}, который сразу найдет документы, где обе conditions выполнены​. Полезный прием – порядок полей в compound-индексе выбирать по частоте использования и типу запросов (аналог ESR-правила).

  • Multikey индексы: MongoDB автоматически создает мульти ключевой индекс, если индексируется поле-массив – каждый элемент массива индексируется отдельно​. Это похоже на GIN в Postgres. Ограничение – в одном compound-индексе только одно поле может быть массивом.

  • TTL индексы: Специальный вид индекса для автоматического удаления «протухших» документов. По сути, это индекс по дате с опцией expireAfterSeconds. Используется для хранения с ограниченным временем жизни (например, сессии, логи). Особенность: TTL-индекс всегда одиночный (не может быть составным)​. Фоновый процесс MongoDB будет периодически (обычно раз в минуту) сканировать этот индекс и удалять документы старше указанного значения. Влияние на производительность: на чтение TTL-индекс не влияет, на запись – минимально (чуть увеличивает нагрузку при удалении, но это распределено во времени)​

  • Вторичные индексы: Все индексы кроме _id считаются вторичными. MongoDB позволяет много вторичных индексов, но важно учитывать их влияние на память. Высоконагруженный MongoDB обычно настроен так, чтобы рабочие индексы помещались в RAM, иначе будут частые чтения с диска.

  • Ограничения:

    • Максимум 32 полей в compound-индексе​

    • Размер индексов – по умолчанию ключ не более 1024 байт, однако есть поддержка хеш-индексов (хранят хеш от значения, ограничивая размер ключа). Хеш-индекс дает равномерное распределение, но не поддерживает диапазоны.

    • Индексы в MongoDB не поддерживают некоторые выражения, как SQL (нет вычисляемых индексов до версии 4.4, но в 4.4+ появились Aggregation Expressions indexes – аналог функциональных).

  • Лучшие практики MongoDB:

    • Проектируйте по запросам: как и в SQL, не индексируйте то, что не используется в запросах.

    • Compound вместо нескольких: один составной индекс на (field1, field2) лучше, чем два отдельных, для запроса, использующего оба​

    • Следите за размером: слишком большие индексы или их количество могут вытеснить данные из памяти. Помните: MongoDB хранит индексы в памяти (WireTiger использует своппинг страниц, но эффективно нужен RAM). Indeks, занимающий гигабайты, может стать узким местом.

    • Используйте профилировщик MongoDB: он покажет, сколько документов сканируется при запросах (метрика nscannedObjects / nscanned). Если запрос сканирует много документов при небольшом output – не хватает индекса.

    • Тестируйте с explain(): метод db.collection.explain() покажет, какой индекс используется и сколько документов проверяется. Это аналог SQL EXPLAIN.

Индексы в Apache Cassandra

Cassandra – распределенная колонночная NoSQL, основанная на идеях DynamoDB и BigTable. Ее подход к индексам отличается:

  • Primary Key vs Secondary Index: В Cassandra каждая таблица имеет свой Primary Key, состоящий из Partition Key (один или несколько столбцов) и опционально Clustering columns. Partition Key определяет, на каком узле хранится запись (ключ партиции хэшируется и распределяется по кластеру). Clustering columns определяют сортировку внутри партиции. Таким образом, основной способ эффективного доступа в Cassandra – через Primary Key (зная точный partition key, либо диапазон по clustering key внутри партиции). Без знания partition key поиск требует обращаться ко всем узлам.

  • Вторичные индексы: Cassandra позволяет создавать индекс на колонку, не входящую в Primary Key. Но это менее эффективно и рекомендуется в ограниченных случаях. Почему? Вторичный индекс не глобальный, а локальный на каждом узле. Когда вы делаете запрос по такому индексу, координатор рассылает запрос всем узлам к локальному индексу. Те узлы, у которых найдется значение, вернут результаты, остальным будет нечего вернуть. При большом количестве узлов это не масштабируется – запрос превращается практически в полный скан кластера. Cassandra документация прямо говорит: не используйте индекс для высоко-кардинальных или очень низко-кардинальных столбцов​

    • Высокая кардинальность (уникальные значения): в индексе почти столько же точек, сколько строк. Запрос по такому значению обратится ко всем узлам, причем на каждом узле, возможно, найдется лишь 1 результат. Много сетевых вызовов ради нескольких записей – неэффективно​

    • Низкая кардинальность (очень мало уникальных значений): например, булево поле или статус с 2-3 значениями. Индекс по нему соберет сотни тысяч ключей, указывающих на многие партиции. Запрос «status = true» опять же пойдет на все узлы, и каждый вернет огромный список. Проще было бы сканировать последовательность. Cassandra прямо говорит, что индекс на boolean "не имеет смысла"​

    • Часто обновляемые/удаляемые данные: вторичные индексы тоже хранят tombstones при удалении. Если часто обновлять поле с индексом, будет накапливаться много tombstone-записей, что ведет к деградации, вплоть до отказа запросов, если tombstone слишком много​

    • Большие партиции: если индексируется колонка внутри очень большой партиции, запрос должен все равно перебрать эту партицию, что не быстрее полного скана ее же​

  • Когда вторичные индексы допустимы: Когда запросы по индексу редкие и не критичны по задержке, либо когда кластер небольшой. Cassandra 3+ также имеет т.н. SASI (Storage-Attached Secondary Index) – улучшенная версия индексов, более эффективная и с поддержкой префиксного поиска. Но и для SASI рекомендации по кардинальности остаются в силе​

  • Денормализация вместо индексов: Распространенный подход в Cassandra – если нужны разные ключи доступа, лучше создать отдельную таблицу (или материализованное представление) с нужным Primary Key, чем полагаться на вторичные индексы. Например, если у нас есть таблица по user_id (partition key), а часто надо искать по email, то создают вторую таблицу (или materialized view) с partition key = email, содержащую ссылку на user. Тем самым вы получаете индексированное хранилище по email, но управляемое явно на уровне данных.

  • TTL: В Cassandra можно назначать Time To Live на данные (записи «протухают» и удаляются автоматически). Это не индекс, но часто используется вместо явного TTL-индекса, как в Mongo. Однако массовое истечение TTL может создавать нагрузку из-за множества tombstones, поэтому планируйте единовременный объем истекающих данных.

  • Лучшие практики Cassandra:

    • Проектируйте схему от запросов: В Cassandra говорят "query-based modeling". Нормализация менее важна, важнее, чтобы каждый запрос читал одну партицию. Заранее продумайте, по каким ключам будет доступ – сделайте их частью Primary Key. Например, для блога, если нужно выбирать посты по автору и дате – сделайте PRIMARY KEY(author, date); если по тегам – возможно, заведите отдельную таблицу posts_by_tag(tag, post_id).

    • Избегайте вторичных индексов для критичных путей: они не масштабируются при росте данных/узлов. Используйте, только если данных мало или это "административный" запрос вне основного потока.

    • Ограничивайте размер партиций: индексы (и вообще запросы) страдают, если партиция содержит миллионы записей. Лучше разбивать (например, добавить в partition key еще и год, если храните события по пользователю, чтоб партиция не росла бесконечно).

    • Monitoring: Cassandra не так легко профилировать, но следует обращать внимание на метрики Read Latency, количество tombstones (в ответах CQL драйвера при tracing видно), и на координацию запросов. Если запрос выполняется слишком долго и, судя по tracing, обходит много узлов – вероятно, не хватает правильного индексационного подхода.

Итого, индексация в NoSQL все так же служит ускорению запросов, но еще сильнее завязана на шаблоны запросов. MongoDB в этом плане ближе к реляционным – там вы просто создаете нужные индексы. Cassandra же поощряет продуманное распределение данных и создание дополнительных структур вместо «индексирования по месту».

Типичные ошибки и анти-паттерны индексирования

Ниже перечислены распространенные ошибки при работе с индексами, которые могут ухудшить производительность:

  • Неиндексированные JOIN-ы: Забытая индексация столбцов, по которым выполняются соединения таблиц. Это приводит к тому, что база делает полные перекрестные обходы – крайне медленно на больших таблицах. Анти-паттерн: отсутствие индекса на внешнем ключе. Решение – всегда индексировать внешние ключи и вообще поля, участвующие в JOIN, чтобы каждая сторона соединения могла быстро найти соответствия.

  • Индексирование колонок с низкой селективностью: Например, поле gender (M/F) или status с несколькими значениями, когда распределение равномерное. Такие индексы почти бесполезны – условие отсекает половину или треть таблицы, оптимизатор, скорее всего, выберет seq scan. Пример: индекс на поле, где ~50% строк имеют значение 'true'. Запрос WHERE field = true вернет половину таблицы – использование индекса заставит пройти по миллионам записей из индекса и затем прочитать столько же строк таблицы, что может быть медленнее, чем просто один проход по таблице крупными блоками​. Лучше не создавать индекс, чем создавать его впустую. Если же очень нужно ускорить такие запросы, можно применить частичный индекс (PostgreSQL) – например, индексировать только WHERE status='ACTIVE' если неактивных гораздо больше, либо рассмотреть другой подход (разбить таблицу, кэшировать и т.д.).

  • Дублирующие/пересекающиеся индексы: Создание нескольких индексов, покрывающих одни и те же колонки. Например, индекс по (A) и по (A,B) – первый дублируется вторым по части A. Или два индекса: (A,B) и (B,A) – часто тоже избыточны, если запросы по отдельности A и B не идут. Лишние индексы нужно удалять​. Проверить дубликаты можно, сравнив наборы колонок. Иногда индексы появляются дублирующиеся из-за неявных причин (например, уникальный индекс по PK и PK как кластерный – в SQL Server это отдельно, в Postgres PK = уникальный B-Tree индекс). Следите, чтобы не поддерживать два индекса там, где нужен один.

  • Отсутствие покрывающих индексов (индекс не покрывает запрос): Ситуация, когда индекс вроде и используется, но запрос все равно лезет в таблицу за дополнительными колонками. Это значит, что можно улучшить: добавить недостающие поля в индекс (включить в состав или через INCLUDE в PG), чтобы произошел Index Only Scan. Anti-pattern: часто используемый «узкий» индекс, но каждый раз потом идет чтение таблицы. Например, индекс на user_id, а запрос SELECT user_name WHERE user_id=... – тут все равно нужно идти в таблицу за user_name. Если запрос суперчастый, лучше сделать составной индекс (user_id, user_name) – он станет покрывающим. Без покрытия – лишние нагрузки.

  • Функции и выражения в WHERE без соответствующего индекса: Например, WHERE DATE(created_at) = '2023-01-01'. Такой запрос не сможет использовать индекс по created_at, потому что к колонке применяется функция (за исключением случаев, когда оптимизатор умеет преобразовать, что редко). Это анти-паттерн – оборачивать колонку функцией на стадии фильтрации. В результате всегда полный просмотр. Решения: либо переписать условие (например, WHERE created_at >= '2023-01-01' AND created_at < '2023-01-02' – уже можно использовать индекс по дате), либо завести функциональный индекс по DATE(created_at) если такая конструкция часто нужна. Аналогично с другими функциями и выражениями – делайте вычисления вне SQL или добавляйте индекс на выражение.

  • Индексы, которые не соответствуют шаблонам запросов: Индекс ради индекса. Например, разработчик проиндексировал все отдельные колонки таблицы «на всякий случай». Но реальные запросы сложные, фильтруют по множеству условий – в итоге ни один из одинарных индексов не используется, нужен был составной. Или наоборот: создали составной индекс (A,B,C), а часто выполняются запросы только по B – индекс не помогает, а отдельного по B нет. Лекарство – анализировать статистику запросов и соответствие индексов им, вносить изменения.

  • Не учтено влияние изменения данных на индекс: Например, индекс по полю, которое очень часто обновляется (скажем, счетчик, баланс). При каждом обновлении запись переносится на новую позицию в индексе, может происходить фрагментация. Это может стать точкой блокировок или горячей страницей. Если такие обновления критичны, иногда индексы на таких колонках избегают, либо применяют специальные подходы (например, partial index только на часто запрашиваемое подмножество значений, или используют кластеризацию).

  • Слепая вера в индексы: Полагать, что раз индекс есть, то запрос автоматически оптимальный. Всегда проверяйте план. Бывает, что индекс не используется, потому что запрос написан не оптимально (условия несовместимы с индексом, или статистика неверная). Иногда нужно подсказать оптимизатору (например, в редких случаях использовать индексные подсказки вроде USE INDEX в MySQL, но это крайность). В общем, следите за реальным поведением.

Рекомендации по проектированию схемы с учетом индексов

При разработке схемы базы данных под высокие нагрузки, держите в уме индексы с самого начала. Вот ключевые рекомендации:

  • Индексируйте по запросам: Выпишите ключевые запросы системы (особенно те, что в критичном пути или будут выполняться часто). Убедитесь, что по каждому предусмотрен хотя бы один индекс, который будет им помогать. Например, в веб-приложении обычно есть запросы "получить объект по ID" (нужен PK или уникальный индекс), "список объектов с фильтром по пользователю и времени" (нужен составной индекс по user_id+date), и т.д.

  • Оцените кардинальность полей: Чем более уникальны значения в столбце, тем больше выгода индексации (индекс отсеет максимум строк)​. Столбцы с тысячами уникальных значений – хорошие кандидаты для индекса, а с парой значений – почти всегда нет. Если бизнес-логика требует фильтрации по низкоселективному полю, подумайте о частичном индексе (например, индексовать только редкое значение) или об изменении логики хранения (например, вынести часто встречающееся значение в отдельную таблицу, а основную фильтровать только по редкому).

  • Не забывайте про сортировку и группировку: Если известно, что данные часто запрашиваются в определенном порядке, сделайте индекс в этом порядке. Индекс может одновременно служить и для фильтрации, и для сортировки. Например, для ленты событий ORDER BY timestamp DESC индекс по timestamp DESC ускорит выдачу самых новых событий, особенно в сочетании с LIMIT. Группировку индекс поддержит, только если она совпадает с порядком (как обсуждалось выше), так что обычно групповой агрегат лучше оптимизировать другим путем (материализованные агрегаты, кэш).

  • Комбинируйте условия в составных индексах: Вместо множества одиночных индексов на каждое поле, старайтесь создавать составные индексы, покрывающие сразу несколько условий запроса (AND). Это даст больше шансов, что именно один индекс полностью обслужит запрос, вместо множества частичных. Но не переусердствуйте: индекс на 5 колонок, из которых только 2 реально фильтруются в запросах, будет лишней тратой ресурсов. Найдите минимальный набор.

  • Учитывайте частоту запросов: Индекс для редкого отчетного запроса, возможно, не нужен – его можно выполнить и с полной выборкой раз в месяц. А вот индекс для API, которое дергается 1000 раз в секунду, жизненно необходим. Расставляйте приоритеты: наиболее частые и критичные по производительности запросы – в первую очередь оптимизировать индексами.

  • Минимизируйте количество индексов на сильно изменяемых таблицах: Если таблица получает сотни вставок в секунду (например, лог событий), постарайтесь держать на ней только индекс PK (может быть по времени, если партиционирование) и парочку необходимых для основных запросов. Массовая индексация таких данных быстро приведет к проблемам с производительностью записи.

  • Используйте покрывающие и частичные индексы по необходимости: Для часто выполняемых однотипных запросов имеет смысл «добавить» в индекс столбцы, чтобы избежать обращений к таблице. Например, часто читается поле balance по user_id – можно индексировать (user_id, balance). А если приложение чаще нужно выбрать только пользователей с определенным флагом, можно применить partial index: CREATE INDEX ... ON users(email) WHERE active = true – такой индекс будет меньше по размеру и ускорит только нужный кейс.

  • Следите за ростом данных: Архитектура, прекрасно работавшая на 100k записей, может упереться в потолок на 100 млн. Пересматривайте стратегию индексации при росте. Возможно, понадобятся новые индексы под новые запросы или удаление старых. Может оказаться полезным партиционирование данных (например, по дате) – тогда часть запросов будет вообще работать с меньшими частями таблицы, снижая потребность в индексах или объем их работы.

  • Тестируйте и профилируйте: Перед развертыванием на продакшене, прогоните нагрузочные тесты с профилированием запросов. Убедитесь, что планы запросов оптимальны, индексы используются как ожидается. Лучше поймать отсутствие индекса на этапе теста, чем получать alarm о медленных запросах в бою.

  • Документируйте решения: В командной работе полезно оставлять комментарии, какие индексы и зачем были созданы. Это поможет в будущем понять логику и избежать случайного удаления нужного индекса или дублирования.

Все эти рекомендации сводятся к одному: думайте о том, как данные будут использоваться, и подстраивайте структуру хранения (включая индексы) под эти использования.

Чеклист выбора индекса под задачу

В заключение, приведем чеклист – набор вопросов, который стоит пройти, решая, нужен ли индекс и какой именно:

  • 1. Что за запрос мы ускоряем?
    Выпишите пример запроса (SELECT) с всеми его частями (WHERE, JOIN, ORDER BY, GROUP BY, LIMIT).

  • 2. Какие колонки участвуют в фильтрах и соединениях?
    Именно они – первые кандидаты на индексирование. Если несколько – планируем составной индекс.

  • 3. Сколько данных отбирает этот запрос?
    Если он обычно возвращает <1-5% таблицы – индекс обязательно нужен (селективный запрос). Если 50% и более – индекс, возможно, не даст выигрыша​, можно обойтись полным сканированием (но подумать о партиционировании или изменении запроса). При 5-20% – ситуация пограничная, решается экспериментом (иногда индекс все же быстрее).

  • 4. Подходит ли тип индекса под операторы?
    Равенство, диапазон – B-Tree идеален. LIKE '%...' – понадобится специальный (например, полнотекстовый или GIN). Гео-запросы – R-Tree или GiST. Сначала убедитесь, что в принципе существует индекс, умеющий ускорить такой запрос.

  • 5. Нужен ли составной индекс?
    Если в WHERE несколько условий по разным столбцам, или комбинация фильтра+сортировка – спроектируйте составной индекс, покрывающий эти элементы в правильном порядке (обычно равенства и точные фильтры впереди, сортировка/диапазон следом).

  • 6. Нужно ли индексировать все эти поля или хватит части?
    Если запрос часто фильтруется по (A,B), но иногда бывают фильтры только по A – индекс по (A,B) справится и с тем, и с другим (по A он тоже используется). А вот если иногда фильтруют только по B, то нужен отдельный индекс по B или перестановка. Решите, нужно ли несколько индексов или можно обойтись одним.

  • 7. Будет ли индекс покрывающим?
    Посмотрите на SELECT-список. Если запросу нужны только поля, которые вы собираетесь индексировать, то получится индекс, покрывающий запрос (что хорошо). Если же запрос возвращает много других колонок, то индекс все равно вынужден будет обращаться к таблице. Можно ли добавить парочку столбцов в индекс, чтобы покрыть запрос полностью? Это улучшит производительность, но увеличит индекс – решение принимается исходя из частоты запроса.

  • 8. Каковы побочные эффекты для записи?
    Оцените, как часто вставляются/обновляются данные в таблицу, и насколько замедлит это новый индекс. Если таблица огромная и постоянно пишется, каждый новый индекс ощутимо снизит throughput записи. В highload-системах баланс критичен: иногда лучше пожертвовать скоростью редкого запроса, чем замедлить всю систему из-за индекса. Можно использовать статистику: например, каждый индекс может добавлять ~5-10% к времени обработки вставки (оценочно). Соответственно, десяток лишних индексов удвоит время вставки – плохо. Если без индекса не обойтись (требование бизнеса по быстрому чтению), возможно масштабирование: больше реплик для разгрузки, шардинг и прочее.

  • 9. Протестируйте с EXPLAIN.
    Создайте индекс в тестовой среде и посмотрите план запроса. Используется ли новый индекс? Каков оценочный выигрыш (смотрите cost или реальное время с ANALYZE)? Убедитесь, что план стал лучше (меньше cost, меньше прочитанных строк). Иногда бывает, что индекс не используется из-за неверной оценки – тогда может помочь ANALYZE или даже SET enable_seqscan=off для проверки, может ли вообще индекс дать выигрыш.

  • 10. Мониторьте в бою.
    После выхода в продакшен, следите за метриками: ушли ли проблемы с медленным запросом? Нет ли побочных эффектов – выросло ли время вставки, не появились ли lock-и? Проверьте через статистику (например, pg_stat_user_indexes), что индекс действительно используется (idx_scan растет). Если индекс не прижился – возможно, он не нужен и его стоит убрать.

Пройдя по этому чеклисту, вы с большой вероятностью примете обоснованное решение об индексации. Правильный выбор индексов – это во многом искусство, основанное на данных: понимание природы данных и запросов, экспериментирование и измерение. Следуя описанным принципам и избегая перечисленных ошибок, вы сможете обеспечить высокую производительность своих highload-систем за счет эффективной индексации данных

создано: 2016-12-08
обновлено: 2026-06-17
396



Помог ли вам этот ответ?
Нажмите оценку и напишите коротко почему. Так мы сможем сделать следующие ответы точнее и полезнее.
Насколько вы довольны ответом?
Ваш отзыв напрямую влияет на качество следующих подсказок и ответов.


Поделиться:
Пожаловаться

Найди готовое или заработай

С нашими удобными сервисами без комиссии*

Как это работает? | Узнать цену?

Найти исполнителя
$0 / весь год.
  • У вас есть задание, но нет времени его делать
  • Вы хотите найти профессионала для выполнения задания
  • Возможно применение функции гаранта на сделку
  • Приоритетная поддержка
  • идеально подходит для студентов, у которых нет времени для решения заданий
Готовое решение
$0 / весь год.
  • Вы можете продать (как исполнитель) или купить (как заказчик) готовое решение
  • Вам предоставят готовое решение
  • Будет предоставлено в минимальные сроки т.к. задание уже готовое
  • Вы получите базовую гарантию 8 дней
  • Вы можете заработать на материалах
  • подходит как для студентов так и для преподавателей
Я исполнитель
$0 / весь год.
  • Вы профессионал своего дела
  • У вас есть опыт и желание зарабатывать
  • Вы хотите помочь в решении задач или написании работ
  • Возможно применение функции гаранта на сделку
  • подходит для опытных студентов так и для преподавателей

Комментарии

Оставить комментарий

Если у вас есть какое-либо предложение, идея, благодарность или комментарий, не стесняйтесь писать. Мы очень ценим отзывы и рады услышать ваше мнение.
To reply

Лекции и учебник по "Базы данных, знаний и хранилища данных. Big data, СУБД и SQL и noSQL"

Термины: Базы данных, знаний и хранилища данных. Big data, СУБД и SQL и noSQL