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 - **Декларативное партиционирование** (`PARTITION BY`, появилось в PG 10, доработано в 11–14) разбивает одну логическую таблицу на физические партиции по ключу. Три стратегии: **RANGE** (диапазоны, чаще всего для time-series), **LIST** (явные списки значений), **HASH** (равномерное распределение по остатку хеша). - Главная выгода — **управление жизненным циклом данных**: удаление старых данных через `DROP TABLE`/`DETACH PARTITION` (метаданные, мгновенно) вместо `DELETE` (миллионы строк, dead tuples, нагрузка на vacuum). Плюс **partition pruning** — планировщик отсекает ненужные партиции и сканирует только релевантные. - **Partition pruning** работает только если ключ партиционирования присутствует в `WHERE` (или вычислим из условий). Есть pruning на этапе планирования (constraint exclusion / новый механизм) и **runtime pruning** для параметров prepared statements и nested loop. - **Ключевое ограничение**: любой `PRIMARY KEY` / `UNIQUE` обязан включать колонки ключа партиционирования. Глобальных уникальных индексов нет. FK на партиционированную таблицу можно (с PG 12), FK из партиционированной — тоже. - Партиционирование — **не про "ускорить любой запрос"**, а про обслуживаемость больших таблиц, локальность горячих данных и дешёвое удаление. Запросы без ключа партиционирования могут стать **медленнее** (скан всех партиций + накладные расходы планировщика). - Старое **наследование (table inheritance) + триггеры/CHECK** — legacy. Декларативное партиционирование почти всегда предпочтительнее; для автоматизации создания партиций используют **pg_partman**. ## Теория ### Что такое партиционирование и зачем оно Партиционированная таблица — это «фасад»: у неё нет собственного хранилища, данные физически лежат в **партициях** (отдельных таблицах). Маршрутизация строки в нужную партицию происходит по **ключу партиционирования** на стороне сервера автоматически при `INSERT`. Когда партиционирование оправдано: | Сценарий | Почему помогает | |---|---| | Очень большие таблицы (десятки/сотни ГБ, сотни млн строк) | Меньшие индексы и таблицы → лучше кэш-локальность, быстрее `VACUUM`/`ANALYZE` каждой партиции отдельно | | Time-series (логи, метрики, события) | RANGE по времени; данные пишутся в «свежую» партицию, старые — read-only | | Регулярное удаление старых данных (retention) | `DROP TABLE partition` / `DETACH` вместо массового `DELETE` — мгновенно, без bloat | | Данные с естественной группировкой (по тенанту, региону, статусу) | LIST/HASH → локальность, отдельное обслуживание | | Параллельная загрузка/обслуживание | Можно перестраивать индексы, делать `VACUUM` по партиции независимо | Когда **не** стоит: - Таблица маленькая/средняя — накладные расходы планировщика и сложность не окупаются. - Нет естественного ключа партиционирования, который попадает в `WHERE` большинства запросов. - Требуется глобальная уникальность по колонке, не входящей в ключ партиционирования (невозможно без ключа в индексе). ### RANGE партиционирование Самый частый случай — time-series. Границы задаются как `FROM (inclusive) TO (exclusive)`. ```sql -- Родительская (партиционированная) таблица CREATE TABLE events ( id bigint GENERATED ALWAYS AS IDENTITY, tenant_id bigint NOT NULL, created_at timestamptz NOT NULL, payload jsonb NOT NULL, -- ВАЖНО: ключ партиционирования (created_at) обязан входить в PK PRIMARY KEY (id, created_at) ) PARTITION BY RANGE (created_at); -- Месячные партиции; TO эксклюзивен → нет пересечения границ CREATE TABLE events_2026_05 PARTITION OF events FOR VALUES FROM ('2026-05-01') TO ('2026-06-01'); CREATE TABLE events_2026_06 PARTITION OF events FOR VALUES FROM ('2026-06-01') TO ('2026-07-01'); -- DEFAULT-партиция ловит всё, что не попало в диапазоны (страховка от ошибки INSERT) CREATE TABLE events_default PARTITION OF events DEFAULT; ``` Особенности RANGE: - Границы соседних партиций **не должны пересекаться**, иначе ошибка при создании. - `MINVALUE` / `MAXVALUE` обозначают «без нижней/верхней границы»: `FROM (MINVALUE) TO ('2026-01-01')`. - При многоколоночном ключе RANGE сравнение лексикографическое: первая колонка важнее. ### LIST партиционирование Явный список значений — удобно для дискретных категорий (регион, статус, тип). ```sql CREATE TABLE customers ( id bigint GENERATED ALWAYS AS IDENTITY, region text NOT NULL, name text NOT NULL, PRIMARY KEY (id, region) ) PARTITION BY LIST (region); CREATE TABLE customers_eu PARTITION OF customers FOR VALUES IN ('DE', 'FR', 'NL', 'PL'); CREATE TABLE customers_us PARTITION OF customers FOR VALUES IN ('US', 'CA'); CREATE TABLE customers_other PARTITION OF customers DEFAULT; ``` ### HASH партиционирование Равномерное распределение по `hash(key) % modulus`. Используется, когда нужна локальность/параллелизм, но нет естественных диапазонов/списков (например, шардирование по `tenant_id`). ```sql CREATE TABLE measurements ( device_id bigint NOT NULL, ts timestamptz NOT NULL, value double precision NOT NULL, PRIMARY KEY (device_id, ts) ) PARTITION BY HASH (device_id); -- 4 партиции: modulus одинаковый, remainder 0..3 CREATE TABLE measurements_p0 PARTITION OF measurements FOR VALUES WITH (MODULUS 4, REMAINDER 0); CREATE TABLE measurements_p1 PARTITION OF measurements FOR VALUES WITH (MODULUS 4, REMAINDER 1); CREATE TABLE measurements_p2 PARTITION OF measurements FOR VALUES WITH (MODULUS 4, REMAINDER 2); CREATE TABLE measurements_p3 PARTITION OF measurements FOR VALUES WITH (MODULUS 4, REMAINDER 3); ``` Минусы HASH: нельзя дёшево «удалить старое» (нет диапазонов), DEFAULT-партиция запрещена, изменение числа партиций требует пересоздания/перераспределения данных. ### Partition pruning (отсечение партиций) Это механизм, при котором планировщик/исполнитель **исключает партиции**, которые заведомо не содержат искомых строк. Это главный источник ускорения запросов. Два этапа: 1. **Plan-time pruning** — на этапе планирования, когда значения в `WHERE` известны как константы. 2. **Run-time pruning** (PG 11+) — на этапе исполнения, когда значения становятся известны только в рантайме: параметры prepared statement (`$1`), `generic plan`, значения от внешней стороны nested loop join, подзапросы. ```sql -- Pruning сработает: created_at в WHERE → планировщик трогает только events_2026_06 EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM events WHERE created_at >= '2026-06-01' AND created_at < '2026-06-10'; ``` В плане для эффективного случая видно только нужную партицию (или `Append`/`Subplans Removed: N` при runtime pruning): ``` Append -> Seq Scan on events_2026_06 events_1 Filter: (created_at >= ... AND created_at < ...) (остальные партиции отсечены) ``` ```sql -- ПЛОХО: нет ключа партиционирования → скан ВСЕХ партиций SELECT * FROM events WHERE payload->>'type' = 'login'; ``` Связанные параметры: - `enable_partition_pruning` (по умолчанию `on`) — общий выключатель нового механизма. - `constraint_exclusion` — старый механизм (для наследования и `UNION ALL`); для декларативного партиционирования основной путь — именно `enable_partition_pruning`. - `enable_partitionwise_join` / `enable_partitionwise_aggregate` (по умолчанию `off`) — позволяют джойнить/агрегировать пары совпадающих партиций по отдельности; включают вручную, т.к. растёт расход памяти на планирование. Важно: **функции от ключа ломают pruning**. `WHERE date_trunc('day', created_at) = '...'` не отсечёт партиции — нужно сравнивать сам `created_at` с границами диапазона. То же про неявные приведения типов и `created_at::date = '...'`. Проверка runtime pruning с prepared statement: ```sql PREPARE q(timestamptz, timestamptz) AS SELECT count(*) FROM events WHERE created_at >= $1 AND created_at < $2; EXPLAIN (ANALYZE) EXECUTE q('2026-06-01', '2026-06-02'); -- В плане появится "Subplans Removed: N" — это runtime pruning ``` ### Индексы на партициях С PG 11 индекс, созданный на **партиционированной таблице**, является «шаблоном»: автоматически создаётся на каждой существующей и будущей партиции (партиционированный индекс). ```sql -- Создаст локальные индексы на всех партициях events CREATE INDEX ON events (tenant_id, created_at); ``` Нюансы: - Глобальных индексов в PostgreSQL **нет** — каждый индекс локален для партиции. - `CREATE INDEX ... CONCURRENTLY` напрямую на партиционированной таблице **не поддерживается**. Паттерн: создать индекс `CONCURRENTLY` на каждой партиции по отдельности, затем `CREATE INDEX ON parent (...) ONLY` (создаст «инвалидный» родительский индекс) и `ALTER INDEX parent_idx ATTACH PARTITION child_idx` для каждого — после привязки всех партиций родительский индекс становится валидным. ### Уникальность, PK и ON CONFLICT ```sql -- ОШИБКА: unique constraints on partitioned tables must include all partitioning columns CREATE TABLE t (id bigint PRIMARY KEY, d date) PARTITION BY RANGE (d); -- ERROR: insufficient columns in PRIMARY KEY constraint definition ``` Правило: **`PRIMARY KEY` и `UNIQUE` обязаны включать все колонки ключа партиционирования.** Причина — отсутствие глобального индекса: уникальность гарантируется только в пределах партиции, поэтому ключ партиционирования должен быть частью ограничения, чтобы дубликаты не могли «разъехаться» по разным партициям. Следствия для `ON CONFLICT`: - `ON CONFLICT (col) DO ...` требует, чтобы был соответствующий уникальный индекс/констрейнт — а значит, `col` должен включать ключ партиционирования. - `ON CONFLICT DO NOTHING` без указания конфликтного таргета работает; с таргетом — только если такой констрейнт существует на родителе. ### Внешние ключи (FK) - **FK из обычной таблицы на партиционированную** — поддерживается с **PG 12** (раньше было нельзя). Внутренне создаются триггеры на каждую партицию. - **FK из партиционированной таблицы на другую** — поддерживается. - FK всё так же требует, чтобы целевые колонки имели уникальный индекс — а тот должен включать ключ партиционирования. ### Обслуживание: ATTACH / DETACH, заблаговременное создание ```sql -- Создать таблицу отдельно, наполнить, затем подключить как партицию CREATE TABLE events_2026_07 (LIKE events INCLUDING ALL); -- ... массовая загрузка в events_2026_07 без нагрузки на родителя ... -- ATTACH: PG проверяет, что данные подходят под границы. -- Чтобы избежать полного сканирования на проверку, заранее создаём CHECK, -- совпадающий с будущими границами: ALTER TABLE events_2026_07 ADD CONSTRAINT ck CHECK (created_at >= '2026-07-01' AND created_at < '2026-08-01') NOT VALID; ALTER TABLE events_2026_07 VALIDATE CONSTRAINT ck; ALTER TABLE events ATTACH PARTITION events_2026_07 FOR VALUES FROM ('2026-07-01') TO ('2026-08-01'); -- DETACH: отвязать партицию (станет обычной таблицей). PG 14+: CONCURRENTLY без долгой блокировки ALTER TABLE events DETACH PARTITION events_2026_05 CONCURRENTLY; -- Retention: удалить старое — мгновенно, без DELETE DROP TABLE events_2026_05; ``` Блокировки: - `ATTACH PARTITION` берёт `SHARE UPDATE EXCLUSIVE` на родителе (с PG 12 не блокирует чтения/записи), но `ACCESS EXCLUSIVE` на присоединяемой таблице. - `DETACH PARTITION` обычный — `ACCESS EXCLUSIVE` на родителе (блокирует всех). `DETACH ... CONCURRENTLY` (PG 14+) снимает эту проблему. - Создание партиции через `CREATE TABLE ... PARTITION OF` берёт `ACCESS EXCLUSIVE` на родителе на короткое время. Создавать партиции **заранее** обязательно: если строка не попадает ни в одну партицию и нет DEFAULT, `INSERT` упадёт с ошибкой `no partition of relation ... found for row`. DEFAULT-партиция спасает, но: вставка в неё при наличии большого объёма усложняет последующий `ATTACH` (PG сканирует DEFAULT, проверяя, что её данные не конфликтуют с новой партицией) и берёт сильную блокировку. ### pg_partman Расширение для автоматизации: создаёт будущие партиции по расписанию (фоновый воркер или `run_maintenance()`), удаляет/архивирует старые по retention-политике, поддерживает RANGE (time и serial). ```sql CREATE EXTENSION pg_partman; SELECT partman.create_parent( p_parent_table := 'public.events', p_control := 'created_at', p_interval := '1 month', p_premake := 4 -- держать 4 партиции «в запасе» вперёд ); -- Retention: автоматически дропать партиции старше 6 месяцев UPDATE partman.part_config SET retention = '6 months', retention_keep_table = false WHERE parent_table = 'public.events'; -- Запускать периодически (cron/pg_cron): SELECT partman.run_maintenance('public.events'); ``` ### Многоуровневое (sub-) партиционирование Партиция сама может быть партиционированной таблицей. Типичный паттерн: RANGE по времени, внутри — HASH/LIST по тенанту. ```sql CREATE TABLE events2 ( tenant_id bigint NOT NULL, created_at timestamptz NOT NULL, PRIMARY KEY (tenant_id, created_at) ) PARTITION BY RANGE (created_at); CREATE TABLE events2_2026_06 PARTITION OF events2 FOR VALUES FROM ('2026-06-01') TO ('2026-07-01') PARTITION BY HASH (tenant_id); CREATE TABLE events2_2026_06_h0 PARTITION OF events2_2026_06 FOR VALUES WITH (MODULUS 4, REMAINDER 0); -- ... h1..h3 ``` Осторожно: число листовых партиций растёт мультипликативно (12 месяцев × 4 хеша = 48). Слишком много партиций (тысячи) перегружает планировщик — растёт время планирования и потребление памяти. ### Отличие от старого наследования (table inheritance) | Аспект | Декларативное (PG 10+) | Наследование (legacy) | |---|---|---| | Маршрутизация INSERT | Автоматически по ключу | Вручную (триггеры/правила) | | Границы | Декларативные, без пересечений | Через `CHECK` руками, можно ошибиться | | Pruning | `enable_partition_pruning`, runtime pruning | Только `constraint_exclusion`, нет runtime | | Индексы/PK | Партиционированный индекс, PK с ключом | Нет общего PK, индексы вручную на каждой | | FK | Поддержаны (PG 12+) | Плохо | | `ATTACH`/`DETACH` | Да | Нет (только `INHERIT`/`NO INHERIT`) | | Накладные расходы | Меньше, оптимизировано | Больше, триггеры медленные | Наследование сегодня нужно лишь в редких случаях (нерегулярные/перекрывающиеся границы, добавление колонок только в дочерние). Для новых систем — только декларативное партиционирование. ### Влияние на запросы и планы (Go-примеры) С точки зрения приложения партиционированная таблица — обычная таблица: `INSERT/SELECT` идут к родителю, маршрутизация прозрачна. ```go // database/sql — вставка идёт в родителя, PG сам маршрутизирует в партицию. // Ключ партиционирования (created_at) должен быть валидным значением, // иначе при отсутствии нужной партиции и DEFAULT — ошибка. _, err := db.ExecContext(ctx, ` INSERT INTO events (tenant_id, created_at, payload) VALUES ($1, $2, $3) `, tenantID, time.Now().UTC(), payload) if err != nil { // pq: no partition of relation "events" found for row return fmt.Errorf("insert event: %w", err) } ``` ```go // pgx — ОБЯЗАТЕЛЬНО передавать ключ партиционирования в WHERE, // чтобы сработал partition pruning и не сканировались все партиции. rows, err := pool.Query(ctx, ` SELECT id, payload FROM events WHERE tenant_id = $1 AND created_at >= $2 -- ключ партиционирования AND created_at < $3 -- даёт RANGE pruning ORDER BY created_at DESC LIMIT 100 `, tenantID, from, to) ``` ```go // Анти-паттерн: вычисляемое выражение от ключа ломает pruning. // WHERE date_trunc('month', created_at) = '2026-06-01' -- НЕ отсекает партиции // Правильно — сравнивать сам ключ с границами диапазона: // WHERE created_at >= '2026-06-01' AND created_at < '2026-07-01' ``` Замечания по планам: - При большом числе партиций даже с pruning время **планирования** заметно. Помогает `plan_cache_mode` + prepared statements (runtime pruning). - `EXPLAIN (ANALYZE)` показывает реально просканированные партиции; `Subplans Removed: N` — отсечённые в рантайме. - Глобальной сортировки по индексу нет «бесплатно»: `ORDER BY` через несколько партиций может потребовать `Merge Append` (если у каждой партиции подходящий индекс) или внешней сортировки. ## Подводные камни / gotchas - **Запросы без ключа партиционирования сканируют все партиции** и могут стать медленнее, чем были на одной таблице. Партиционирование выбирают под паттерн доступа. - **PK/UNIQUE без ключа партиционирования невозможны** → теряете глобальную уникальность по «бизнес-ключу» (например, `email`), если он не входит в ключ. Решения: вынести проверку в приложение, использовать HASH по самому уникальному столбцу, либо отдельную справочную таблицу. - **`ON CONFLICT` по произвольной колонке не работает** — нужен констрейнт, включающий ключ партиционирования. - **Функции/касты над ключом в `WHERE` убивают pruning** (`created_at::date`, `date_trunc(...)`, неявные приведения `timestamp` vs `timestamptz`). Сравнивайте сырой ключ с границами. - **Забыли создать партицию заранее** → `INSERT` падает (`no partition found for row`). Нужны pg_partman/cron или DEFAULT-партиция как страховка. - **DEFAULT-партиция — ловушка для производительности**: большой объём в ней замедляет/блокирует `ATTACH` новых партиций (PG сканирует DEFAULT на конфликты под `ACCESS EXCLUSIVE`). - **`ATTACH` без заранее подготовленного `CHECK`** делает полное сканирование подключаемой таблицы под блокировкой. Готовьте `NOT VALID` + `VALIDATE` заранее. - **`DETACH PARTITION` (без CONCURRENTLY) берёт `ACCESS EXCLUSIVE`** на родителе — блокирует весь трафик к таблице. На проде используйте `DETACH ... CONCURRENTLY` (PG 14+). - **`CREATE INDEX CONCURRENTLY` нельзя на родителе** — только на партициях по отдельности с последующим `ATTACH PARTITION` индекса. - **Слишком много партиций (тысячи)** раздувают время планирования и память; обновление статистики и autovacuum по множеству объектов тоже стоит ресурсов. Балансируйте гранулярность. - **Перемещение строки между партициями** (`UPDATE`, меняющий ключ) реализовано как `DELETE`+`INSERT` — дороже и может ловить триггеры BEFORE/AFTER неожиданно; до PG 11 было запрещено вовсе. - **`TRUNCATE` родителя** очищает все партиции; будьте внимательны в скриптах обслуживания. - **`enable_partitionwise_join/aggregate` выключены по умолчанию** — для аналитики по совпадающим партициям их нужно включать осознанно (рост памяти планировщика). ## Вопросы на собеседовании **В:** Чем декларативное партиционирование отличается от наследования таблиц? **О:** Декларативное (PG 10+) даёт автоматическую маршрутизацию INSERT по ключу, декларативные непересекающиеся границы, партиционированные индексы и PK (с включением ключа), FK, `ATTACH/DETACH`, современный pruning с runtime-режимом. Наследование требует ручных триггеров/правил для маршрутизации, `CHECK`-констрейнтов вручную, не имеет общего PK и опирается лишь на `constraint_exclusion` без runtime pruning. Для новых систем — только декларативное. **В:** Почему PRIMARY KEY обязан включать ключ партиционирования? **О:** В PostgreSQL нет глобальных индексов — каждый индекс локален для партиции, и уникальность гарантируется только в пределах партиции. Если бы PK не содержал ключ партиционирования, два одинаковых значения могли бы оказаться в разных партициях и не нарушили бы локальную уникальность. Включение ключа в PK гарантирует, что строки с конфликтующим ключом попадают в одну партицию. **В:** Что такое partition pruning и когда он не срабатывает? **О:** Это отсечение партиций, заведомо не содержащих искомых строк. Работает на этапе планирования (константы в WHERE) и в рантайме (параметры `$1`, generic plan, внешняя сторона nested loop). Не срабатывает, когда ключа партиционирования нет в условии, либо когда над ним применены функции/касты (`created_at::date`, `date_trunc`) или есть несовпадение типов, мешающее сравнению с границами. **В:** Как удалять старые данные в time-series таблице эффективно? **О:** Через `DROP TABLE partition` или `DETACH PARTITION` (а затем архивирование/дроп) вместо массового `DELETE`. `DROP` — это операция над метаданными: мгновенно, без генерации dead tuples, без нагрузки на vacuum и без раздувания индексов. `DELETE` миллионов строк создаёт bloat и долгую работу autovacuum. **В:** Как создать индекс на партиционированной таблице без долгой блокировки записи? **О:** `CREATE INDEX CONCURRENTLY` на родителе не поддерживается. Нужно: создать индекс `CONCURRENTLY` на каждой партиции отдельно, затем создать «шаблонный» индекс на родителе через `CREATE INDEX ON parent (...) ONLY` (он будет невалидным), и `ALTER INDEX ... ATTACH PARTITION` привязать к нему все партиционные индексы. После привязки всех партиций родительский индекс становится валидным. **В:** Какие риски у DEFAULT-партиции? **О:** Она полезна как страховка от потери строк, но: при `ATTACH` новой партиции PostgreSQL сканирует DEFAULT под `ACCESS EXCLUSIVE`, проверяя, что её содержимое не пересекается с границами новой партиции — при больших объёмах это долго и блокирует. Поэтому DEFAULT не должна накапливать много данных; партиции лучше создавать заранее (pg_partman/cron). **В:** Когда HASH-партиционирование, а когда RANGE? **О:** RANGE — когда есть естественный диапазонный ключ (время, числовые серии) и нужны retention/pruning по диапазонам. HASH — когда нужна равномерная нагрузка/локальность без диапазонов (например, по `tenant_id` или `device_id`), но при этом теряется дешёвый retention (нельзя «дропнуть старое»), и изменение числа партиций требует перераспределения данных. DEFAULT для HASH не разрешён. **В:** Может ли партиционирование замедлить запросы? **О:** Да. Запросы без ключа партиционирования сканируют все партиции и добавляют накладные расходы. При большом числе партиций растёт время планирования и потребление памяти. `ORDER BY`/агрегации поверх многих партиций могут требовать `Merge Append` или внешней сортировки. Партиционирование оправдано только под конкретный паттерн доступа и объём. **В:** Поддерживаются ли внешние ключи на партиционированную таблицу? **О:** FK *из* партиционированной таблицы — давно. FK *на* партиционированную таблицу — с PG 12 (внутренне через триггеры на каждую партицию). Целевые колонки должны иметь уникальный индекс, а тот обязан включать ключ партиционирования. **В:** Что такое run-time pruning и зачем он нужен? **О:** Это отсечение партиций на этапе исполнения, когда значения становятся известны не при планировании, а в рантайме: параметры prepared statements, generic plans, значения от внешней стороны nested loop join, подзапросы. В `EXPLAIN ANALYZE` отображается как `Subplans Removed: N`. Нужен, потому что иначе prepared statement с generic plan был бы вынужден сканировать все партиции. ## На что копают на senior+ - **Стоимость планирования при тысячах партиций**: как `plan_cache_mode`, generic vs custom plan и runtime pruning взаимодействуют; почему массовое число партиций — анти-паттерн, и где проходит практический предел (обычно сотни, не тысячи). - **Locking-модель обслуживания**: какие блокировки берут `CREATE/ATTACH/DETACH PARTITION` в разных версиях PG, как `DETACH CONCURRENTLY` (PG 14) и `ATTACH` без full-scan (через подготовленный `CHECK NOT VALID`) делают онлайн-обслуживание безопасным. - **Эволюция фич по версиям**: PG 10 — базовое декларативное; PG 11 — default-партиция, hash, обновление ключа с перемещением строки, партиционированные индексы/PK, runtime pruning; PG 12 — FK на партиционированную, ускорение pruning и `COPY`; PG 13 — partition-wise оптимизации, BEFORE row-триггеры; PG 14 — `DETACH CONCURRENTLY`, ускорение обновления статистики. - **partition-wise join/aggregate**: когда включать, требования к совпадению схем партиционирования обеих таблиц, влияние на память планировщика. - **Стратегия глобальной уникальности** при отсутствии глобальных индексов: вынос в приложение, HASH по уникальному ключу, отдельная dedup-таблица, или сознательный отказ. - **Взаимодействие с autovacuum/статистикой**: per-partition статистика, `ANALYZE` родителя vs партиций, как раздутая DEFAULT-партиция и неравномерное распределение по HASH влияют на оценки кардинальности и планы. - **Архитектурный выбор vs шардинг**: партиционирование — внутри одного инстанса; для горизонтального масштабирования за пределами одной машины нужны Citus/логический шардинг — где граница и как они комбинируются.