Индексирование и хеши: почему индекс на хеш бесполезен

Хеширование паролей в JavaScript и серверных приложениях обычно строится вокруг криптографических функций, таких как bcrypt, scrypt или argon2. Библиотеки уровня password-hash или аналогичные обёртки реализуют принцип однонаправленного преобразования: исходный пароль превращается в строку фиксированного или псевдофиксированного формата, которую невозможно восстановить обратно в исходное значение.

Ключевое свойство таких хешей — детерминированность при одинаковых входных данных и соли и полная непредсказуемость результата при малейшем изменении входа. Даже минимальное отличие в символе пароля приводит к радикально другому хешу.

Именно эта особенность делает попытки индексирования хешей в базах данных специфичной задачей, которая часто воспринимается неправильно.


Как работают индексы в базах данных

Индекс в реляционных базах данных — это структура данных, оптимизирующая поиск по определённому столбцу. Чаще всего используются:

  • B-tree индексы
  • Hash-индексы (внутрибазовые, не криптографические)
  • Bitmap индексы (в аналитических системах)

Основная идея индекса — ускорение операций вида:

  • равенство (=)
  • диапазоны (<, >, BETWEEN)
  • сортировка (ORDER BY)

Индекс работает эффективно, когда данные обладают структурируемой упорядоченностью или предсказуемым распределением, позволяющим быстро сузить область поиска.


Почему индекс по хешу выглядит логичным, но не даёт пользы

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

SEL ECT * FR OM users WH ERE password_hash = ?

Однако в реальной архитектуре систем хранения паролей ситуация отличается:

1. Поиск происходит не по паролю, а по идентификатору пользователя

Типичный сценарий авторизации:

SELECT password_hash FR OM users WHERE email = ?

Сначала находится пользователь, затем выполняется проверка хеша в приложении. Индекс нужен на email, а не на password_hash.


2. Хеш не используется как поисковый ключ

Хеш пароля не является полем, по которому выполняется выборка записей в массовом режиме. Он:

  • не участвует в фильтрации множества пользователей
  • используется только в одном сравнении после получения записи

Индекс на поле, по которому нет массовых запросов, теряет смысл.


3. Сильная селективность делает индекс избыточным

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

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


Соль и разрушение повторяемости данных

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

hash = H(password + salt)

Это приводит к следующим последствиям:

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

Индекс становится формально допустимым, но практически бесполезным, потому что:

  • отсутствуют повторяющиеся ключи
  • отсутствуют диапазонные операции
  • отсутствует предсказуемый порядок

Поведение индексов при случайных данных

Криптографические хеши обладают свойством равномерного распределения по пространству значений. Это означает:

  • значения распределены псевдослучайно
  • соседние входы не дают соседних выходов
  • кластеризация отсутствует

B-tree индекс в таких условиях начинает вести себя почти как случайный доступ:

  • увеличение высоты дерева
  • рост числа дисковых операций
  • отсутствие возможности эффективно сокращать пространство поиска

В итоге индекс не ускоряет поиск, а добавляет накладные расходы на поддержку структуры.


Hash-индекс базы данных и криптографический хеш — разные сущности

Важно различать:

  • hash index внутри СУБД (например, PostgreSQL hash index)
  • криптографический хеш пароля (bcrypt/argon2/scrypt)

Hash-index предназначен для ускорения точного сравнения ключей, но:

  • он работает эффективно только в памяти или при ограниченных сценариях
  • не учитывает соль и многоступенчатую обработку паролей
  • не используется как основная структура для защищённых данных

Криптографический хеш, наоборот:

  • медленный по конструкции
  • намеренно затратный для вычисления
  • не предназначен для массового индексирования

Почему индекс по password_hash ухудшает систему

Даже если технически создать индекс по полю хеша пароля, возникают негативные эффекты:

1. Рост затрат на запись

Каждая регистрация или смена пароля требует:

  • вычисления хеша
  • обновления индекса
  • перестроения структуры дерева

Это увеличивает нагрузку без реального прироста производительности запросов.


2. Отсутствие реальных сценариев использования

Практически нет запросов вида:

  • «найти всех пользователей с таким же хешем пароля»
  • «сгруппировать пользователей по хешу»
  • «отсортировать по хешу пароля»

Следовательно, индекс не обслуживает бизнес-логику.


3. Иллюзия оптимизации

Добавление индекса создаёт формальное ощущение оптимизации, но фактически:

  • ускорение отсутствует
  • план выполнения запроса не меняется
  • оптимизатор может игнорировать индекс

Связь с библиотекой password-hash в JavaScript

В JavaScript-экосистеме библиотеки уровня password-hash обычно реализуют:

  • генерацию соли
  • выбор алгоритма хеширования
  • проверку пароля через пересчёт хеша

Примерная логика:

const hash = passwordHash.generate(password);
const verify = passwordHash.verify(password, hash);

Здесь отсутствует необходимость в индексировании самого хеша, потому что:

  • поиск пользователя происходит по отдельному ключу (id, email)
  • сравнение выполняется уже в прикладной логике
  • база данных не участвует в криптографической проверке

Таким образом, хранение хеша — это операция хранения атрибута, а не механизм поиска.


Сценарии, где индекс кажется оправданным, но не является

Иногда возникает идея использовать индекс по хешу в следующих случаях:

  • быстрый поиск совпадений при дедупликации
  • проверка утечек паролей
  • анализ совпадений между пользователями

Однако даже в этих сценариях:

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

В случае утечек используются специализированные структуры (например, bloom filters), а не индексы БД.


Ошибочные архитектурные подходы

Часто встречается попытка оптимизации схемы хранения:

CRE ATE   INDEX idx_password_hash ON users(password_hash);

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

  • случайность распределения
  • отсутствие группировок
  • одноразовый характер использования поля

В результате индекс:

  • занимает место на диске
  • увеличивает время записи
  • не участвует в большинстве планов выполнения запросов

Итоговая техническая логика явления

Комбинация факторов приводит к тому, что индекс по хешу пароля оказывается нефункциональным:

  • криптографический хеш неструктурирован
  • соль разрушает повторяемость
  • доступ к полю происходит редко и точечно
  • поиск выполняется по другим индексируемым полям
  • оптимизатор не использует индекс в планах выполнения

В результате поле password_hash остаётся частью защищённого хранилища данных, но не становится элементом поисковой или индексной инфраструктуры базы данных.