Menu

Индексы в SQLite: CREATE INDEX и когда их использовать

Разбираем, как работают индексы в SQLite, когда они реально ускоряют запросы, а когда только мешают, и как проверить, что планировщик их действительно использует.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Что такое индекс на самом деле

Индекс — это отдельная структура данных, отсортированное B-дерево, которое позволяет SQLite находить строки по значению столбца, не сканируя всю таблицу целиком. Без индекса запрос вроде WHERE email = 'rosa@example.com' читает каждую строку и сверяет её. С индексом по email SQLite проходит по дереву примерно за log(n) шагов и сразу попадает в нужную запись.

Но за такое ускорение приходится платить. Индекс — это копия значений индексируемого столбца плюс указатель обратно на строку. Каждый INSERT, UPDATE индексируемого столбца и DELETE вынужден обновлять и сам индекс. Места на диске уходит больше, скорость записи слегка проседает. Сделка такая: немного теряем на записи — сильно выигрываем на чтении.

Как создать индекс в SQLite

Базовый синтаксис команды CREATE INDEX:

Соглашение об именовании: в большинстве команд принято писать idx_<таблица>_<столбец> — так сразу понятно, для чего нужен индекс. Имя должно быть уникальным в пределах всей базы, а не только таблицы — поэтому в него и включают название таблицы.

Чтобы удалить индекс:

DROP INDEX idx_users_email;

Индексы в SQLite — это чисто производительные «леса». Удалили индекс — данные не пострадают, изменится только скорость запросов.

Уникальные индексы в SQLite

Уникальный индекс работает сразу в двух ролях: ускоряет поиск и гарантирует, что в индексируемой колонке не появится двух строк с одинаковым значением.

Третья вставка падает с ошибкой UNIQUE constraint failed: accounts.username. SQLite автоматически создаёт уникальные индексы для столбцов PRIMARY KEY и UNIQUE — их можно увидеть под именами вида sqlite_autoindex_<table>_<n>. Писать CREATE UNIQUE INDEX руками нужно только тогда, когда ограничение не было объявлено прямо в самой таблице.

Что на самом деле делает планировщик

Создание индекса ещё не означает, что SQLite станет его использовать. Планировщик запросов выбирает стратегию под каждый конкретный запрос, и посмотреть, что он выбрал, можно через EXPLAIN QUERY PLAN:

Ищите в выводе SEARCH ... USING INDEX idx_orders_customer — это означает, что индекс задействован. Если же видите SCAN orders, значит планировщик решил, что полное сканирование таблицы дешевле (часто это верно на маленьких таблицах), либо форма запроса просто не позволила использовать индекс. Подробнее о том, как читать такие планы, — в отдельной статье ниже.

Когда SQLite не использует индекс

У индексов есть несколько хорошо известных слепых зон. Каждый из примеров ниже сводит индекс по email на нет:

-- Функция оборачивает столбец
SELECT * FROM users WHERE lower(email) = 'rosa@example.com';

-- Ведущий шаблонный символ в LIKE
SELECT * FROM users WHERE email LIKE '%@example.com';

-- Несоответствие типов вызывает преобразование
SELECT * FROM users WHERE email = 12345;

B-дерево отсортировано по «сырому» значению email, поэтому любое преобразование столбца на лету заставляет SQLite перейти к полному сканированию. Вариантов решения несколько: хранить данные уже в нормализованном виде (отдельный столбец email_lower), использовать индекс по выражению (CREATE INDEX idx ON users(lower(email))) или подключить полнотекстовый поиск SQLite для поиска по подстроке.

Покрывающий индекс sqlite

Если индекс уже содержит все столбцы, которые нужны запросу, SQLite ответит, ни разу не обратившись к самой таблице — это и есть покрывающий индекс. Фокус в том, чтобы включить дополнительные столбцы прямо в определение индекса:

