Hash-индексы для точных совпадений: устройство, ограничения и сценарии применения.
Hash-индексы специализированы на точных совпадениях. Они могут быть быстрее B-дерева для операций =, но имеют серьёзные ограничения.
Hash-индекс — это тип индекса, который использует хэш-функцию для вычисления позиции ключа. В отличие от B-дерева, он не хранит данные в отсортированном виде.
Hash-индекс поддерживает только одну операцию — точное совпадение (=).
-- ✅ Работает с Hash-индексом
SELECT * FROM users WHERE email = 'test@example.com';
-- ❌ НЕ работает с Hash-индексом
SELECT * FROM users WHERE email > 'test@example.com'; -- Ошибка или Seq Scan
SELECT * FROM users WHERE email LIKE 'test%'; -- Ошибка или Seq ScanПри вставке или поиске:
hash_value = hash_function(key)Ключ: 'user@example.com'
↓
hash_function()
↓
0x7A3F2B1C (32 бита)
↓
Bucket #45678
↓
[(hash, TID), (hash, TID), ...]
┌─────────────────────────────────────────┐
│ Metapage (страница 0) │ ← Метаданные
├─────────────────────────────────────────┤
│ Bucket 0 │
│ Bucket 1 │
│ Bucket 2 │
│ ... │
│ Bucket N │
└─────────────────────────────────────────┘
Каждый bucket содержит:
┌─────────────────────────┐
│ (hash, TID) │
│ (hash, TID) │
│ (hash, TID) │ ← Коллизии в одном бакете
└─────────────────────────┘
Коллизия — разные ключи дают одинаковый хэш.
PostgreSQL использует цепочки (chaining):
Bucket #123:
┌──────────────────────────────┐
│ hash=0xABCD, TID=(1, 5) │ ← Ключ 'abc'
│ hash=0xABCD, TID=(3, 2) │ ← Ключ 'xyz' (коллизия!)
│ hash=0xABCD, TID=(7, 1) │ ← Ключ '123' (коллизия!)
└──────────────────────────────┘
При поиске проверяются все 3 записи через точное сравнение ключей
CREATE INDEX idx_users_email ON users USING HASH (email);| Сценарий | Hash | B-дерево |
|---|---|---|
Только = | ✅ Быстрее | ✅ Хорошо |
>, <, BETWEEN | ❌ Не поддерживает | ✅ Отлично |
ORDER BY | ❌ Не поддерживает | ✅ Отлично |
LIKE с префиксом | ❌ Не поддерживает | ✅ Хорошо |
IS NULL | ❌ Не поддерживает | ✅ Хорошо |
| Операция | Hash-индекс | B-дерево |
|---|---|---|
| Поиск (=) | O(1) | O(log n) |
| Вставка | O(1) | O(log n) |
| Диапазон | N/A | O(log n + k) |
В реальности разница часто незаметна:
-- Тест на 1 млн записей
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';B-дерево:
Index Scan using idx_users_email on users
Execution Time: 0.08 ms
Hash-индекс:
Index Scan using idx_users_hash on users
Execution Time: 0.07 ms
Разница: 0.01 мс — в пределах погрешности.
Hash-индекс может дать преимущество в специфичных случаях:
= в секундуДля 99% приложений разница незаметна.
-- ❌ Эти запросы НЕ используют Hash-индекс
SELECT * FROM users WHERE email > 'test@example.com';
SELECT * FROM users WHERE email BETWEEN 'a' AND 'm';
SELECT * FROM users ORDER BY email;
SELECT * FROM users WHERE email LIKE 'test%';
SELECT DISTINCT email FROM users; -- Требуется сортировкаHash-индекс не может быть уникальным:
-- ❌ Ошибка
CREATE UNIQUE INDEX idx_unique ON users USING HASH (email);
-- ERROR: hash indexes cannot be uniqueЕсли нужна уникальность — используйте B-дерево:
-- ✅ Работает
CREATE UNIQUE INDEX idx_unique ON users (email);До версии 10 Hash-индексы не были WAL-логгируемыми:
С PostgreSQL 10 эти проблемы решены — Hash-индексы полностью безопасны.
Hash-индексы обычно меньше B-дерева для тех же данных:
-- Создаём таблицу
CREATE TABLE test_hash (
id BIGSERIAL,
email VARCHAR(255)
);
INSERT INTO test_hash (email)
SELECT 'user' || i || '@example.com'
FROM generate_series(1, 1000000) AS i;
-- Индексы
CREATE INDEX idx_btree ON test_hash USING BTREE (email);
CREATE INDEX idx_hash ON test_hash USING HASH (email);
-- Сравниваем размер
SELECT
indexname,
pg_size_pretty(pg_relation_size(indexname::regclass)) as size
FROM pg_indexes
WHERE tablename = 'test_hash';Результат:
indexname | size
-------------+--------
idx_btree | 56 MB
idx_hash | 42 MB
Почему меньше? Hash хранит 32-битный хэш вместо полного ключа.
SELECT
indexrelname,
idx_scan as scans,
idx_tup_read as tuples_read
FROM pg_stat_user_indexes
WHERE indexrelname = 'idx_users_hash';SELECT
indexname,
tablename,
indexdef
FROM pg_indexes
WHERE indexdef LIKE '%USING hash%';Оптимизатор проигнорирует Hash-индекс, если:
=-- Принудительное использование (для теста)
SET enable_seqscan = off;
SET enable_indexscan = on;= — никаких диапазонных запросов=-- Таблица справочника
CREATE TABLE countries (
code CHAR(2) PRIMARY KEY,
name VARCHAR(100)
);
-- Частые JOIN по code
CREATE INDEX idx_countries_code ON countries USING HASH (code);
-- Запрос
SELECT o.*, c.name as country_name
FROM orders o
JOIN countries c ON o.country_code = c.code
WHERE c.code = 'US';Почему Hash уместен:
=)CREATE TABLE sessions (
session_id UUID,
user_id BIGINT,
data JSONB
);
-- Поиск сессии по ID
CREATE INDEX idx_sessions_id ON sessions USING HASH (session_id);
-- Запрос
SELECT data FROM sessions WHERE session_id = '...';CREATE INDEX idx_email ON users (email); -- BTREE по умолчаниюПреимущества:
CREATE INDEX idx_email ON users (email) INCLUDE (name, created_at);Преимущества:
-- Подготовка
CREATE TABLE perf_test (
id BIGSERIAL,
email VARCHAR(255)
);
INSERT INTO perf_test (email)
SELECT 'user' || i || '@example.com'
FROM generate_series(1, 5000000) AS i;
-- Индексы
CREATE INDEX idx_btree ON perf_test USING BTREE (email);
CREATE INDEX idx_hash ON perf_test USING HASH (email);
-- Тест B-дерева
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM perf_test WHERE email = 'user2500000@example.com';
-- Тест Hash
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM perf_test WHERE email = 'user2500000@example.com';Сравните:
= — не поддерживает диапазонные запросыВопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.