Senior Go Interview Prep - Core Go: https://go.vbloher.org/docs/01-core-go/ - Механика defer в Go: https://go.vbloher.org/docs/01-core-go/defer/ - Встраивание структур и интерфейсов (Embedding): https://go.vbloher.org/docs/01-core-go/embedding/ - Ошибки в Go: error, wrapping, errors.Is/As/Join: https://go.vbloher.org/docs/01-core-go/errors/ - Дженерики в Go (1.18+): https://go.vbloher.org/docs/01-core-go/generics/ - Интерфейсы в Go: https://go.vbloher.org/docs/01-core-go/interfaces/ - Устройство map в Go: https://go.vbloher.org/docs/01-core-go/maps/ - panic / recover: механика, раскрутка стека и runtime-паники: https://go.vbloher.org/docs/01-core-go/panic-recover/ - Указатели в Go: https://go.vbloher.org/docs/01-core-go/pointers/ - Рефлексия в Go (reflect): https://go.vbloher.org/docs/01-core-go/reflection/ - Внутреннее устройство слайсов в Go: https://go.vbloher.org/docs/01-core-go/slices/ - Строки, руны и байты в Go: https://go.vbloher.org/docs/01-core-go/strings-runes-bytes/ - Система типов Go: defined types, alignment, memory layout: https://go.vbloher.org/docs/01-core-go/type-system/ - Concurrency: https://go.vbloher.org/docs/02-concurrency/ - sync/atomic: https://go.vbloher.org/docs/02-concurrency/atomic/ - Буферизованные vs небуферизованные каналы: https://go.vbloher.org/docs/02-concurrency/buffered-unbuffered/ - Канал vs Mutex: когда что выбрать: https://go.vbloher.org/docs/02-concurrency/channel-vs-mutex/ - Каналы: устройство hchan: https://go.vbloher.org/docs/02-concurrency/channels/ - Утечки горутин, дедлоки, livelock, starvation: https://go.vbloher.org/docs/02-concurrency/common-leaks-deadlocks/ - sync.Cond: https://go.vbloher.org/docs/02-concurrency/cond/ - context: https://go.vbloher.org/docs/02-concurrency/context/ - Горутины: жизненный цикл, стоимость, стек: https://go.vbloher.org/docs/02-concurrency/goroutines-lifecycle/ - sync.Mutex и sync.RWMutex: https://go.vbloher.org/docs/02-concurrency/mutex-rwmutex/ - sync.Once: https://go.vbloher.org/docs/02-concurrency/once/ - Паттерны конкурентности: https://go.vbloher.org/docs/02-concurrency/patterns/ - Race Detector (гонки данных и -race): https://go.vbloher.org/docs/02-concurrency/race-detector/ - Планировщик GMP: https://go.vbloher.org/docs/02-concurrency/scheduler-gmp/ - select: https://go.vbloher.org/docs/02-concurrency/select/ - sync.WaitGroup: https://go.vbloher.org/docs/02-concurrency/waitgroup/ - Runtime и память: https://go.vbloher.org/docs/03-runtime-memory/ - Паттерны аллокаций и снижение давления на GC: https://go.vbloher.org/docs/03-runtime-memory/allocation-patterns/ - Escape Analysis: когда переменная убегает в кучу: https://go.vbloher.org/docs/03-runtime-memory/escape-analysis/ - Сборщик мусора Go: concurrent tri-color mark-sweep: https://go.vbloher.org/docs/03-runtime-memory/gc/ - Тюнинг GC: GOGC и GOMEMLIMIT: https://go.vbloher.org/docs/03-runtime-memory/gogc-gomemlimit/ - GOMAXPROCS: параллелизм планировщика и проблема контейнеров: https://go.vbloher.org/docs/03-runtime-memory/gomaxprocs/ - Утечки горутин (goroutine leaks): https://go.vbloher.org/docs/03-runtime-memory/goroutine-leaks/ - Утечки памяти в Go (несмотря на GC): https://go.vbloher.org/docs/03-runtime-memory/memory-leaks/ - Модель памяти Go (Go Memory Model): happens-before и синхронизация: https://go.vbloher.org/docs/03-runtime-memory/memory-model/ - pprof: профилирование CPU, памяти и блокировок в Go: https://go.vbloher.org/docs/03-runtime-memory/pprof/ - Execution Tracer и runtime/trace: тайминги вместо агрегатов: https://go.vbloher.org/docs/03-runtime-memory/runtime-tracing/ - Стек vs Куча: где живут данные в Go: https://go.vbloher.org/docs/03-runtime-memory/stack-vs-heap/ - Тестирование: https://go.vbloher.org/docs/04-testing/ - testify, assert/require и golden files: https://go.vbloher.org/docs/04-testing/assertions-testify/ - Бенчмарки в Go: https://go.vbloher.org/docs/04-testing/benchmarks/ - Покрытие, -race и флаки-тесты: https://go.vbloher.org/docs/04-testing/coverage-race/ - Нативный fuzzing в Go (1.18+): https://go.vbloher.org/docs/04-testing/fuzzing/ - Интеграционные тесты, testcontainers-go, TestMain: https://go.vbloher.org/docs/04-testing/integration-testcontainers/ - Моки, стабы и тестируемость: https://go.vbloher.org/docs/04-testing/mocks/ - Table-driven тесты, subtests и параллельность: https://go.vbloher.org/docs/04-testing/table-driven/ - Backend: https://go.vbloher.org/docs/05-backend/ - Аутентификация и авторизация: AuthN/AuthZ, сессии vs токены, RBAC/ABAC, API keys, mTLS, секреты: https://go.vbloher.org/docs/05-backend/auth-authz/ - Graceful Shutdown HTTP/gRPC сервера в Go: https://go.vbloher.org/docs/05-backend/graceful-shutdown/ - gRPC: типы RPC, интерсепторы, контекст, метаданные, error model: https://go.vbloher.org/docs/05-backend/grpc/ - JWT (JSON Web Token): https://go.vbloher.org/docs/05-backend/jwt/ - Middleware-паттерн в Go: https://go.vbloher.org/docs/05-backend/middleware/ - net/http: Server, Handler, ServeMux, таймауты, Client и контекст: https://go.vbloher.org/docs/05-backend/net-http/ - OAuth2: роли, grant types, OIDC, токены и типовые ошибки: https://go.vbloher.org/docs/05-backend/oauth2/ - OpenAPI/Swagger, code generation, contract-first vs code-first, валидация: https://go.vbloher.org/docs/05-backend/openapi/ - Protocol Buffers: схемы, wire format, эволюция и совместимость: https://go.vbloher.org/docs/05-backend/protobuf/ - REST: принципы, версионирование, идемпотентность, статусы, пагинация, ошибки: https://go.vbloher.org/docs/05-backend/rest/ - Сети и протоколы: https://go.vbloher.org/docs/06-networking/ - Пулы соединений: http.Transport, БД, утечки: https://go.vbloher.org/docs/06-networking/connection-pooling/ - DNS: записи, резолвинг, кэширование, DNS в Go: https://go.vbloher.org/docs/06-networking/dns/ - Версии HTTP: 1.1, 2, 3: https://go.vbloher.org/docs/06-networking/http-versions/ - TCP/IP: модель, транспорт и что важно бэкендеру: https://go.vbloher.org/docs/06-networking/tcp-ip/ - TLS: handshake, сертификаты, mTLS, производительность: https://go.vbloher.org/docs/06-networking/tls/ - UDP и надёжность поверх UDP: https://go.vbloher.org/docs/06-networking/udp/ - WebSocket: upgrade, фреймы, масштабирование: https://go.vbloher.org/docs/06-networking/websocket/ - Базы данных: https://go.vbloher.org/docs/07-databases/ - Пул соединений к PostgreSQL в Go: database/sql, pgx, pgxpool, PgBouncer: https://go.vbloher.org/docs/07-databases/connection-pooling-pgx/ - Взаимоблокировки (Deadlocks) в PostgreSQL: https://go.vbloher.org/docs/07-databases/deadlocks/ - Индексы в PostgreSQL: https://go.vbloher.org/docs/07-databases/indexes/ - Уровни изоляции транзакций в PostgreSQL: https://go.vbloher.org/docs/07-databases/isolation-levels/ - MVCC в PostgreSQL: версии строк, видимость, VACUUM и bloat: https://go.vbloher.org/docs/07-databases/mvcc/ - Обзор NoSQL и Redis: https://go.vbloher.org/docs/07-databases/nosql-redis/ - Партиционирование таблиц в PostgreSQL: https://go.vbloher.org/docs/07-databases/partitioning/ - Архитектура PostgreSQL: https://go.vbloher.org/docs/07-databases/postgresql-architecture/ - Планирование и оптимизация запросов в PostgreSQL: https://go.vbloher.org/docs/07-databases/query-planning/ - Репликация в PostgreSQL: https://go.vbloher.org/docs/07-databases/replication/ - Шардирование (горизонтальное масштабирование): https://go.vbloher.org/docs/07-databases/sharding/ - Транзакции в PostgreSQL и Go (database/sql, pgx): https://go.vbloher.org/docs/07-databases/transactions/ - Распределённые системы: https://go.vbloher.org/docs/08-distributed-systems/ - CAP теорема: https://go.vbloher.org/docs/08-distributed-systems/cap-theorem/ - Circuit Breaker: https://go.vbloher.org/docs/08-distributed-systems/circuit-breaker/ - Консенсус и Raft: репликация состояния в присутствии отказов: https://go.vbloher.org/docs/08-distributed-systems/consensus-raft/ - Модели согласованности: https://go.vbloher.org/docs/08-distributed-systems/consistency/ - Гарантии доставки сообщений: at-most-once / at-least-once / exactly-once: https://go.vbloher.org/docs/08-distributed-systems/delivery-guarantees/ - Eventual Consistency: https://go.vbloher.org/docs/08-distributed-systems/eventual-consistency/ - Идемпотентность в распределённых системах: https://go.vbloher.org/docs/08-distributed-systems/idempotency/ - Apache Kafka: https://go.vbloher.org/docs/08-distributed-systems/kafka/ - Transactional Outbox: https://go.vbloher.org/docs/08-distributed-systems/outbox/ - RabbitMQ: AMQP 0-9-1, маршрутизация, надёжность доставки и сравнение с Kafka: https://go.vbloher.org/docs/08-distributed-systems/rabbitmq/ - Ретраи: backoff, jitter, budgets и идемпотентность: https://go.vbloher.org/docs/08-distributed-systems/retries/ - Saga Pattern: https://go.vbloher.org/docs/08-distributed-systems/saga/ - Observability: https://go.vbloher.org/docs/09-observability/ - Grafana: https://go.vbloher.org/docs/09-observability/grafana/ - Метрики: RED, USE, Golden Signals: https://go.vbloher.org/docs/09-observability/metrics/ - OpenTelemetry: https://go.vbloher.org/docs/09-observability/opentelemetry/ - Prometheus: https://go.vbloher.org/docs/09-observability/prometheus/ - SLI / SLO / SLA: https://go.vbloher.org/docs/09-observability/slo-sli/ - Структурированное логирование (slog): https://go.vbloher.org/docs/09-observability/structured-logging/ - Distributed Tracing: https://go.vbloher.org/docs/09-observability/tracing/ - System Design: https://go.vbloher.org/docs/10-system-design/ - Analytics Pipeline: https://go.vbloher.org/docs/10-system-design/analytics-pipeline/ - Chat System: https://go.vbloher.org/docs/10-system-design/chat/ - Фреймворк System Design интервью: https://go.vbloher.org/docs/10-system-design/framework/ - Notification Service: https://go.vbloher.org/docs/10-system-design/notification-service/ - Order Service: https://go.vbloher.org/docs/10-system-design/order-service/ - Payment Service: https://go.vbloher.org/docs/10-system-design/payment-service/ - Rate Limiter: https://go.vbloher.org/docs/10-system-design/rate-limiter/ - URL Shortener: https://go.vbloher.org/docs/10-system-design/url-shortener/ - DevOps: https://go.vbloher.org/docs/11-devops/ - CI/CD: пайплайны, стадии, стратегии деплоя: https://go.vbloher.org/docs/11-devops/cicd/ - Облака (AWS / GCP) для бэкендера: https://go.vbloher.org/docs/11-devops/cloud-aws-gcp/ - Docker для Go-разработчика: https://go.vbloher.org/docs/11-devops/docker/ - GitHub Actions и GitLab CI: https://go.vbloher.org/docs/11-devops/github-gitlab-ci/ - Kubernetes для Go-разработчика: https://go.vbloher.org/docs/11-devops/kubernetes/ - Terraform / Infrastructure as Code: https://go.vbloher.org/docs/11-devops/terraform/ - Алгоритмы: https://go.vbloher.org/docs/12-algorithms/ - Типовые алгоритмические задачи и паттерны: https://go.vbloher.org/docs/12-algorithms/common-problems/ - Асимптотическая сложность (Big-O): https://go.vbloher.org/docs/12-algorithms/complexity/ - Структуры данных в Go: https://go.vbloher.org/docs/12-algorithms/data-structures/ - Специфика live-coding на Go: https://go.vbloher.org/docs/12-algorithms/go-specifics/ - Behavioral: https://go.vbloher.org/docs/13-behavioral/ - Конфликты, разногласия и работа со стейкхолдерами: https://go.vbloher.org/docs/13-behavioral/conflicts/ - Как проходит senior-интервью: этапы, оценка, оффер: https://go.vbloher.org/docs/13-behavioral/interview-flow/ - Лидерство и менторство: https://go.vbloher.org/docs/13-behavioral/leadership-mentoring/ - Типовые поведенческие вопросы для Senior: https://go.vbloher.org/docs/13-behavioral/senior-questions/ > Модуль: Базы данных · Уровень: Senior ## TL;DR - **B-tree** — дефолтный индекс, отсортированное сбалансированное дерево. Работает на `=`, `<`, `>`, `BETWEEN`, `IN`, `IS NULL`, `LIKE 'prefix%'`, `ORDER BY`. Поддерживает уникальность. 90% случаев — это он. - **Hash** — только `=`. С PG 10 WAL-логируется и crash-safe. Чуть компактнее B-tree на равенстве, но без диапазонов и сортировки — почти всегда B-tree предпочтительнее. - **GIN** — для составных значений: `jsonb`, массивы (`@>`, `&&`), full-text (`tsvector @@ tsquery`), `pg_trgm` (LIKE/ILIKE/похожесть). Хранит «значение → список TID». Медленный на запись, очень быстрый на поиск; есть `fastupdate` (pending list). - **GiST** — обобщённое дерево для «перекрытий»: геоданные (PostGIS), диапазоны (`tsrange &&`), kNN (`ORDER BY point <-> point`), exclusion constraints. Lossy по природе. - **SP-GiST** — несбалансированные структуры (quadtree, radix), для непересекающихся данных (IP, точки). - **BRIN** — block range index. Крошечный, хранит min/max по блокам. Только для больших таблиц с физической корреляцией (timestamp append-only). Не точечный — даёт кандидатов на recheck. - **Составной индекс**: порядок колонок решает всё. Leftmost prefix rule. Equality-колонки слева, range — справа, по последней range-колонке возможен `ORDER BY`. - **Partial** (`WHERE`), **covering/INCLUDE** (index-only scan + visibility map), **expression index** (индекс по `lower(email)`). - Индекс **не используется** при низкой селективности, функции/каста над колонкой, type mismatch, `OR` (иногда), `LIKE '%x'`, устаревшей статистике. - Каждый индекс — налог на каждый `INSERT/UPDATE/DELETE`. HOT-update спасает только если меняемые колонки не индексированы. Настраивай `fillfactor`, следи за bloat. ## Теория ### Зачем индекс и общая модель Индекс — это вспомогательная структура данных, которая позволяет находить строки без полного сканирования таблицы (heap). В PostgreSQL индексы **отделены от таблицы**: индекс хранит ключ + указатель `TID` (ctid = `(block, offset)`), указывающий на физическое расположение версии строки в heap. Ключевое следствие архитектуры MVCC: индекс ссылается на **конкретную версию строки (tuple)**, а не на «логическую строку». Это объясняет write amplification, необходимость visibility map для index-only scan и причину bloat индексов. Планировщик выбирает между: - **Seq Scan** — последовательное чтение всего heap (хорош при высокой селективности выборки = много строк). - **Index Scan** — обход индекса + случайные обращения в heap за каждой строкой. - **Index Only Scan** — данные берутся только из индекса (нужна visibility map). - **Bitmap Index Scan / Bitmap Heap Scan** — строит битмап TID'ов, сортирует по блокам, читает heap последовательно. Хорош для средней селективности и для комбинации нескольких индексов (`BitmapAnd`, `BitmapOr`). ```sql EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE user_id = 42; ``` --- ### B-tree Сбалансированное многоуровневое дерево (вариант B+tree Lehman–Yao с поддержкой конкурентного доступа). Листья хранят отсортированные ключи и связаны в двусвязный список — поэтому возможен эффективный диапазонный обход и обход в обе стороны для `ORDER BY ... DESC`. **Что обслуживает:** - Равенство: `=`, `IN (...)`. - Диапазоны: `<`, `<=`, `>`, `>=`, `BETWEEN`. - `IS NULL` / `IS NOT NULL` (NULL-ы хранятся в индексе). - Префиксный поиск: `LIKE 'abc%'`, `~ '^abc'` — но только если индекс на `text_pattern_ops` при не-C локали (см. ниже). - Сортировку: `ORDER BY col` и `ORDER BY col DESC` без отдельного шага Sort. - Уникальность (`UNIQUE`, PK), exclusion на равенство. **Сортировка и направление.** Индекс хранит ключи в определённом порядке (`ASC NULLS LAST` по умолчанию). Если запрос требует другого порядка, планировщик может пройти индекс в обратную сторону. Но для составного индекса со смешанными направлениями нужно совпадение: ```sql -- индекс CREATE INDEX idx ON events (created_at DESC, id ASC); -- этот ORDER BY будет index-ordered (без Sort): SELECT * FROM events ORDER BY created_at DESC, id ASC LIMIT 50; -- а этот потребует Sort или обратного прохода только если направления зеркальны целиком: SELECT * FROM events ORDER BY created_at ASC, id DESC LIMIT 50; -- OK (полное зеркало) SELECT * FROM events ORDER BY created_at ASC, id ASC LIMIT 50; -- НЕ index-ordered -> Sort ``` **Локаль и операторные классы.** При локали отличной от `C` сравнение строк нелинейно относительно байтов, поэтому `LIKE 'abc%'` по умолчанию **не** использует обычный B-tree. Нужен специальный класс: ```sql CREATE INDEX idx_login_prefix ON users (login text_pattern_ops); -- теперь работает: SELECT * FROM users WHERE login LIKE 'admin%'; -- но для = по-прежнему лучше обычный индекс (text_pattern_ops не годен для ORDER BY в локали) ``` **Дедупликация (PG 13+).** B-tree умеет сжимать повторяющиеся ключи, храня один ключ + список TID. Сильно уменьшает размер индекса по колонке с низкой кардинальностью внутри составного индекса. ```go // pgx: типичный equality + range запрос rows, err := pool.Query(ctx, `SELECT id, amount FROM orders WHERE user_id = $1 AND created_at >= $2 ORDER BY created_at DESC LIMIT 100`, userID, since) ``` --- ### Hash Хранит 32-битный хеш ключа. Поддерживает **только** `=`. Никаких диапазонов, сортировки, префиксов, multicolumn. | | B-tree | Hash | |---|---|---| | Операторы | `=`, `<`, `>`, range, `ORDER BY` | только `=` | | Multicolumn | да | нет (PG) | | Размер | больше | меньше для длинных ключей | | WAL / crash-safe | да | да (с PG 10) | | Unique | да | нет | До PG 10 Hash-индексы не писались в WAL (не реплицировались, терялись при краше) — поэтому исторически их избегали. Сейчас они валидны, но выигрыш мал и узок: имеет смысл на очень длинных ключах (длинные строки, UUID как text), где хеш фиксированной длины экономит место и сравнения по `=` идут чаще, чем диапазоны. На практике дефолт — B-tree. --- ### GIN (Generalized Inverted Index) Инвертированный индекс: для **каждого элемента внутри значения** хранится список строк, где он встречается. Идеален, когда одно значение содержит много искомых единиц. **Применения:** - `jsonb` — операторы `@>`, `?`, `?|`, `?&`. Класс `jsonb_path_ops` компактнее, но поддерживает только `@>`. - Массивы — `@>` (содержит), `<@` (содержится в), `&&` (пересекается), `=`. - Full-text — `tsvector @@ tsquery`. - `pg_trgm` — `LIKE '%substr%'`, `ILIKE`, похожесть (`%` оператор). ```sql -- jsonb CREATE INDEX idx_doc ON documents USING gin (data jsonb_path_ops); SELECT * FROM documents WHERE data @> '{"status":"active"}'; -- массив тегов CREATE INDEX idx_tags ON articles USING gin (tags); SELECT * FROM articles WHERE tags @> ARRAY['go','postgres']; -- full-text с генерируемой колонкой (PG 12+) ALTER TABLE articles ADD COLUMN tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || body)) STORED; CREATE INDEX idx_tsv ON articles USING gin (tsv); SELECT * FROM articles WHERE tsv @@ websearch_to_tsquery('english', 'golang index'); -- подстрочный LIKE через триграммы CREATE EXTENSION pg_trgm; CREATE INDEX idx_name_trgm ON users USING gin (name gin_trgm_ops); SELECT * FROM users WHERE name ILIKE '%vova%'; ``` **fastupdate / pending list.** Запись в GIN дорогая (нужно обновить много posting list). PG буферизует вставки в «pending list» и вливает их пачкой при vacuum или при достижении `gin_pending_list_limit`. Плюс — быстрые вставки; минус — поиск должен сканировать pending list, и периодически случается дорогой merge. Для read-heavy таблиц иногда `fastupdate=off`. --- ### GiST (Generalized Search Tree) Каркас для построения деревьев под произвольные предикаты «перекрытия/близости». Внутренние узлы хранят предикат, покрывающий поддерево (например, bounding box). Поиск отсекает поддеревья, чьи предикаты не пересекаются с запросом. **Применения:** - Геоданные (PostGIS): `&&`, `ST_Contains`, `ST_DWithin`. - Диапазонные типы: `int4range`, `tstzrange` с `&&` (пересечение), `@>`. - **kNN-поиск** (ближайшие соседи) через оператор расстояния: ```sql CREATE INDEX idx_geom ON places USING gist (location); -- ближайшие 10 точек к заданной — индекс отдаёт их в порядке расстояния SELECT id, location <-> point '(40.0, 30.0)' AS dist FROM places ORDER BY location <-> point '(40.0, 30.0)' LIMIT 10; -- exclusion constraint: запретить пересекающиеся брони одной комнаты CREATE EXTENSION btree_gist; ALTER TABLE bookings ADD CONSTRAINT no_overlap EXCLUDE USING gist (room_id WITH =, during WITH &&); ``` GiST **lossy**: индекс может вернуть кандидатов, которые требуется перепроверить на heap (recheck). Также GiST не гарантирует уникальность ключей (структура несбалансирована по содержимому). **SP-GiST** — родственник для разделяемых непересекающихся пространств (quadtree, k-d tree, radix-trie): IP-адреса (`inet`), точки, префиксы строк. Когда данные естественно разбиваются на непересекающиеся регионы. --- ### BRIN (Block Range Index) Не индексирует строки — индексирует **диапазоны блоков**. Для каждого «range» (по умолчанию 128 страниц) хранит сводку: min/max значения. Размер индекса — килобайты на гигабайтную таблицу. Работает только при **физической корреляции** между порядком значений и порядком хранения. Классика — append-only таблица с `created_at`: новые строки физически идут в конец, поэтому min/max по блокам монотонны. ```sql CREATE INDEX idx_brin_ts ON metrics USING brin (recorded_at) WITH (pages_per_range = 128); SELECT * FROM metrics WHERE recorded_at >= now() - interval '1 hour'; ``` При запросе BRIN исключает range'и, чьи min/max не пересекают условие, и читает только подходящие блоки (с обязательным recheck каждой строки). Если корреляция слабая (данные перемешаны / частые UPDATE), BRIN почти бесполезен — каждый range покрывает весь диапазон значений. Поддерживается `brin_summarize_new_values()` / автосуммаризация для новых блоков. | Индекс | Размер | Скорость поиска | Стоимость записи | Когда | |---|---|---|---|---| | B-tree | средний | очень высокая | средняя | дефолт, =, range, sort | | Hash | малый | высокая (=) | средняя | только =, длинные ключи | | GIN | большой | высокая (поиск в составном) | высокая | jsonb, массивы, FTS, trgm | | GiST | средний | высокая (overlap/kNN) | средняя-высокая | гео, диапазоны, exclusion | | BRIN | крошечный | средняя (recheck) | очень низкая | огромные таблицы, корреляция | --- ### Составные (multicolumn) индексы Один индекс по нескольким колонкам. Порядок колонок критичен. **Leftmost prefix rule.** B-tree эффективно использует только **непрерывный левый префикс**, причём все колонки до последней использованной должны участвовать по равенству. ```sql CREATE INDEX idx ON t (a, b, c); ``` | Запрос | Использование | |---|---| | `WHERE a = 1` | да (префикс a) | | `WHERE a = 1 AND b = 2` | да (a,b) | | `WHERE a = 1 AND b = 2 AND c = 3` | да полностью | | `WHERE a = 1 AND c = 3` | частично: только a как граница, c как фильтр внутри | | `WHERE b = 2` | нет эффективного поиска (b не лидирует) — обычно Seq/Bitmap по другому индексу | | `WHERE a = 1 AND b > 5 AND c = 3` | a,b как границы; c уже не сужает дерево (после range) | **Правило проектирования:** колонки с равенством — слева, диапазон — последним. После первой range-колонки остальные колонки индекса для поиска бесполезны (но могут давать порядок). По последней range/eq-колонке индекс может обслуживать `ORDER BY`. ```sql -- equality (status, user_id), затем range/sort по created_at CREATE INDEX idx ON orders (status, user_id, created_at DESC); SELECT * FROM orders WHERE status = 'paid' AND user_id = 7 ORDER BY created_at DESC LIMIT 20; -- index-ordered, без Sort ``` PG также умеет **skip scan**-подобное поведение через несколько Bitmap-индексов, но это не замена правильному порядку колонок. --- ### Partial индексы (`WHERE`) Индекс строится только по подмножеству строк. Меньше размер, дешевле запись, плотнее. ```sql -- индексируем только активные строки — soft delete CREATE INDEX idx_active_email ON users (email) WHERE deleted_at IS NULL; SELECT * FROM users WHERE email = $1 AND deleted_at IS NULL; -- использует индекс -- частичный уникальный индекс: уникальность только среди активных CREATE UNIQUE INDEX uniq_active_login ON users (login) WHERE deleted_at IS NULL; -- очередь: индекс только по необработанным CREATE INDEX idx_pending ON jobs (created_at) WHERE state = 'pending'; ``` Важно: планировщик использует partial-индекс только если **условие запроса доказуемо влечёт предикат индекса**. `WHERE deleted_at IS NULL` подойдёт, а `WHERE deleted_at IS NULL OR x` — нет. --- ### Covering / INCLUDE и index-only scan **Index-only scan** — данные читаются из индекса без обращения к heap. Условие: все нужные колонки есть в индексе И строка видима по visibility map. `INCLUDE` (PG 11+) добавляет «полезную нагрузку» в листья индекса без участия в ключе (не влияет на сортировку/уникальность, не используется для поиска): ```sql CREATE INDEX idx_cov ON orders (user_id) INCLUDE (amount, status); SELECT amount, status FROM orders WHERE user_id = 42; -- может стать Index Only Scan ``` **Visibility map (VM).** PG не хранит видимость прямо в индексе. Чтобы пропустить обращение в heap, страница heap должна быть помечена «all-visible» в VM. VM обновляет **VACUUM**. Поэтому на таблице с активной записью без vacuum index-only scan вырождается в обычный (видно в EXPLAIN: `Heap Fetches: N`). Высокий `Heap Fetches` — сигнал, что нужен vacuum/autovacuum-тюнинг. ```sql EXPLAIN (ANALYZE) SELECT amount FROM orders WHERE user_id = 42; -- Index Only Scan ... Heap Fetches: 0 <- хорошо ``` INCLUDE vs ключ: класть колонку в ключ нужно, если по ней идёт поиск/сортировка; в INCLUDE — если она нужна только для возврата. INCLUDE дешевле (нет порядка), но раздувает листья. --- ### Expression (functional) индексы Индекс по результату выражения. Запрос должен использовать **точно то же выражение**. ```sql -- регистронезависимый поиск CREATE INDEX idx_lower_email ON users (lower(email)); SELECT * FROM users WHERE lower(email) = lower($1); -- использует индекс -- по полю jsonb CREATE INDEX idx_country ON profiles ((data ->> 'country')); SELECT * FROM profiles WHERE data ->> 'country' = 'RU'; -- по дате из timestamp (но осторожно с time zone и immutability) CREATE INDEX idx_day ON events ((created_at::date)); ``` Выражение должно быть `IMMUTABLE`. `now()`, `current_date`, нестабильные касты к timestamptz нельзя. Статистика собирается по выражению как по виртуальной колонке (`ANALYZE` это делает автоматически для expression-индекса) — это бонус для оценки селективности нестандартных предикатов. --- ### Когда индекс НЕ используется 1. **Низкая селективность.** Если условие отбирает большую долю таблицы (часто > ~5-10%), Seq Scan дешевле случайных чтений. Пример: `WHERE is_active = true`, когда 95% активны. Это правильное решение планировщика, а не баг. 2. **Функция/каст над колонкой.** `WHERE lower(email) = ...` не использует индекс по `email` (нужен expression-индекс). `WHERE date(created_at) = '...'` ломает индекс по `created_at` — переписывай на range `created_at >= d AND created_at < d+1`. 3. **Type mismatch / неявный каст.** Колонка `varchar`, а сравнение с числом; или `bigint`-колонка сравнивается с `text`-параметром. Каст применяется к колонке → индекс не годится. В Go это часто результат биндинга не того типа. 4. **`OR` по разным колонкам.** `WHERE a = 1 OR b = 2` может не дать индексного плана по одному индексу; решение — два индекса + `BitmapOr`, либо переписать через `UNION`. 5. **Leading wildcard.** `LIKE '%abc'` или `LIKE '%abc%'` не использует обычный B-tree (нет префикса) — нужен `pg_trgm` GIN/GiST. 6. **Устаревшая статистика.** После массовой загрузки без `ANALYZE` планировщик ошибается в оценке строк. Лечится `ANALYZE table;` и autovacuum-настройками. 7. **NULL-семантика.** Стандартный B-tree индексирует NULL, но некоторые операции (`<> `, `NOT IN`) плохо ложатся на индекс. 8. **Маленькая таблица.** Seq Scan дешевле — нормально. ```sql -- плохо: каст ломает индекс по created_at WHERE created_at::date = '2026-06-14' -- хорошо: sargable range WHERE created_at >= '2026-06-14' AND created_at < '2026-06-15' ``` ```go // type mismatch в Go: колонка bigint, а сюда прилетает строка // плохо — драйвер пошлёт text, PG кастует колонку pool.Query(ctx, `SELECT * FROM t WHERE id = $1`, "42") // хорошо — числовой тип pool.Query(ctx, `SELECT * FROM t WHERE id = $1`, int64(42)) ``` Диагностика: `EXPLAIN (ANALYZE, BUFFERS)` — сравни `rows` (оценка) с `actual rows`. Большое расхождение = проблема со статистикой/предикатом. --- ### Стоимость на запись Индексы ускоряют чтение ценой записи и обслуживания. **Write amplification.** Каждый `INSERT` обновляет heap **и каждый** индекс. Каждый `DELETE` помечает версию мёртвой (индексные записи остаются до vacuum). `UPDATE` в MVCC = новая версия строки → новые TID → запись во **все** индексы (а не только в те, чьи колонки изменились). **HOT (Heap-Only Tuple) update.** Оптимизация: если изменяемые колонки **не входят ни в один индекс** и на странице есть место, новая версия пишется на ту же страницу и связывается цепочкой; индексы НЕ трогаются. Это резко снижает write amplification и bloat. - Условие HOT: ни одна обновляемая колонка не индексирована (включая expression/partial/INCLUDE-ключи — но INCLUDE-колонки в современных версиях не блокируют HOT так же, как ключевые; проверяй версию). - Дать HOT место помогает `fillfactor`. ```sql -- оставить 20% свободного места на странице под будущие HOT-обновления ALTER TABLE orders SET (fillfactor = 80); -- для часто обновляемых таблиц 70-90; для append-only оставляй 100 (дефолт) ``` **Fillfactor для индексов.** B-tree-индексы тоже имеют fillfactor (дефолт 90). Низкий fillfactor уменьшает расщепления страниц (page splits) при вставках, но увеличивает размер. **Bloat и обслуживание.** Мёртвые версии и удалённые индексные записи накапливаются. Лечение: - `VACUUM` — освобождает место под reuse, чистит индексы, обновляет visibility map (нужна для index-only scan). - `ANALYZE` — обновляет статистику. - `REINDEX CONCURRENTLY` (PG 12+) — перестроить раздутый индекс без долгой блокировки. - `CREATE INDEX CONCURRENTLY` — построить индекс без блокировки записи (дольше, два прохода, может оставить INVALID индекс при сбое). ```sql CREATE INDEX CONCURRENTLY idx ON big_table (col); -- не блокирует writes REINDEX INDEX CONCURRENTLY idx; -- де-bloat без эксклюзивной блокировки -- проверка INVALID индексов после сбоя CONCURRENTLY: SELECT indexrelid::regclass FROM pg_index WHERE NOT indisvalid; ``` **Стоимость лишних индексов:** замедляют запись, едят место и кэш, нагружают autovacuum, могут увести планировщик в худший план. Ищи неиспользуемые: ```sql SELECT relname, indexrelname, idx_scan FROM pg_stat_user_indexes WHERE idx_scan = 0 AND indexrelid NOT IN (SELECT conindid FROM pg_constraint) ORDER BY pg_relation_size(indexrelid) DESC; ``` ## Подводные камни / gotchas - **`date(col) = ...` / `col::date = ...`** убивает индекс по `col`. Всегда переписывай на полуинтервал `>= ... AND < ...`. - **Type mismatch из Go-кода.** Передавай параметры точным типом (`int64`, не `string`). Неявный каст применяется к колонке и отключает индекс. - **Порядок колонок в составном индексе.** `(a, b)` не помогает запросу `WHERE b = ?`. Не плоди индексы «на всякий случай» — проектируй под реальные запросы. - **Index-only scan не работает без vacuum.** `Heap Fetches` высокий → VM устарела. Это частая причина «почему covering index не ускоряет». - **GIN на write-heavy таблице** может стать узким местом из-за стоимости вставки и pending-list merge. Меряй `gin_pending_list_limit`. - **`CREATE INDEX` без `CONCURRENTLY`** берёт `SHARE` lock — блокирует запись в таблицу на всё время сборки. В проде почти всегда `CONCURRENTLY`. - **`CREATE INDEX CONCURRENTLY` нельзя в транзакции** и при сбое оставляет невалидный индекс, который надо дропнуть и пересоздать. - **HOT ломается**, если обновляемую колонку добавили в индекс (даже partial/expression). Внезапный рост bloat после добавления индекса — типичный симптом. - **`LIKE '%x%'`** требует `pg_trgm`, а не обычный B-tree. `text_pattern_ops` помогает только префиксу. - **NULLs в составном UNIQUE.** До PG 15 `NULL` считались различными → несколько строк с NULL проходят уникальность. PG 15+ — `UNIQUE NULLS NOT DISTINCT`. - **BRIN на перемешанных данных** бесполезен — нужна физическая корреляция. После `UPDATE`-heavy нагрузки корреляция деградирует. - **Дублирующие индексы.** `(a)` лишний при наличии `(a, b)` для поиска по `a`, но НЕ лишний, если по `(a)` нужен меньший index-only scan. Проверяй осознанно. - **Локаль collation.** Смена collation (например, обновление glibc) может сделать B-tree-индексы по text «битыми» по сортировке → нужен REINDEX. ## Вопросы на собеседовании **В:** Почему `UPDATE` одной неиндексированной колонки может всё равно записать во все индексы, и как этого избежать? **О:** MVCC создаёт новую версию строки с новым TID; по умолчанию все индексы должны указать на новый TID. Избежать помогает HOT-update: если ни одна изменяемая колонка не индексирована и на странице есть место (регулируется `fillfactor`), новая версия пишется на ту же страницу цепочкой, индексы не трогаются. **В:** В чём разница между Index Scan, Index Only Scan и Bitmap Heap Scan и когда планировщик выбирает каждый? **О:** Index Scan — обход индекса + случайный поход в heap за каждой строкой (хорош при высокой селективности, малом числе строк). Index Only Scan — данные целиком из индекса при all-visible странице (VM). Bitmap — строит битмап TID, сортирует по блокам, читает heap последовательно; хорош для средней селективности и комбинирования индексов через BitmapAnd/Or. **В:** Дан индекс `(a, b, c)`. Какие из запросов его используют: `WHERE b=1`; `WHERE a=1 AND c=3`; `WHERE a=1 AND b>2 AND c=3`? **О:** `b=1` — нет (b не лидирует). `a=1 AND c=3` — да, но только `a` как граница поиска, `c` фильтруется внутри. `a=1 AND b>2 AND c=3` — `a` и `b` ограничивают дерево, но после range по `b` колонка `c` уже не сужает поиск (только фильтр/порядок). **В:** Когда выберешь GIN, а когда GiST? **О:** GIN — когда значение содержит много искомых элементов и нагрузка read-heavy: jsonb, массивы, full-text, trgm. Быстрее ищет, но дороже на запись. GiST — для предикатов перекрытия/близости: геометрия, диапазоны, kNN (`ORDER BY <->`), exclusion constraints; дешевле на запись, lossy, поддерживает расстояние. **В:** Что нужно, чтобы `INCLUDE`-индекс реально дал index-only scan? **О:** Все возвращаемые колонки должны быть в индексе (ключ или INCLUDE), и страница heap должна быть all-visible в visibility map, которую обновляет VACUUM. Иначе будут `Heap Fetches > 0` и сканирование выродится в обычный index scan. **В:** Почему запрос `WHERE created_at::date = current_date` медленный и как починить? **О:** Каст к date применяется к колонке — индекс по `created_at` неприменим, выражение к тому же non-immutable. Переписать на sargable range: `created_at >= current_date AND created_at < current_date + 1`, тогда B-tree используется. **В:** Когда BRIN лучше B-tree и почему он такой маленький? **О:** BRIN хранит только min/max на диапазон блоков, а не запись на строку — отсюда крошечный размер. Лучше B-tree на огромных таблицах с физической корреляцией (append-only по времени), где допустимы recheck и не нужны точечные/уникальные поиски. При перемешанных данных бесполезен. **В:** Как безопасно добавить индекс на большую боевую таблицу и потом бороться с его bloat? **О:** `CREATE INDEX CONCURRENTLY` (вне транзакции, не блокирует запись; при сбое дропнуть INVALID и повторить). Для де-bloat — `REINDEX INDEX CONCURRENTLY`. Следить за `pg_stat_user_indexes.idx_scan`, чтобы выявлять неиспользуемые индексы. **В:** Почему партиальный уникальный индекс часто лучше обычного UNIQUE при soft delete? **О:** `CREATE UNIQUE INDEX ... WHERE deleted_at IS NULL` обеспечивает уникальность только среди живых строк, позволяя переиспользовать «занятые» значения после удаления, и при этом индекс компактнее и дешевле. ## На что копают на senior+ - Чтение `EXPLAIN (ANALYZE, BUFFERS)` вживую: отличить оценку от факта, увидеть `Rows Removed by Filter`, `Heap Fetches`, `Bitmap`, понять почему выбран Seq Scan (правильно ли это). - Глубокое понимание MVCC ↔ индексы: TID на версию, write amplification, HOT, роль visibility map в index-only scan, причины bloat и стратегия vacuum/autovacuum (`autovacuum_vacuum_scale_factor`, cost limits). - Проектирование составного индекса под конкретный набор запросов: equality-слева/range-справа, обслуживание `ORDER BY ... LIMIT` без Sort, выбор между ключом и INCLUDE. - Статистика и оценка кардинальности: `pg_stats`, `n_distinct`, `most_common_vals`, расширенная статистика `CREATE STATISTICS` для коррелированных колонок (функциональные зависимости, ndistinct). - Sargability: умение превращать несаргируемые предикаты (функции, касты, leading wildcard, OR) в индексируемые; знание `pg_trgm` для подстрок. - Эксплуатация: `CREATE/REINDEX CONCURRENTLY`, lock-семантика DDL, миграции без даунтайма, борьба с дублирующими/неиспользуемыми индексами, влияние collation/glibc на корректность B-tree. - Trade-off запись vs чтение: fillfactor, fastupdate для GIN, выбор класса операторов (`jsonb_path_ops` vs `jsonb_ops`, `text_pattern_ops`), partial-индексы для сужения write-стоимости. - Понимание, почему «добавить индекс» не всегда решение: иногда нужно переписать запрос, нормализовать тип параметра в Go, или сменить план через статистику.