Block Range Index для очень больших таблиц с естественным порядком данных.
BRIN — это сверхкомпактный индекс для очень больших таблиц. Если B-дерево хранит каждую запись, то BRIN хранит сводку о диапазонах страниц.
BRIN (Block Range Index) — это индекс, который хранит сводную информацию о диапазонах страниц таблицы.
Вместо хранения индекса для каждой строки (как B-дерево), BRIN хранит минимум и максимум для каждого диапазона страниц.
B-дерево:
┌─────────────────────────────────┐
│ key1 → TID1 │
│ key2 → TID2 │
│ key3 → TID3 │
│ ... (миллионы записей) │
└─────────────────────────────────┘
BRIN:
┌─────────────────────────────────┐
│ Страницы 1-128: min=1, max=128 │
│ Страницы 129-256: min=129, max=256 │
│ Страницы 257-384: min=257, max=384 │
└─────────────────────────────────┘
| Сценарий | Подходит? | Почему |
|---|---|---|
| Временные ряды | ✅ Да | Данные упорядочены по времени |
| Лог-таблицы | ✅ Да | Последовательная вставка |
| Таблицы с SERIAL | ✅ Да | ID растёт монотонно |
| Случайные данные | ❌ Нет | Мин/макс на диапазон неэффективны |
| Частые UPDATE | ⚠️ Осторожно | Требуется частый VACUUM |
Таблица: 1000 страниц
BRIN с pages_per_range = 128:
┌─────────────────────────────────────────┐
│ Range 0: страницы 0-127 │
│ min(created_at) = '2026-01-01' │
│ max(created_at) = '2026-01-08' │
├─────────────────────────────────────────┤
│ Range 1: страницы 128-255 │
│ min(created_at) = '2026-01-09' │
│ max(created_at) = '2026-01-16' │
├─────────────────────────────────────────┤
│ Range 2: страницы 256-383 │
│ min(created_at) = '2026-01-17' │
│ max(created_at) = '2026-01-24' │
└─────────────────────────────────────────┘
8 диапазонов для 1000 страниц вместо миллионов записей в B-дереве
SELECT * FROM logs WHERE created_at > '2026-01-20';Эффективность: если запрос попадает в 1% диапазонов, читаем только 1% страниц.
Параметр определяет количество страниц в диапазоне:
-- По умолчанию 128 страниц на диапазон
CREATE INDEX idx ON table USING BRIN (column);
-- Меньше страниц = точнее индекс, но больше размер
CREATE INDEX idx ON table USING BRIN (column) WITH (pages_per_range = 64);
-- Больше страниц = меньше индекс, но менее точный
CREATE INDEX idx ON table USING BRIN (column) WITH (pages_per_range = 256);| pages_per_range | Размер индекса | Точность | Когда использовать |
|---|---|---|---|
| 32 | Больше | Выше | Данные неупорядочены |
| 128 (default) | Средний | Средняя | Данные упорядочены |
| 512 | Меньше | Ниже | Данные строго упорядочены |
| 1024 | Минимальный | Низкая | Очень большие таблицы |
CREATE INDEX index_name ON table_name USING BRIN (column);CREATE INDEX index_name ON table_name USING BRIN (column)
WITH (pages_per_range = 64, autosummarize = on);CREATE TABLE sensor_data (
id BIGSERIAL,
sensor_id INTEGER,
value NUMERIC,
recorded_at TIMESTAMP
);
-- BRIN по времени
CREATE INDEX idx_sensor_data_time ON sensor_data
USING BRIN (recorded_at);
-- BRIN по ID (монотонно растёт)
CREATE INDEX idx_sensor_data_id ON sensor_data
USING BRIN (id);-- Таблица с 100 млн записей (~10 GB)
CREATE TABLE big_logs (
id BIGSERIAL,
message TEXT,
created_at TIMESTAMP
);
INSERT INTO big_logs (message, created_at)
SELECT 'Log message', NOW() - (i || ' seconds')::INTERVAL
FROM generate_series(1, 100000000) AS i;
-- Индексы
CREATE INDEX idx_btree ON big_logs USING BTREE (created_at);
CREATE INDEX idx_brin ON big_logs USING BRIN (created_at);
-- Размер
SELECT
indexname,
pg_size_pretty(pg_relation_size(indexname::regclass)) as size
FROM pg_indexes
WHERE tablename = 'big_logs';Результат:
indexname | size
-------------+--------
idx_btree | 2.1 GB
idx_brin | 2.3 MB
BRIN в 900 раз меньше!
EXPLAIN ANALYZE
SELECT * FROM big_logs
WHERE created_at > NOW() - INTERVAL '1 hour';B-дерево:
Index Scan using idx_btree on big_logs
Index Cond: (created_at > ...)
Execution Time: 50 ms
BRIN:
Bitmap Heap Scan on big_logs
Recheck Cond: (created_at > ...)
-> Bitmap Index Scan on idx_brin
Index Cond: (created_at > ...)
Execution Time: 150 ms
BRIN медленнее в 3 раза, но индекс в 900 раз меньше.
-- Вставка 1 млн записей
-- С B-деревом: 120 секунд
-- С BRIN: 90 секунд
-- Без индекса: 80 секундBRIN быстрее для вставки, потому что обновляется реже (только при добавлении нового диапазона).
По умолчанию BRIN обновляется только при VACUUM. С autosummarize = on PostgreSQL автоматически обновляет сводки.
-- Включить autosummarize
CREATE INDEX idx_brin ON table USING BRIN (column)
WITH (autosummarize = on);
-- Включить для существующего индекса
ALTER INDEX idx_brin SET (autosummarize = on);| Режим | Когда использовать |
|---|---|
autosummarize = on | Частая вставка, нужны актуальные данные |
autosummarize = off | Пакетная загрузка, VACUUM по расписанию |
-- Интервал между автосуммаризациями (по умолчанию 1 час)
SET brin_auto_summarize_threshold = '30min';-- ✅ Хорошо: данные упорядочены по времени
INSERT INTO logs (created_at)
SELECT NOW() - (i || ' seconds')::INTERVAL
FROM generate_series(1, 1000000) AS i;
-- ❌ Плохо: случайные данные
INSERT INTO logs (created_at)
SELECT NOW() - (random() * 1000000 || ' seconds')::INTERVAL
FROM generate_series(1, 1000000) AS i;Почему: если данные в диапазоне 1-128 имеют min='2020-01-01' и max='2026-01-01', BRIN не может отфильтровать ни один запрос по дате.
-- ❌ Ошибка
CREATE UNIQUE INDEX idx_brin ON table USING BRIN (column);
-- ERROR: BRIN indexes cannot be unique-- Частые UPDATE требуют частого VACUUM
UPDATE logs SET created_at = NOW() WHERE id = 123;
-- BRIN не обновляется автоматически
-- Требуется VACUUM для обновления сводок-- Обновление сводок BRIN
VACUUM table_name;
-- BRIN требует VACUUM после массовых вставок/обновлений-- Полное перестроение
REINDEX INDEX idx_brin;
-- Конкурентное перестроение (PostgreSQL 12+)
REINDEX INDEX CONCURRENTLY idx_brin;-- Расширение pageinspect
CREATE EXTENSION IF NOT EXISTS pageinspect;
-- Просмотр содержимого BRIN
SELECT * FROM brin_page_items(get_raw_page('idx_brin', 1));SELECT
indexname,
pg_size_pretty(pg_relation_size(indexname::regclass)) as size,
pg_relation_size(indexname::regclass) /
pg_relation_size(relname::regclass) as ratio
FROM pg_indexes, pg_class
WHERE tablename = 'logs'
AND relname = tablename;SELECT
indexrelname,
idx_scan,
idx_tup_read,
pg_size_pretty(pg_relation_size(indexrelid)) as size
FROM pg_stat_user_indexes
WHERE relname = 'logs';-- Корреляция между физической и логической сортировкой
SELECT
attname,
correlation
FROM pg_stats
WHERE tablename = 'logs'
AND attname = 'created_at';| Correlation | Интерпретация |
|---|---|
| 1.0 | Идеально упорядочены |
| 0.5 | Частично упорядочены |
| 0.0 | Случайный порядок |
| -1.0 | Обратный порядок |
BRIN эффективен при correlation > 0.8
| Характеристика | B-дерево | BRIN |
|---|---|---|
| Размер | O(n) | O(n/pages_per_range) |
| Поиск (точка) | O(log n) | O(n/pages_per_range) |
| Поиск (диапазон) | O(log n + k) | O(n/pages_per_range + k) |
| Вставка | Медленнее | Быстрее |
| Уникальность | ✅ Да | ❌ Нет |
| Для случайных данных | ✅ Да | ❌ Нет |
✅ Выбирайте BRIN, если:
❌ Не выбирайте BRIN, если:
CREATE TABLE application_logs (
id BIGSERIAL PRIMARY KEY,
level VARCHAR(10),
message TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- BRIN для времени (основной фильтр)
CREATE INDEX idx_logs_time ON application_logs
USING BRIN (created_at) WITH (pages_per_range = 256);
-- BRIN для ID
CREATE INDEX idx_logs_id ON application_logs
USING BRIN (id);
-- Запрос: логи за последний час
SELECT * FROM application_logs
WHERE created_at > NOW() - INTERVAL '1 hour'
ORDER BY created_at DESC;CREATE TABLE sensor_readings (
sensor_id INTEGER,
reading_time TIMESTAMP,
temperature NUMERIC,
humidity NUMERIC
) PARTITION BY RANGE (sensor_id);
-- BRIN на каждой партиции
CREATE INDEX idx_readings_time ON sensor_readings
USING BRIN (reading_time) WITH (pages_per_range = 128);
-- Запрос: данные сенсора за период
SELECT * FROM sensor_readings
WHERE sensor_id = 42
AND reading_time BETWEEN '2026-01-01' AND '2026-01-31';CREATE TABLE transactions (
id BIGSERIAL,
account_id INTEGER,
amount NUMERIC,
transaction_time TIMESTAMP
);
-- Комбинированный подход
CREATE INDEX idx_transactions_account ON transactions
(account_id, transaction_time); -- B-дерево для account_id
CREATE INDEX idx_transactions_time ON transactions
USING BRIN (transaction_time); -- BRIN для времени
-- Запрос: транзакции аккаунта за период
SELECT * FROM transactions
WHERE account_id = 123
AND transaction_time > '2026-01-01';Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.