Базы данных
Типы индексов: GIN, GiST, BRIN, Hash, Bloom и HOT-обновления
Команда добавляет CREATE INDEX ON events(payload) на JSONB-столбец и наблюдает, как время запроса меняется с 4 секунд… на 4 секунды. B-tree не может индексировать внутри JSONB. GIN может. Знание того, какой тип индекса соответствует какой форме данных, — это разница между рабочим индексом и потраченным часом и гигабайтом.
Почему B-tree недостаточно
B-tree индексирует одно значение на строку и отвечает на запросы equality, range и sort по этому значению. Когда строка содержит несколько значений (JSONB-ключи, элементы массива, текстовые лексемы) или когда отношение «меньше чем» не определено (геометрические фигуры, IP-диапазоны), B-tree не имеет полезного порядка сортировки. Другие типы индексов заполняют эти пробелы.
| Тип индекса | Лучше всего для | Цена |
|---|---|---|
| B-tree | Equality, range, sort на упорядоченных типах | ~10-30% размера таблицы; 5-50 мкс/запись |
| GIN | JSONB containment, массивы, full-text | 2-5x размер; медленный на high-write JSONB |
| GiST | Геопространственные (PostGIS), range-типы, nearest-neighbour | Зависит от расширения; сложное обновление |
| BRIN | Огромные insert-only таблицы с natural ordering | Крошечный размер; только при физически упорядоченных данных |
| Hash | Equality на очень широких ключах | Нет range; обычно B-tree лучше |
| Bloom | Multi-column equality, непредсказуемые комбинации фильтров | Приближённый (false positives); нужна re-проверка в heap |
GIN: inverted index для composite-типов
Generalized Inverted Index. Где B-tree хранит одно значение на строку, GIN хранит много значений на строку — каждый ключ в JSONB-документе, каждый элемент в массиве, каждый лексем в tsvector. Внутри GIN — B-tree indexed values, каждое указывает на posting list (или posting tree) TID-ов, содержащих это значение.
Use cases:
- JSONB containment:
WHERE payload @> '{"event_type":"login"}' - Array overlap:
WHERE tags && ARRAY['premium','active'] - Full-text search:
WHERE to_tsvector('english', body) @@ to_tsquery('postgres')
GIN-индексы обычно в 2-5x больше индексируемых данных столбца. На high-write JSONB-столбцах каждая вставка строки обновляет каждый indexed ключ — fastupdate режим откладывает эти обновления в background-процесс. Продакшн-дисциплина: измерь GIN write-cost после добавления на write-heavy столбец; при спайке latency вставки — рассмотри fastupdate или expression-индексы на известных горячих JSONB-полях.
GiST: generic search tree
Для данных без глобального порядка сортировки: геопространственные фигуры, IP-диапазоны, timestamp-диапазоны. GiST расширяем — каждый non-leaf узел хранит «предикат» (например, bounding box), обобщающий своё поддерево. Запросы отсекают поддеревья, проверяя предикат перед спуском.
Большинство инженеров встречают GiST через PostGIS:
CREATE INDEX ON locations USING GIST (geom);
-- включает: WHERE ST_DWithin(geom, ST_MakePoint(-74, 40.7), 1000)Range-типы (int4range, tsrange) также используют GiST по умолчанию. Nearest-neighbour запросы (ORDER BY geom <-> point LIMIT 10) требуют GiST.
BRIN: крошечный индекс для огромных упорядоченных таблиц
Block Range INdex. Хранит min и max значения per block range (дефолт: 128 страниц, ~1 MB heap). Для ответа на запрос Postgres читает BRIN-метаданные, находит candidate ranges и сканирует только эти блоки. BRIN приближённый — сужает heap-scan, но не устраняет его.
BRIN полезен только когда данные физически упорядочены по indexed-столбцу. Классический случай: time-series таблица со вставками в порядке timestamp.
-- 10 миллиардов строк, 8 TB таблица
CREATE INDEX ON events USING BRIN (created_at);
-- Результат: ~несколько MB индекс; range-запрос читает только нужные 128-страничные блокиДля случайно распределённых данных BRIN не даёт ускорения — min-max каждого block range охватывает весь диапазон.
Hash-индексы
Только equality; нет range-поддержки; нет ORDER BY. До Postgres 10 Hash-индексы не логировались в WAL и не переживали сбои — избегай в старых инсталляциях. После Postgres 10 — стабильны. Узкий use case: equality-lookup на очень широких ключах. На практике B-tree почти всегда предпочтительнее (поддерживает range и ORDER BY). Дефолт B-tree.
Bloom-индексы для multi-column equality
Расширение bloom — вероятностный индекс. Bloom на (a, b, c, d, e) ускоряет любой запрос с фильтром по любому подмножеству столбцов — без leading-column rule. Цена: approximate — каждый матч — кандидат, требующий re-проверки в heap (false positives возможны). Use case: таблицы с много столбцами, где запросы непредсказуемо комбинируют разные наборы фильтров, что делает невозможным построить 2^N composite B-tree.
HOT-обновления и fillfactor
При UPDATE Postgres создаёт новую tuple-версию (MVCC) и должен обновить каждый индекс, ссылающийся на эту строку — даже индексы с неизменёнными столбцами. Это write amplification: одно логическое обновление разворачивается в N обновлений индексов.
HOT (Heap-Only Tuple) устраняет обновления индексов при выполнении обоих условий:
- Ни один indexed-столбец не изменился в этом UPDATE.
- Новый tuple помещается на ту же heap-страницу, что и старый (есть свободное место).
Когда HOT срабатывает, меняется только heap-страница — записи индекса по-прежнему указывают на старый TID, который цепочкой ведёт к новой версии. Результат: резко сниженный write-overhead на update-heavy workloads.
Fillfactor управляет заполненностью каждой страницы при вставке (дефолт: 100% для таблиц, 90% для B-tree). Установка fillfactor в 70-80% на update-heavy таблице оставляет место на страницах для HOT:
ALTER TABLE orders SET (fillfactor = 70);Цена: начальное хранилище больше — меньший fillfactor означает больше страниц. Продакшн-команды измеряют частоту обновлений и настраивают fillfactor на горячих таблицах — часто упускаемый рычаг.
Write-cost accounting
Каждый индекс добавляет ~5-50 мкс на запись на каждый затронутый индекс. Для таблицы с 10 индексами каждый INSERT стоит 50-500 мкс в overhead на индексы сверх heap write. GIN-индексы на JSONB-столбцах с многими ключами могут стоить 50-200 мкс на запись — больше любого B-tree.
Senior-команды моделируют index budget для каждой таблицы: при заданном write-throughput target, сколько индексов может себе позволить таблица? Инструменты: pg_stat_statements для baseline latency, pgbench для синтетических write-бенчмарков под новые индексы.
Размер индекса и shared_buffers
Postgres кеширует горячие страницы в shared_buffers (типично 25% RAM сервера). Большие индексы конкурируют с heap-страницами за cache-место. 5 GB GIN-индекс на JSONB-столбце может вытеснить горячие heap-страницы, превращая запросы, ранее быстрые из cache, в disk-bound. Симптом: pg_stat_database.blks_hit / blks_read падает после добавления нового индекса. Mitigation: partial-индексы, expression-индексы на known-hot полях, удаление неиспользуемых.
Почему ANALYZE важнее самого индекса
Выбор планировщика — использовать индекс или нет — зависит от статистики таблицы (pg_statistic): распределение значений, количество строк, корреляции. Устаревшая статистика вынуждает планировщика недо- или переоценивать количество строк, соответствующих фильтру, что приводит к неверному выбору плана.
Продакшн-дисциплина:
- Запускай
ANALYZEпосле bulk-insert, больших UPDATE или schema-изменений. - Снижай
autovacuum_analyze_scale_factorдля таблиц со skewed данными (дефолт 0.2 = 20% изменений — установи 0.02 для горячих OLTP-таблиц). - Используй
CREATE STATISTICSдля коррелированных столбцов (например,country, city).
«Неправильный выбор индекса» — часто в действительности «устаревшая статистика».
- B-tree size, 8-байт key, 100M строк
- ~1 GB
- GIN size vs индексируемые данные столбца
- 2-5x больше
- BRIN size, 10B-row time-series
- ~несколько MB
- Hash-индекс в продакшне
- редко (B-tree обычно лучше)
- HOT update экономия (нет indexed-столбца)
- устраняет N обновлений индексов
- GIN write-overhead на high-write JSONB
- ~50-200 мкс на строку
- B-tree write-overhead на индекс
- ~5-50 мкс на запись
- fillfactor default (таблица / B-tree)
- 100% / 90%
- fillfactor для update-heavy HOT-friendly таблиц
- 70-80%
- ANALYZE trigger (autovacuum_analyze_scale_factor default)
- 20% строк изменено
Таблица хранит геолокационные данные, горячий запрос: WHERE ST_DWithin(geom, $1, 500). Какой тип индекса правильный?
HOT-обновления пропускают поддержку индекса. Что требуется для срабатывания HOT?
Упорядочи от лучшего к худшему для сценария: 'поиск JSONB-документов по наличию ключа'.
- 1 GIN — спроектирован для multi-value типов; нативно отвечает на @> и ? операторы
- 2 Expression B-tree на (payload->>'key') — работает для equality на known-field, но не общий containment
- 3 B-tree на payload — индексирует весь документ как непрозрачный; бесполезен для containment
- 4 BRIN на payload — только для min/max на упорядоченных данных; нерелевантен здесь
Почему это работает
Почему Postgres поставляется с шестью типами индексов вместо одного универсального? Потому что фундаментальные структуры данных несовместимы. B-tree требует тотального порядка (каждое значение сравнимо с другим). JSONB-документы не имеют тотального порядка. Геометрические фигуры требуют пространственного предиката, а не линейного ключа. Time-series блоки нуждаются в range summary. Один универсальный индекс был бы либо астрономически дорогим, либо бесполезным. Дизайн-решение — несколько специализированных типов — делает каждый индекс tight и соответствующим своей форме данных.
- 01Коллега предлагает добавить GIN-индекс на JSONB-столбец audit_log, получающий 5000 inserts/сек. Каковы write-cost последствия и какая альтернатива может работать?
- 02Объясни, когда BRIN резко лучше B-tree и когда он бесполезен.
- 03Что такое write amplification в контексте индексов и какие механизмы его снижают?
Postgres поставляется с шестью типами индексов. B-tree покрывает equality, range и sort на упорядоченных типах — 95% продакшн-использования. GIN индексирует несколько значений на строку (JSONB-ключи, элементы массивов, tsvector-лексемы) используя inverted-структуру; в 2-5x больше и медленнее на запись. GiST для геопространственных и range-типов без линейного порядка. BRIN хранит min-max per block range и полезен только при физически упорядоченных данных (time-series). Hash — equality only, редко предпочтительнее B-tree. Bloom — probabilistic multi-column equality без leading-column rule ценой false positives.
HOT-обновления пропускают поддержку индекса, когда ни один indexed-столбец не изменился и heap-страница имеет место — настраивай fillfactor 70-80% на update-heavy таблицах. GIN write-cost на high-write JSONB может достигать 50-200 мкс на вставку; сужай до expression-индексов на known-hot полях. ANALYZE должен быть актуальным для правильного выбора индекса планировщиком; устаревшая статистика — самая частая причина «неожиданного Seq Scan при существующем индексе».
- Index-only scan, Visibility Map и INCLUDEsenior
- Типичные сбои в продакшне и аудит индексовsenior
- Упражнение по проектированию индексов: стратегия полнотекстового поискаsenior
- Индексы: спроектировать и проаудитить реальный набор индексовsenior
- Индексы: тест с выбором ответаsenior
- Индексы: тест на припоминаниеsenior
встречается в174
- Путь запроса: семь остановок от сокета до ответаjunior
- Accept и парсинг: от очереди ядра до типизированного запросаmiddle
- Маршрутизация и middleware: что выполняется и в каком порядкеmiddle
- Обработчик и ответ: от бизнес-логики до байтов на проводеmiddle
- Стриминг и backpressure: когда клиент читает медленнее, чем вы пишетеsenior
- Таймауты и хвостовая задержка: бюджеты, дедлайны и ловушка fan-outsenior
- Middleware и DI: два паттерна, формирующие любой backendjunior
- Пишем middleware: сигнатуры, next() и три модели фреймворковmiddle
- Инверсия управления: как зависимости добираются до классаmiddle
- Скоупы и время жизни DI: singleton, request, transientmiddle
- DI как шов для тестов: фейки, моки и граница, которая важнаsenior
- DI-контейнеры в продакшене: графы разрешения, циклы и когда не стоитsenior
- Блокирующий vs неблокирующий I/O: два способа ждатьjunior
- Event loop: один поток, упорядоченные фазыmiddle
- Что блокирует цикл: CPU-работа и синхронные вызовыmiddle
- Вынос CPU-работы: worker threads и пул libuvmiddle
- Backpressure и ограниченная конкурентностьsenior
- Пропускная способность под нагрузкой: хвостовая задержка и насыщениеsenior
- Зачем пул: цена создания соединенияjunior
- Размер пула: почему больше не значит быстрееmiddle
- Взятие и таймауты: очередь ожидания — настоящий дроссель задержкиmiddle
- Стратегии retry: backoff, jitter и thundering herdmiddle
- Наблюдаемость, production-инциденты и дизайн для глобального масштабаsenior
- Задачи, микрозадачи и scheduler.yield()middle
- Точность таймеров, троттлинг и фоновая работаmiddle
- Event loop Node.js: фазы, nextTick и задержка циклаsenior
- Стратегии рендеринга: SSG, SSR, ISR, streaming и гидратацияjunior
- SSG, SSR, ISR, streaming и RSC — как работает каждая стратегияmiddle
- Цена гидратации: selective, progressive, острова, resumabilitymiddle
- Core Web Vitals: что измеряют LCP, INP и CLSjunior
- LCP: четыре фазы, одна доминирующая стоимостьmiddle
- INP: input delay, processing, presentationmiddle
- Lab vs field: почему они расходятся и как использовать каждыйmiddle
- Трейдоффы метрик, RUM-атрибуция и цикл CI+полеsenior
- Общая картина: от URL до LCP до INP как эстафетаjunior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Биты в проводеjunior
- Математика задержкиmiddle
- Bufferbloat и перегрузкаsenior
- Граница физического уровняsenior
- Номера последовательности и состояние соединенияmiddle
- Управление потоком и перегрузкойmiddle
- BBR, производственная наблюдаемость и за пределами TCPsenior
- CDN: контент по соседствуjunior
- Anycast и GeoDNS: маршрутизация к ближайшему edgemiddle
- Многоуровневый кеш и Cache-Controlmiddle
- Заголовок Vary и cache keysmiddle
- Stale-while-revalidate и cache stampedesenior
- Edge workers и edge-side compositionsenior
- CDN: операции и observabilitysenior
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- WebSocket vs SSE vs long-polling: выбор правильного транспортаmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Алгоритмы балансировки: от round-robin до power-of-two-choicesmiddle
- L4 vs L7 балансировка и сохранение IP клиентаmiddle
- Health checks, connection draining и slow startmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- QUIC-потоки и head-of-line blockingjunior
- Объединённое рукопожатие и 1-RTTmiddle
- Connection ID и миграция сетиmiddle
- Обнаружение потерь и управление перегрузкойmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- Развёртывание и стоимость CPUsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- Двенадцать слоёв: один URL, семь действующих лицjunior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Критический путь рендеринга и Core Web Vitalsmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Метрики и cardinality: cost-модель time-series databasemiddle
- Логи и объём: cost-модель структурного логированияmiddle
- Трейсы и сэмплирование: cost-модель distributed tracingmiddle
- Join-ключи и exemplar''''ы: как три сигнала становятся компонуемымиmiddle
- Observability 2.0: широкие события и сдвиг стоимостиsenior
- Режимы сбоя и инженерная практика: cardinality budget''''ы, PII и сэмплированиеsenior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- Log levels и маршрутизация алертовmiddle
- Стратегии sampling и стоимость логовmiddle
- PII-редакция и log injectionsenior
- Propagation trace-контекста в логахsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- Сигналы OTel, Semantic Conventions и проводной формат OTLPmiddle
- Авто-инструментирование и ручные спаны: правило 80/20 в OTelmiddle
- Collector OTel: receivers, processors, exporters и паттерны развёртыванияmiddle
- Стратегии сэмплирования: head, tail и parent-basedmiddle
- Vendor-нейтральность, eBPF-инструментирование, Operator и OTel в браузере и serverlesssenior
- Эксплуатация OTel Collector: надёжность, version skew, режимы отказа и управлениеsenior
- RED и USE: два чек-листа, одна дисциплина триажаjunior
- Инструментация RED в Prometheus: счётчики, гистограммы и дисциплина cardinalitymiddle
- USE на Linux: CPU, память, диск, сеть и PSImiddle
- Golden signals, структура дашборда и auto-RED в service meshmiddle
- Cardinality как драйвер затрат: label, PII, exemplars и семплированиеmiddle
- Native histograms, SLO и паттерны production-сбоевmiddle
- Выбор SLI и SLO-целей: отношения, не ощущенияmiddle
- Multi-window multi-burn-rate-алертинг: почему AND лучше ORmiddle
- Error budget policy, latency SLO и составные journeysmiddle
- Iceberg SLI, математика составного SLO и SLA vs SLOsenior
- Flame graph: читаем картинку, которая показывает, куда ушло времяjunior
- Sampling vs instrumentation profiling: почему 99 Гц побеждает в productionmiddle
- Типы профилей: CPU, память, off-CPU, mutex — какой когда братьmiddle
- Continuous profiling: always-on flame graphs с eBPF и корреляцией trace-idmiddle
- Как flame graph строится из сэмплов и как использовать его в productionmiddle
- Linux perf, внутренности eBPF, PGO и ограничения sampling''''аsenior
- Profiling в production: безопасность, war stories, OTel profiles и дизайн инфраструктурыsenior
- Debugging-воронка: SLO → RED → trace → profilejunior
- Архитектура OTel: один SDK, четыре сигнала, один wire-форматmiddle
- Экономия на observability: удерживаем затраты в пределах 5% inframiddle
- Масштаб, безопасность и ROI наблюдаемых системsenior
- Сначала профиль: измерь куда реально уходит времяjunior
- Закон Амдала и self-time: потолок любого ускорения, которое ты можешь выпуститьmiddle
- Измерительный цикл: микробенч, макробенч, prod-профиль, эффект наблюдателяmiddle
- Чтение флейм-графов: формы, профайлеры по языкам и 60-секундный сканmiddle
- Статистические baseline''''ы: почему один запуск — не измерениеmiddle
- История профайлеров и ловушки микробенчей: от Кнута до GWPsenior
- Hardware counters, профили холодного старта и безопасность профилейsenior
- Непрерывное профилирование в масштабе: затраты, CI-гейты, корреляция с трейсами и антипаттерныsenior
- Что делает путь горячим: симптом против причиныjunior
- Пять форм hotspot''''а: CPU, аллокации, кэш, лок, syscallmiddle
- Чтение parent и child chains: где применять правкуmiddle
- JIT deopt, цикл fix-and-verify и PR-time профилированиеmiddle
- Аппаратные счётчики и Intel TMA: диагностика подкатегорийsenior
- False sharing и горячие пути нативных мостовsenior
- Горячие пути в production: безопасность, хвостовая латентность и происхождение инструментовsenior
- Иерархия памяти: почему расстояние важнее числа операцийjunior
- Row-major vs column-major: порядок доступа и разрыв в 9xjunior
- Branch prediction: 10–30 циклов штрафа за неожиданный ifmiddle
- Hardware prefetcher, TLB и memory-level parallelismsenior
- Основы GC: за что рантайм берёт налогjunior
- Алгоритмы GC: поколенческая гипотеза, concurrent marking и write barriermiddle
- GC tradeoffs: пауза, throughput, память и давление аллокацийmiddle
- Настройка GC: пейсинг, форма кучи и наблюдаемость аллокацийmiddle
- Внутреннее устройство GC: tri-color инвариант, write barriers и глубокое погружение в рантаймыsenior
- GC в production: наблюдаемость, безопасность, edge cases и управление флотомsenior
- N+1: одна логическая операция, много round-trip''''овjunior
- Семейства фиксов: JOIN, IN, preload и DataLoadermiddle
- Обнаружение N+1: query logs, APM traces и CI gatesmiddle
- DataLoader: батчинг по дереву резолверовmiddle
- Кросс-протокольный N+1: HTTP fan-out и Redis MGETmiddle
- N+1 в масштабе: исчерпание пула, изменения планов и денормализацияsenior
- Batching: амортизируй фиксированную цену каждой операцииjunior
- Окно батчинга: размер и время ожиданияmiddle
- Batching в Kafka и Postgresmiddle
- io_uring и наблюдаемость пакетированияmiddle
- От Nagle до io_uring: эволюция пакетированияmiddle
- Backpressure, изоляция сбоев и безопасность батчей в продакшенеsenior
- Что на самом деле стоит bundle: download, parse, compile, executejunior
- Core Web Vitals: LCP, INP и CLSmiddle
- Code splitting: route-level, component-level, vendor splittingmiddle
- Tree shaking и compression: удаляем то, что не используемmiddle
- Third-party scripts: тихий убийца бюджетаmiddle
- CI enforcement и RUM: делаем бюджеты рабочимиmiddle
- V8 JIT-пайплайн, HTTP-приоритеты и безопасность bundlesenior
- Цикл performance: дисциплина, а не проектjunior
- Классификация и исправление: сопоставление family bottleneck с методамиmiddle
- Observability-стек и CI gates: ловить регрессии до выпускаmiddle
- От инцидента к enforcement: SLO burn до верифицированного исправления за 35 минутmiddle
- Культура, экономика и масштаб performancesenior