Так как обе колонки, которые запрашивает запрос, лежат прямо в индексе, SQLite сообщает USING COVERING INDEX. Обращаться к самой таблице уже не нужно. Покрывающий индекс sqlite — одна из самых выгодных оптимизаций для горячих путей чтения, расплата лишь одна: индекс занимает больше места. Составные индексы — отдельная большая тема, и ей посвящён следующий материал.

Как посмотреть список индексов

Есть два способа узнать, что вообще создано в базе:

Так вы получите список всех индексов в базе вместе с их CREATE-выражениями. Для конкретной таблицы используйте PRAGMA index_list('products'); — он покажет только индексы этой таблицы, а PRAGMA index_info('idx_products_name'); подскажет, какие именно колонки в индексе. Всё, что начинается с sqlite_autoindex_, SQLite создал автоматически под ограничение PRIMARY KEY или UNIQUE — удалить такие индексы нельзя.

Когда индекс лучше не добавлять

Бывают ситуации, когда индекс sqlite не помогает, а только вредит:

  • Маленькие таблицы. Пара сотен строк сканируется за микросекунды. Планировщик всё равно, скорее всего, проигнорирует индекс — а вы получите лишние накладные расходы на запись.
  • Колонки с частой записью, по которым почти не фильтруют. Каждая вставка и обновление трогает все индексы таблицы. Индекс на колонке, по которой вы почти никогда не ищете, — это чистый минус.
  • Колонки с низкой кардинальностью сами по себе. Индекс по колонке status, у которой всего три возможных значения, мало что отсекает. Он ещё может пригодиться как вторая колонка составного индекса или в виде частичного индекса, но в одиночку чаще всего не окупается.
  • Уже покрыто другим индексом. Если есть индекс на (a, b), отдельный индекс на (a) не нужен. SQLite использует ведущие колонки составного индекса и для запросов, где фильтр идёт только по a.

Честный ответ на вопрос «а стоит ли добавить этот индекс?» почти всегда один и тот же: попробуйте, прогоните EXPLAIN QUERY PLAN, замерьте на реальных данных — и решайте.

Дальше: составные индексы

Индекс по одной колонке закрывает многие случаи, но в реальных запросах часто фильтруют и сортируют сразу по нескольким полям. Здесь в дело вступают составные индексы — индексы на (a, b, c), — и порядок колонок в них значит куда больше, чем кажется на первый взгляд. Об этом — на следующей странице.

Часто задаваемые вопросы

Как создать индекс в SQLite?

Самый простой вариант — CREATE INDEX index_name ON table_name(column_name);. Если нужна уникальность, пишите CREATE UNIQUE INDEX. Учтите: имя индекса должно быть уникальным в пределах всей базы, а не только таблицы. Удалить индекс можно командой DROP INDEX index_name;.

Когда стоит добавлять индекс в SQLite?

Индекс имеет смысл на тех колонках, по которым вы часто фильтруете, делаете JOIN или сортируете — особенно если таблица большая, а запрос возвращает лишь небольшую часть строк. Не нужно индексировать всё подряд: каждый индекс замедляет INSERT, UPDATE и DELETE, плюс занимает место на диске. И обязательно проверяйте через EXPLAIN QUERY PLAN, что планировщик действительно использует ваш индекс.

Почему SQLite не использует мой индекс?

Типичные причины: таблица настолько маленькая, что полное сканирование выходит дешевле; колонка обёрнута в функцию (например, WHERE lower(email) = ... — индекс по email тут не сработает); в запросе используется OR по неиндексированным колонкам; либо устарела статистика. Запустите ANALYZE, чтобы обновить статистику, и посмотрите план через EXPLAIN QUERY PLAN — увидите, что выбрал планировщик.

Как посмотреть все индексы таблицы в SQLite?

Для конкретной таблицы — PRAGMA index_list('table_name');. Если хочется увидеть все индексы базы, запросите напрямую sqlite_master: SELECT name, sql FROM sqlite_master WHERE type = 'index';. Записи вида sqlite_autoindex_* — это автоматические индексы, которые SQLite создаёт под PRIMARY KEY и UNIQUE-ограничения.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